[安全] zig-zag 题解
2025 “网安卫士” 院赛 zig-zag WP
Zig 编译出来的程序,和 Rust 一样也有很多 “噪声” 代码,可以用人脑过滤一下(
以下展示代码片段均通过一定程度的符号恢复,很简单的自己看多两眼也能明白变量的意思
通过输出的字符串先定位到 main.main 函数,前面很多东西,从后面开始看
if ((mem.eql__anon_7366(arg1, &__anon_30974, 0x38, EncInput, len_EncInput) & 1)
!= 0)
int16_t rax_23 = fs.File.writeAll(arg1, &stdout, "right!!!\n", 9)
if (rax_23 != 0)
builtin.returnError(arg1)
int64_t* rax_25
rax_25.w = rax_23
return rax_25
else
int16_t rax_24 = fs.File.writeAll(arg1, &stdout, "wrong. xwx\n", 0xb)
if (rax_24 != 0)
builtin.returnError(arg1)
int64_t* rax_26
rax_26.w = rax_24
return rax_26
return 0
可以看到后面是一个字符串比较和输出
再往前看可以看到这个被加密的输入是怎么来的
void* EncInput
int64_t len_EncInput
EncInput, len_EncInput = main.bufEncrypt(
rol.q(rol.q(rol.q(rol.q(arg1, 3), 0xd), 0x3d), 0x33), &KEY, Input: clrInput,
len_Input: len_clrInput, &Table, 0x40)
int64_t len_EncInput_1 = len_EncInput
void* EncInput_1 = EncInput传入了和原输入相关的参数到一个加密函数里
往上继续追溯这个与输入相关的参数 clrInput,发现上面的操作只是做了一点例如 strip 之类的事情,感性忽略了
可以看到传入加密函数的还有两个关键参数 KEY 和 Table
进去加密函数看两眼
void* main.bufEncrypt(int64_t arg1, int64_t KEY, char* Input, int64_t len_Input, void* Table, int64_t arg6)
int64_t len_Input_1 = len_Input
char* Input_1 = Input
int64_t var_38 = arg6
void* Table_1 = Table
for (int64_t i = 0; i u< len_Input; i += 1)
int64_t i_1 = i
char* rcx_2
rcx_2.b = Input[i]
char var_9_1 = rcx_2.b
int64_t var_98_1 = arg6
if (i u>= arg6)
debug.FullPanic((function 'defaultPanic')).outOfBounds(arg1, i)
noreturn
int64_t rax_7
rax_7.b = rcx_2.b
uint64_t rax_8 = zx.q(rax_7.b)
if (rax_8 u>= 0x100)
debug.FullPanic((function 'defaultPanic')).outOfBounds(arg1, rax_8)
noreturn
uint64_t rax_10 = zx.q(*(KEY + rax_8))
if (rax_10 u>= 0x100)
debug.FullPanic((function 'defaultPanic')).outOfBounds(arg1, rax_10)
noreturn
int64_t rcx_8
rcx_8.b = *(KEY + rax_10)
*(Table + i) = rcx_8.b
if (0 u> len_Input)
debug.FullPanic((function 'defaultPanic')).startGreaterThanEnd(arg1, 0)
noreturn
if (len_Input u> arg6)
debug.FullPanic((function 'defaultPanic')).outOfBounds(arg1, len_Input)
noreturn
if (0 u<= len_Input)
return Table
debug.FullPanic((function 'defaultPanic')).outOfBounds(arg1, 0)
noreturn一些编译器加的复杂错误处理和一些简单的逻辑,可以稍微还原成 Python 代码比较好理解一点
def bufEncrypt(Key: list[int], Input: list[int], lenInput: int, Table: list[int], lenTable: int) -> tuple[list[int], int]:
Result, lenResult = Table, lenInput
assert(lenTable >= lenInput)
for i, v in enumerate(Input):
ni = Key[v]
Result[i] = Key[ni]
return Result, lenResult差不多这样,加密实际上就是在 Key 表里面做两次索引,然后那个 Table 实际上就是返回值,加密函数中会被覆写,原数据不用管
那么接下来该关注 KEY 这个变量了
void KEY
main.createTable(&KEY, arg1, "!i!i!i!p0w3rFu1z1g!i!i!i!", 0x19)在 main 函数的开头部分就初始化了,而且初始化和输入没有任何关系,那这就差不多是一个完全被我们掌握的表了
再看看 main.createTable 函数

哦太大了无法加载,那这种函数就不要想着看了,加载出来也看不懂(人脑的上下文确实比较低(
难道就没办法获取 KEY 了吗?
哦其实由于它的值是恒定的,所以我们可以在它初始化之后直接读这个值
下面使用 pwndbg 来 dump
b *0x010de0cc
r
set $key_addr = $rdi
ni
dump binary memory ./key.bin $key_addr $key_addr+0x100大概的意思是
- 在
0x010de0cc处打一个断点,这个地方就是010de0cc e81f050000 call main.createTable的地方 - 然后跑到断点处
- zig 使用 System V AMD64 ABI 调用约定,第一个参数放在
rdi里 - 设置一个变量把参数存起来
- ni 步过函数,确保完成初始化
- 把
KEY的值 dump 出来,通过上下文我们知道它的大小是0x100
要素齐全了
加密函数只是两次索引,表的长度和密文的长度都不大,暴力跑一下是没问题的
Table = [0xAA for _ in range(0x40)]
def bufDecrypt(Key: list[int], Result: list[int]) -> list[int]:
flag = []
for v in Result:
for ni, u in enumerate(Key):
if u == v:
key_v = ni
break
for org_v, key_u in enumerate(Key):
if key_u == key_v:
flag_v = org_v
break
flag.append(flag_v)
return flag
with open("./zig-zag/key.bin", "rb") as f:
Key = f.read()
Key = list(Key)
EncInputB64 = "UcvibmHiUjzL16eIhwdfCDALgIgvLV8IMBtsX9Z9Xw88bBtfBrhgXxtsBrhuxmBuX8Y2Xy/NOiA="
EncInput = list(base64.b64decode(EncInputB64))
flag = bufDecrypt(Key, EncInput)
print(bytes(flag))flag 是
flag{aN@ly21n9_c0mp1Ex_c0d3_i5_h@3d_bU7_d3bUgI7g_Is_Ez!}