[安全] 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 之类的事情,感性忽略了

可以看到传入加密函数的还有两个关键参数 KEYTable
进去加密函数看两眼

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 函数
ohno
哦太大了无法加载,那这种函数就不要想着看了,加载出来也看不懂(人脑的上下文确实比较低(

难道就没办法获取 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!}