先看看pwntools的shellcraft.amd64.sh()
为我们提供了怎样的一段代码
1 | /* execve(path='/bin///sh', argv=['sh'], envp=0) */ |
可以看到这段代码是为了执行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 | 0000000000401000 <_start>: |
它借助了栈来实现执行了execve("/bin///sh","sh",0)
。但是里面采用了很多怪怪的操作,我们试试能不能通过优化这些操作来减少shellcode的大小。
在将异或操作改成直接push 0x6873后,手动编译成功的文件照样成功运行了。但是在objdump的时候就能发现,shellcode里出现了\x00:
1 | 401010: 68 73 68 00 00 push 0x6873 |
不太妙,看来pwntools成为经典是有原因的。那干脆如果允许读入\x00,我们能否再精简shellcode呢?我们做一些尝试:
1 | global _start |
这样写是不行的,syscall并不会进入shell。了解之后知道,execve
的第二个参数必须以一个指向空的指针结尾,也就是说第二个参数也要传入一个合法的地址。查到了一个这样的shellcode:
1 | 401000: 48 31 d2 xor rdx,rdx |
看完确实令人感叹。这个shellcode的巧妙在于,将第二个参数和第三个参数的设置过程进行了融合复用,环境变量数组为0看来是合法的,那就将0先入栈然后将地址传给rsi,同时这样写也实现了’/bin/sh’最后在栈中以\x00结束,非常的巧妙。我们看到这样写的shellcode只有26字节大小,只是在mov eax,0x3b设置系统调用的地方有三个\x00。如果这里我们使用pwntools的栈的方法优化一下?
1 | 401000: 48 31 d2 xor rdx,rdx |
没有\x00!而且只要24字节就能实现。不觉得这很酷吗?我觉得这太酷了。shellcode也没那么复杂。
x86传参,网上看到的教程似乎都是__fastcall调用方式,最终两个参数好像是用ecx和ebx在传递而不是栈。逻辑也是类似的。