跳至主要内容

ZeroG_CTF2026

Reverse

Docking Check

  • 截图 202606171551
  • 红线部分是编译器优化
  • 实际上v7就应该是strlen(input)
  • 加密方式也给出来了,就可以写脚本了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
enc = [0x77, 0x4C, 0x3A, 0xD6, 0xE0, 0x27, 0xD5, 0x33, 0x14, 0xD6, 0xFA, 0xE9, 0xE0, 0x37, 0x29, 0x7E, 0x3D, 0x94, 0x6D, 0xB3, 0x7A, 0x56, 0xA0, 0xBA, 0xBF, 0x07, 0xA3, 0x7B]
flag = []

def ror1(v,n):
v &= 0xff
n %= 8
return ((v >> n) | ( v << (8 - n))) & 0xff


n1 = 23;
n2 = 6;
n3 = 60;

for i in range(28):
tmp = enc[i] ^ n3
tmp = tmp ^ 0xa5
tmp = ror1(tmp,i%7+1)
tmp = (tmp - n1) & 0xff
s_i = (tmp ^ n2) & 0xff;
flag.append(chr(s_i))
n2 += 13;
n1 += 17;
n3 = enc[i]

print(''.join(flag))

Nebula VM

  • 根据题目并把文件拖到IDA中分析可知这是一个vm题
  • 截图 202606171553
  • 这里可以看到对opcode进行了个解密
  • 截图 202606171556
  • 这里可以看到把v49简单初始化了一下
  • 然后用v17代替v9作为了解密后的opcode
  • 然后就可以分析这个vm在干什么了,可以用ai进行辅助分析
  • 然后写一个脚本
  • 这里可以利用flag是可输入字符进行爆破
  • 因为这个vm是逐个字符进行比对的,发现一个字符不对就立刻退出vm
  • 可以利用这个特性进行爆破,看哪些可输入字符串可以跑vm更远,知道跑完
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#!/usr/bin/env python3
"""
Nebula VM 暴力破解脚本
逐字节爆破,利用 VM 失败时机(PC 值)判断每个字节是否正确
"""

# 加密的字节码数据(从 IDA 的 byte_2160 读取)
ENCRYPTED = [
0xb8,0xce,0xf2,0x39,0x3f,0x69,0xa6,0xaf,0xd4,0xd3,0x1e,0x68,0x40,0x8c,0xa9,0xe3,
0xf1,0x5a,0x7b,0x60,0x87,0x47,0x9c,0xf6,0x2d,0x5b,0x66,0x9c,0x9b,0xd4,0xea,0x09,
0x59,0x7c,0xa3,0xa9,0x85,0x33,0x34,0x5e,0x55,0x82,0x45,0xc4,0x12,0x3c,0xa7,0x23,
0x80,0xa6,0xeb,0x11,0x35,0x68,0x6c,0xac,0xff,0xfa,0x15,0x0c,0x4b,0x03,0xa3,0xdb,
0xc8,0x1e,0x12,0x05,0x6e,0x81,0xe4,0x30,0x75,0x13,0x7e,0xb4,0x83,0xef,0xe8,0x33,
0x2d,0x54,0xaa,0x85,0xc6,0xd8,0xb8,0x34,0x4d,0xab,0xb6,0xfd,0x0c,0x05,0x11,0x4b,
0x46,0xde,0xe3,0xc9,0x3e,0x70,0x50,0x9f,0x85,0xaf,0xec,0x15,0x76,0x7d,0xb8,0x22,
0xc2,0xc6,0x1b,0x41,0x57,0xdd,0xbd,0xe6,0x3d,0xe5,0x36,0x4c,0xab,0x95,0xe0,0x3e,
0x49,0x0e,0x6c,0xfb,0xff,0xa6,0x65,0x6f,0xab,0x11,0xde,0x84,0x93,0x4c,0xd8,0x22,
0x30,0xd6,0x75,0xe1,0x46,0x78,0x0d,0x37,0xeb,0xaa,0xa6,0x32,0x59,0x14,0x37,0xca,
0x99,0x65,0x61,0x69,0x0e,0xe0,0xf4,0xd7,0x94,0x43,0x6e,0xca,0x93,0xfd,0x98,0xb2,
0x41,0x61,0x1a,0xd1,0x77,0xaa,0xac,0x46,0x3c,0x3a,0x2e,0xce,0x8b,0xa5,0x70,0x1b,
0xd0,0xdf,0xd3,0xb9,0x60,0x00,0x25,0x0f,0xe4,0xd2,0xb6,0x45,0x43,0xbd,0x2a,0xf3,
0xd3,0x97,0x6a,0x54,0x04,0x18,0xfd,0xe7,0x8d,0xeb,0x77,0x3c,0x1b,0x0b,0x90,0x8a,
0x79,0x4f,0x22,0x00,0xcf,0xb0,0x36,0x5d,0x45,0x21,0x2f,0xd5,0x81,0xbf,0x78,0x42,
0x11,0x06,0xf5,0x80,0x96,0x68,0xb3,0x67,0xfc,0xda,0xa7,0x8d,0x61,0x64,0x20,0x48,
0x0b,0x4e,0x76,0xb8,0xbf,0xaf,0x27,0x2f,0x54,0x42,0x9e,0xf4,0xd2,0x0d,0x28,0xac,
0x31,0x97,0xca,0xf0,0x07,0x33,0x7c,0x77,0x69,0xe8,0xe6,0x18,0x1a,0x54,0x1c,0x88,
0xd9,0xff,0x32,0x29,0x40,0xa1,0xb5,0xdf,0x1a,0x42,0x2f,0x75,0x82,0xbc,0xd4,0xc3,
0x01,0xf3,0x58,0x91,0xb1,0xe9,0xec,0x8e,0x7e,0x7a,0x97,0x9d,0xcb,0x85,0x31,0x5a,
0x49,0x51,0xd2,0xf8,0xdf,0x11,0x64,0x42,0x95,0x92,0x18,0x07,0x03,0x6b,0x69,0xb3,
0x37,0xd5,0x2a,0x00,0x57,0x58,0xa0,0xa6,0xcc,0x2a,0xf9,0x3d,0x5a,0xb4,0x81,0xcb,
0x37,0x3e,0x62,0xbe,0x8d,0xf0,0xd5,0x1e,0x05,0xa1,0x6d,0x95,0xf2,0xec,0x38,0x15,
0x50,0x47,0x9a,0x0e,0x97,0x29,0x0c,0x76,0xbd,0x95,0xd6,0xcd,0x23,0x26,0x60,0xb9,
0xc8,0x8e,0x52,0x7a,0x7f,0x21,0xf4,0xef,0xf7,0x83,0x5f,0x05,0xdc,0x8c,0xe9,0x93,
0xa0,0x56,0x1a,0x01,0xc7,0xe1,0xbe,0xb7,0x6e,0x2b,0x26,0x20,0xd8,0x94,0xb1,0x5b,
0x19,0xa2,0xf3,0xe8,0x8f,0xaf,0x34,0x1e,0x25,0xd3,0xee,0xa4,0x73,0x7c,0x32,0x01,
0xc1,0xe2,0x9b,0x51,0x6d,0x2b,0x2c,0xc6,0xad,0xba,0x1a,0x5c,0x0a,0x24,0x3f,0xdb,
0x88,0x6e,0x43,0x39,0x0d,0xe0,0xa4,0xb4,0x57,0x52,0x3b,0xc4,0xc3,0x9b,0xab,0x73,
0x50,0x06,0xea,0x35,0x96,0x99,0x7c,0xa8,0x4d,0xeb,0xc6,0xac,0x9b,0x67,0x70,0x0b,
0xb5,0xfc,0xa2,0x8f,0x4e,0x30,0x40,0xdc,0xc5,0xa3,0xbe,0x55,0xd0,0x2d,0xf9,0xc3,
0x5e,0xc6,0x5b,0x31,0xe9
]


def decrypt():
"""解密字节码"""
bc = bytearray(501)
key = 0xA9
for i in range(501):
bc[i] = ((i >> 1) ^ ((key ^ ENCRYPTED[i]) & 0xFF)) & 0xFF
key = (key + 37) & 0xFF
return bytes(bc)


def rol8(val, n):
"""8位循环左移"""
n &= 7
val &= 0xFF
return ((val << n) | (val >> (8 - n))) & 0xFF


def run_vm(bc, inp):
"""
执行 VM,返回最终 PC 值
PC 越大说明通过了越多的验证块
严格模拟:寄存器全部清零,只有 R1 = 167,从 offset 0 开始执行
"""
reg = [0] * 28 # v49 全部清零,对应 C 代码的 while(n28) { *v15++=0; --n28; }
reg[1] = 167 # v49[1] = 167 (0xA7),唯一的非零初始化

pc = 0 # 从字节码开头开始执行
flag = False # CMP 结果标志

while pc < len(bc) - 2:
op = bc[pc]

# 遇到 HALT (0xFE) → 成功
if op == 0xFE:
return pc

# 遇到非法操作码 (>0x40,包括 0xFF 陷阱) → 失败
if op > 0x40:
return pc

a1 = bc[pc + 1]
a2 = bc[pc + 2]

if op == 0x11: # LOAD: R[arg1] = input[arg2]
if a1 > 7 or a2 > 0x13:
return pc
reg[a1] = inp[a2]
pc += 3

elif op == 0x20: # XOR_IMM: R[arg1] ^= arg2
if a1 > 7:
return pc
reg[a1] = (reg[a1] & 0xFF) ^ a2
pc += 3

elif op == 0x21: # ADD_IMM: R[arg1] += arg2
if a1 > 7:
return pc
reg[a1] = ((reg[a1] & 0xFF) + a2) & 0xFF
pc += 3

elif op == 0x22: # ROL: R[arg1] = ROL(R[arg1], arg2 & 7)
if a1 > 7:
return pc
reg[a1] = rol8(reg[a1], a2 & 7)
pc += 3

elif op == 0x23: # XOR_REG: R[arg1] ^= R[arg2]
if a1 > 7 or a2 > 7:
return pc
reg[a1] = ((reg[a1] & 0xFF) ^ (reg[a2] & 0xFF)) & 0xFF
pc += 3

elif op == 0x30: # CMP_IMM: flag = (R[arg1] == arg2)
if a1 > 7:
return pc
flag = (reg[a1] & 0xFF) == a2
pc += 3

elif op == 0x31: # JMP_IF: 若 flag 为真则跳转
if flag:
offset = (a2 << 8) | a1 # 小端序
if offset & 0x8000:
offset -= 0x10000
pc = pc + 3 + offset
else:
pc += 3

elif op == 0x40: # MOV_REG: R[arg1] = R[arg2]
if a1 > 7 or a2 > 7:
return pc
reg[a1] = reg[a2] & 0xFF
pc += 3

else:
return pc

return pc


def main():
bc = decrypt()
passcode = [0] * 21 # 21字节(20字符 + null)
INPUT_LEN = 20 # 实际验证的输入长度

print("=" * 50)
print("Nebula VM Brute Force")
print("=" * 50)

for pos in range(INPUT_LEN):
best_val = 0
best_pc = 0

# 尝试 0x00 ~ 0xFF,找到让 VM 跑得最远的那个值
for val in range(256):
passcode[pos] = val
pc = run_vm(bc, passcode)
if pc > best_pc:
best_pc = pc
best_val = val

passcode[pos] = best_val
ch = chr(best_val) if 32 <= best_val < 127 else '?'
print(f" [{pos:2d}] = 0x{best_val:02x} ({ch}) PC -> {best_pc}")

result = bytes(passcode[:INPUT_LEN]).decode()
print(f"\nPasscode: {result}")

# 最终验证
final_pc = run_vm(bc, passcode)
print(f"Final PC: {final_pc} {'(HALT reached!)' if final_pc >= 500 else '(FAILED)'}")


if __name__ == "__main__":
main()

Nebula Patch

  • 丢到IDA发现第一步就是一个反调试
  • 可以利用修改标志寄存器过掉
  • 然后就可以发现这是两段解密
  • 如果第一段不对的话,flag自然就解不出来
  • 其实把第一段加密写一个逆向脚本即可,然后用调试器跳过反调试
  • 截图 202606171616
  • 这个是断点位置,改下ZF就可以跳到输入license环节
  • 截图 202606171617
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdint.h>

uint8_t byte_2110[18] = {
0x04, 0x8E, 0xB3, 0x88, 0xFA, 0x73, 0xD9, 0x1F, 0x81, 0x04, 0x8B, 0x0C, 0xAA, 0x3A, 0x56, 0xA1,
0x37, 0x85
};


#define __ROR1__(x,n) ((uint8_t)(x) >> ((n)&7)) | ((uint8_t)(x) << (8-((n)&7)))


int main(){
uint8_t license[18];
int n23 = 23;
int n103 = 103;
int n109 = 109;
int n18_1;
for(n18_1 = 0;n18_1 < 18;n18_1++){
int n109_new = byte_2110[n18_1];
uint8_t v16 = (n109 ^ n109_new) - n23;
v16 = __ROR1__(v16, n18_1 % 6 + 1);
license[n18_1] = v16 ^ n103;
n103 += 11;
n23 += 23;
n109 = n109_new;
}
printf("License : %s\n",license);
}
  • 这里就用c语言来写脚本了,因为用python的话会出现一些小bug,很烦(其实之前的python脚本都是ai写的,发现加密不是很难,也没血了,后面就是自己搓的了)

Lunar License

  • 其实这个和上一个题的解密逻辑差不多
  • 也是两段加密
  • 只不过不用跳过反调试而已
  • 截图 202606171621
  • 直接给脚本吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdint.h>

#define __ROR1__(x,n) (((uint8_t)(x) >> ((n)&7)) | ((uint8_t)(x) << (8-((n)&7))))
#define __ROL4__(x,n) (((uint32_t)(x) << ((n)&31)) | ((uint32_t)(x) >> (32-((n)&31))))

uint8_t byte_5555555560F0[] = {0xB4,0x68,0x6E,0xBD,0xEB,0xFD,0x0D,0xC7,0xB7,0x86,0xAC,0x6D,0x3A,0x2E,0x68,0x8D};

int main(){
uint8_t ptr_2[16],license[16];
uint8_t v15 =0,n39 = 39;
uint32_t n1280659026 = 0x4C554E52;
int n16;
for(n16 = 0 ;n16 < 16 ;n16++){
uint8_t v19 = byte_5555555560F0[n16];
int x = n16 - (n16 / 5 + (((0xCCCCCCCCCCCCCCCDLL * (unsigned __int128)n16) >> 64) & 0xFC)) + 1;
uint8_t y = (v19 ^ v15 ^ 0x5c) - n39;
uint8_t key = n1280659026 >> (8 * (n16 & 3));
uint8_t tmp = __ROR1__(y,x);
ptr_2[n16] = tmp ^ key;
license[n16] = ptr_2[n16];

n39 += 19;
v15 += 7;
n1280659026 = __ROL4__(n1280659026 ^ v19 ^ 0xA5A5A5A5, 7) + 322376503;
}
for (n16 = 0; n16 < 16; n16++)
printf("%02x", license[n16]);
printf("\n");
}

关于本文

由 GuQing 撰写,采用 CC BY-NC 4.0 许可协议。