今年的题目一言难尽啊……没活可以咬打火机而不是用 rust 来折磨我

题目附件下载

CheckYourKey

JNI_onLoad 将 sub_8965 注册为 ooxx,输入先后经过如下变化

  1. sub_FB40:标准 AES-128-ECB
  2. sub_F7DC:标准 base58
  3. sub_13788:变表 base64

最后与目标值比较,逐个逆回去即可

CheckYourKey

web_run

通过 js 发现是 wasm 逆向题,wasm 的入口是 _main,且定义了 一些交互接口

web_run

先改个后缀

  • .1 是 html
  • .2 是 wasm

jeb 分析 ez_ca.wasm,还原 _f11 部分符号:

web_run

get_input_time 首先接收 20 个字符(fd_read 的实现里会自动加换行,占用一个字符),前 16 个必须满足 %llu/%llu/%llu %llu:%llu 的格式,转成 int 形式后不能是 202211110054:

web_run

之后 generate_serial 函数依据输入的时间生成序列号,要求第二次输入必须与计算得到的序列号相同。但是由于 0xA20 这个地址中的内容恒为 0,因此不管输什么时间进去,就算序列号对了,也会输出 right value,But the time is not the time I want to hide

观察到代码中判断时间为 202211110054 时会退出执行,不产生序列号。于是猜测题目将 2022/11/11 00:54 这个时间对应的序列号作为 flag,故分析 generate_serial 并自行实现就可以求得 flag 了(也可以 patch wasm)。

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
def tohex(v: int):
return hex(v)[2:]

def calc_one(v: int):
v = v * 6364136223846793005 + 1
c = (v >> 33) % 16
return c, v

def generate_serial(minute):
mask = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
tmp = minute + 0 * 100 + 11 * 10000 + 11 * 1000000 + 2022 * 100000000 - 1
tmp &= 0xFFFFFFFF

serial = ''
for i in range(len(mask)):
mask_i = ord(mask[i])
if mask_i != 52 and mask_i != 45:
c, tmp = calc_one(tmp)
if mask_i == 120:
serial += tohex(c)
else:
serial += tohex((c & 3) | 8)
else:
serial += chr(mask_i)
return serial

print(f"RCTF{{{generate_serial(54)}}}")

huowang

huowang

有两个迷宫,第一个是用 unicorn 跑的,里面碰壁了就会触发 exit 系统调用(0x3c),走到终点就会触发 write 系统调用(1),这个地图不太好提出来。

而第二个迷宫就是常规迷宫了,注意到二者用到的输入序列相同,所以先把第二个迷宫所有路径求出来,再一个个试就行了。

找个脚本小改一下输出迷宫 2 的所有路径:

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
#include <stdio.h>

#define M 23
#define N 23
#define MAX_SIZE 10000


int mg[M + 2][N + 2] = {
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1},
{1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1},
{1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1},
{1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1},
{1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1},
{1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1},
{1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1},
{1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1},
{1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1},
{1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1},
{1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1},
{1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1},
{1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
};


struct
{
int i;
int j;
int di;
}St[MAX_SIZE], Path[MAX_SIZE];

int top = -1; //栈顶指针
int cnt = 1; //路径数计数
int minlen = MAX_SIZE; //最短路径长度


static void disp_path()
{
int k;

for (k = 1; k <= top; k++) {
int dx = St[k].i - St[k - 1].i, dy = St[k].j - St[k - 1].j;
if (dy == 0) {
if (dx == 1) printf("s");
else {
printf("w");
}
}
else if (dy == 1) {
printf("d");
}
else {
printf("a");
}
}
printf("\n");

if (top + 1 < minlen)
{
for (k = 0; k <= top; k++)
Path[k] = St[k];
minlen = top + 1;
}
}

static void mg_path(int xi, int yi, int xe, int ye)
{
int i, j, i1, j1, di;
bool is_find;

top++;
St[top].i = xi;
St[top].j = yi;
St[top].di = -1; mg[xi][yi] = -1;
while (top > -1)
{
i = St[top].i; j = St[top].j;
di = St[top].di;
if (i == xe && j == ye)
{
disp_path();
mg[i][j] = 0;
top--;
i = St[top].i; j = St[top].j;
di = St[top].di;
}
is_find = false;
while (di < 4 && !is_find)
{
di++;
switch (di)
{
case 0:
i1 = i - 1;
j1 = j;
break;
case 1:
i1 = i;
j1 = j + 1;
break;
case 2:
i1 = i + 1;
j1 = j;
break;
case 3:
i1 = i;
j1 = j - 1;
break;
}
if (mg[i1][j1] == 0)
is_find = true;
}
if (is_find)
{
St[top].di = di;
top++; St[top].i = i1, St[top].j = j1;
St[top].di = -1;
mg[i1][j1] = -1;
}
else
{
mg[i][j] = 0;
top--;
}
}
}

int main(int argc, char* argv[])
{
mg_path(1, 2, 23, 22);
return 0;
}

将输出的路径存放到 path.txt 中,再用脚本爆破即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
from subprocess import *

with open("path.txt", 'rb') as f:
paths = f.read().split(b'\n')

for path in paths:
p = Popen(["./HuoWang"], stdin=PIPE, stdout=PIPE, stderr=DEVNULL)
p.stdin.write(path + b'\n')
p.stdin.flush()
output = p.stdout.read()
if b"GrandFather" in output:
print(path.decode())
break

picStore(re)

动调跟 luaL_loadfilex,发现程序修改了 lua 引擎的 LoadByte, LoadInt, LoadInteger, LoadNumber 函数,对经过这四个函数的每个读出的字节,都会进行如下判断和变换:

picStore

按照相同的改法修改 Lua5.3.3 代码并重新编译,替换 luadec 自带的 lua-5.3 项目,反编译失败,只能拿到反汇编代码。因此考虑更换 unluac 工具,在修改了的 lua 交互执行环境中用 string.dump 将明文的 picStore.luac dump 出来,再用 unluac 反编译并进行代码美化。

其中和 re 有关的函数有两个,分别是 check_impl 和 check_func。check_impl 将 30 个 note 中的字符读出拼接并传入 check_func 进行检查:

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
function check_impl()
local note_id, L1_2, L2_2, L3_2, L4_2, L5_2, L6_2, L7_2
note_id = 0
L1_2 = 0
L2_2 = ""
L3_2 = false
while note_id < 30 do
L4_2 = check_inuse_impl(note_id)
L5_2 = note_id % 2
if L5_2 == 0 and L4_2 == 1 then
L1_2 = L1_2 + 1
L6_2 = note_id
L5_2 = read_data_impl(L6_2)
L6_2 = #L5_2
if L6_2 ~= 2 then
L3_2 = true
end
L2_2 = L2_2 .. L5_2
end
note_id = note_id + 1
end
if L1_2 == 15 then
if #L2_2 == 30 and L3_2 == false then
if check_func(L2_2) == true then
print("now, you know the flag~")
print(L2_2)
end
end
else
print("you fail!")
end
end

check_func 初始化了一个长度为 256 的 s 盒,对每个字符异或特定值后进行置换,将结果数组传入 check_result_23_impl 进行检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function check_func(A0_2)
local input, tbl, sub_res
input = value_list(A0_2)
sub_res = {}
tbl = {}
tbl[1] = 105
tbl[2] = 244
-- ...
tbl[255] = 9
tbl[256] = 193
for i = 1, #input, 1 do
input[i] = xor(input[i], i - 1)
input[i] = xor(input[i], 255)
input[i] = input[i] & 255
sub_res[#sub_res + 1] = tbl[input[i] + 1]
end

return check_result_23_impl(sub_res) == 1
end

check_result_23_impl 在 elf 中实现,其中实际调用 chk_23 来检查加密后的数组:

picStore

可以看出就是一连串的等式约束,写个 z3 脚本再解密回去即可:

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
from z3 import *

solver = Solver()

a1 = [Int(f'c{i}') for i in range(30)]
v1 = a1[0]
v2 = a1[1]
v3 = a1[2]
v4 = a1[3]
v5 = a1[4]
v6 = a1[5]
v7 = a1[6]
v8 = a1[7]
v10 = a1[8]
v24 = a1[9]
v25 = a1[10]
v26 = a1[11]
v27 = a1[12]
v28 = a1[13]
v29 = a1[14]
v30 = a1[15]
v31 = a1[16]
v32 = a1[17]
v33 = a1[18]
v34 = a1[19]
v35 = a1[20]
v36 = a1[21]
v37 = a1[22]
v38 = a1[23]
v39 = a1[24]
v40 = a1[25]
v20 = a1[26]
v41 = a1[27]
v22 = a1[28]
solver.add(255036*v7+-90989*v3+-201344*v4+122006*v5+-140538*v6+109859*v2-109457*v1-9396023 == 0)
solver.add(277432*v6+110191*v3+-186022*v4+175123*v2-75564*v5-252340*v1-12226612 == 0)
solver.add(127326*v4+260948*v2+-102835*v1+225038*v5-129683*v3-45564209 == 0)
solver.add(-170345*v2+217412*v3-26668*v1+38500*v4-27440782 == 0)
solver.add(25295*v2+69369*v3+191287*v1-24434293 == 0)
solver.add(72265*v1-2384745 == 0)
solver.add(264694*v1-190137*v2+19025100 == 0)
solver.add(101752*v24+67154*v8+-20311*v1+-30496*v6+-263329*v7+-99420*v10+255348*v3+169511*v4-121471*v2+231370*v5-33888892 == 0)
solver.add(17253*v8+-134891*v7+144501*v4+220594*v2+263746*v3+122495*v6+74297*v10+205480*v1-32973*v5-115484799 == 0)
solver.add(251337*v3+-198187*v6+-217900*v2+-62192*v8+-138306*v7+-165151*v4-118227*v1-22431*v5+72699617 == 0)
solver.add(243012*v27+-233931*v4+66595*v7+-273948*v5+-266708*v24+75344*v8-108115*v3-17090*v25+240281*v10+202327*v1-253495*v2+233118*v26+154680*v6+25687761 == 0)
solver.add(41011*v8+-198187*v1+-117171*v7+-178912*v3+9797*v24+118730*v10-193364*v5-36072*v6+10586*v25-110560*v4+173438*v2-176575*v26+54358815 == 0)
solver.add(-250878*v24+108430*v1+-136296*v5+11092*v8+154243*v7+-136624*v3+179711*v4+-128439*v6+22681*v25-42472*v10-80061*v2+34267161 == 0)
solver.add(65716*v30+-18037*v26+-42923*v7+-33361*v4+161566*v6+194069*v25+-154262*v2+173240*v3-31821*v27-80881*v5+217299*v8-28162*v10+192716*v1+165565*v24+106863*v29-127658*v28-75839517 == 0)
solver.add(-236487*v24+-45384*v1+46984*v26+148196*v7+15692*v8+-193664*v6+6957*v10+103351*v29-217098*v28+78149*v4-237596*v5-236117*v3-142713*v25+24413*v27+232544*v2+78860648 == 0)
solver.add(-69129*v10+-161882*v3+-39324*v26+106850*v1+136394*v5+129891*v2+15216*v27+213245*v24-73770*v28+24056*v25-123372*v8-38733*v7-199547*v4-10681*v6+57424065 == 0)
solver.add(-268870*v30+103546*v24+-124986*v27+42015*v7+80222*v2+-77247*v10+-8838*v25+-273842*v4+-240751*v28-187146*v26-150301*v6-167844*v3+92327*v8+270212*v5-87705*v33-216624*v1+35317*v31+231278*v32-213030*v29+114317949 == 0)
solver.add(-207225*v1+-202035*v3+81860*v27+-114137*v5+265497*v30+-216722*v8+276415*v28+-201420*v10-266588*v32+174412*v6+249222*v24-191870*v4+100486*v2+37951*v25+67406*v26+55224*v31+101345*v7-76961*v29+33370551 == 0)
solver.add(175180*v29+25590*v4+-35354*v30+-173039*v31+145220*v25+6521*v7+99204*v24+72076*v27+207349*v2+123988*v5-64247*v8+169099*v6-54799*v3+53935*v1-223317*v26+215925*v10-119961*v28-83559622 == 0)
solver.add(43170*v3+-145060*v2+199653*v6+14728*v30+139827*v24+59597*v29+2862*v10+-171413*v31+-15355*v25-71692*v7-16706*v26+264615*v1-149167*v33+75391*v27-2927*v4-187387*v5-190782*v8-150865*v28+44238*v32-276353*v34+82818982 == 0)
solver.add(-3256*v27+-232013*v25+-261919*v29+-151844*v26+11405*v4+159913*v32+209002*v7+91932*v34+270180*v10+-195866*v3-135274*v33-261245*v1+24783*v35+262729*v8-81293*v24-156714*v2-93376*v28-163223*v31-144746*v5+167939*v6-120753*v30-13188886 == 0)
solver.add(-240655*v35+103437*v30+236610*v27+100948*v8+82212*v6+-60676*v5+-71032*v3+259181*v7+100184*v10+7797*v29+143350*v24+76697*v2-172373*v25-110023*v37-13673*v4+129100*v31+86759*v1-101103*v33-142195*v36+28466*v32-27211*v26-269662*v34+9103*v28-96428951 == 0)
solver.add(-92750*v28+-151740*v27+15816*v35+186592*v24+-156340*v29+-193697*v2+-108622*v8+-163956*v5+78044*v4+-280132*v36-73939*v33-216186*v3+168898*v30+81148*v34-200942*v32+1920*v1+131017*v26-229175*v10-247717*v31+232852*v25+25882*v7+144500*v6+175681562 == 0)
solver.add(234452*v34+-23111*v29+-40957*v2+-147076*v8+16151*v32+-250947*v35+-111913*v30+-233475*v24+-2485*v28+207006*v26+71474*v3+78521*v1-37235*v36+203147*v5+159297*v7-227257*v38+141894*v25-238939*v10-207324*v37-168960*v33+212325*v6+152097*v31-94775*v27+197514*v4+62343322 == 0)
solver.add(-142909*v34+-111865*v31+258666*v36+-66780*v2+-13109*v35+-72310*v25+-278193*v26+-219709*v24+40855*v8+-270578*v38+96496*v5+-4530*v1+63129*v28-4681*v7-272799*v30-225257*v10+128712*v37-201687*v39+273784*v3+141128*v29+93283*v32+128210*v33+47550*v6-84027*v4+52764*v40-140487*v27+105279220 == 0)
solver.add(216020*v38+-248561*v29+-86516*v33+237852*v26+-132193*v31+-101471*v3+87552*v25+-122710*v8+234681*v5+-24880*v7+-245370*v1+-17836*v36-225714*v34-256029*v4+171199*v35+266838*v10-32125*v24-43141*v32-87051*v30-68893*v39-242483*v28-12823*v2-159262*v27+123816*v37-180694*v6+152819799 == 0)
solver.add(-116890*v3+67983*v27+-131934*v4+256114*v40+128119*v24+48593*v33+-41706*v2+-217503*v26+49328*v6+223466*v7+-31184*v5+-208422*v36+261920*v1+83055*v20+115813*v37+174499*v29-188513*v35+18957*v25+15794*v10-2906*v28-25315*v8+232180*v32-102442*v39-116930*v34-192552*v38-179822*v31+265749*v30-54143007 == 0)
solver.add(-215996*v4+-100890*v40+-177349*v7+-159264*v6+-227328*v27+-91901*v24+-28939*v10+206392*v41+6473*v25+-22051*v20+-112044*v34+-119414*v30+-225267*v35+223380*v3+275172*v5+95718*v39-115127*v29+85928*v26+169057*v38-204729*v1+178788*v36-85503*v31-121684*v2-18727*v32+109947*v33-138204*v8-245035*v28+134266*v37+110228962 == 0)
solver.add(-165644*v32+4586*v39+138195*v25+155259*v35+-185091*v3+-63869*v31+-23462*v30+150939*v41+-217079*v8+-122286*v6+5460*v38+-235719*v7+270987*v26+157806*v34+262004*v29-2963*v28-159217*v10+266021*v33-190702*v24-38473*v20+122617*v2+202211*v36-143491*v27-251332*v4+196932*v5-155172*v22+209759*v40-146511*v1+62542*v37+185928391 == 0)
solver.add(57177*v24+242367*v39+226332*v31+15582*v26+159461*v34+-260455*v22+-179161*v37+-251786*v32+-66932*v41+134581*v1+-65235*v29+-110258*v28+188353*v38+-108556*v6+178750*v40+-20482*v25+127145*v8+-203851*v5+-263419*v10+245204*v33+-62740*v20+103075*v2-229292*v36+142850*v30-1027*v27+264120*v3+264348*v4-41667*v35+130195*v7+127279*a1[29]-51967523 == 0)

cipher = []
if solver.check() == z3.sat:
m = solver.model()
for c in a1:
cipher.append(m[c].as_long())

tbl = [105, 244, 63, 10, 24, 169, 248, 107, 129, 138, 25, 182, 96, 176, 14, 89, 56, 229, 206, 19, 23, 21, 22, 198, 179, 167, 152, 66, 28, 201, 213, 80, 162, 151, 102, 36, 91, 37, 50, 17, 170, 41, 3, 84, 85, 226, 131, 38, 71, 32, 18, 142, 70, 39, 112, 220, 16, 219, 159, 222, 11, 119, 99, 203, 47, 148, 185, 55, 93, 48, 153, 113, 1, 237, 35, 75, 67, 155, 161, 74, 108, 76, 181, 233, 186, 44, 125, 232, 88, 8, 95, 163, 200, 249, 120, 243, 174, 212, 252, 234, 58, 101, 228, 86, 109, 144, 104, 121, 117, 87, 15, 132, 12, 20, 165, 115, 136, 135, 118, 69, 68, 2, 82, 123, 250, 251, 53, 255, 51, 221, 211, 195, 145, 140, 254, 0, 116, 43, 29, 217, 197, 183, 168, 188, 34, 218, 146, 147, 98, 149, 246, 180, 103, 33, 40, 207, 208, 192, 143, 26, 154, 225, 100, 141, 175, 124, 230, 62, 177, 205, 110, 202, 253, 173, 46, 52, 114, 164, 166, 137, 158, 122, 13, 83, 178, 133, 189, 187, 7, 184, 77, 245, 216, 190, 194, 72, 157, 172, 171, 199, 160, 45, 49, 27, 204, 81, 6, 92, 59, 209, 239, 130, 97, 61, 214, 215, 73, 90, 126, 42, 30, 240, 79, 224, 78, 223, 111, 60, 4, 5, 196, 231, 106, 64, 139, 235, 150, 227, 238, 191, 127, 31, 156, 54, 241, 242, 134, 247, 128, 65, 94, 57, 210, 236, 9, 193]
plain = b''
for i in range(len(cipher)):
ori = tbl.index(cipher[i])
ori &= 0xFF
plain += bytes([ori ^ 0xFF ^ i])

print(plain.decode())

rdefender

有符号的 rustc 程序,其中定义了两个向量数组,记作 vec1 和 vec2,它们的最大元素个数都是 16。

首先将本地 flag 文件内容读取到向量,中作为 vec1 的第一项,再进入一个 while 1 循环,根据接收到的 8 字节指令执行不同的功能(根据指令的第一字节,可以判断要执行哪个功能)。

对于功能 1,其对应的 8 字节结构体为:

1
2
3
4
struct {
BYTE identifier; // 只能为 0
BYTE str[7];
}

该功能可以向 flag 所在的 vec1 中插入新向量,向量内容为通过 get_data 函数获得的用户输入及 str 字段内容。

对于功能 3,其对应的 8 字节结构体为:

1
2
3
4
5
6
struct {
BYTE identifier; // 只能为 2
BYTE check_type; // 可以为 {0, 1, 2} 中的一个
DWORD low; // 只能是 0x899C66D1
WORD hi; // 只能是 0x5BF3
};

该功能可以向 vec2 中插入新向量,向量内容为通过 get_data 函数获得的用户输入及 check_type 字段内容。

对于功能 2,其对应的 8 字节结构体为:

1
2
3
4
5
6
struct {
BYTE identifier; // 只能为 1
BYTE idx1; // 指定 vec1 下标
BYTE idx2; // 指定 vec2 下标
BYTE reserved[5]; // 保留不用
};

功能 2 是程序的核心部分,其根据指定的 vec1 和 vec2 下标,从二者中分别取出 v1 = vec1[idx1] 及 v2 = vec2[idx2] ,根据 v2.check_type 字段值来决定如何进行 check。

check 方式同样有三种,以下记 v1 和 v2 中的用户输入为 data1 和 data2。

第一种(v2.check_type == 0)伪代码如下:

1
2
3
4
5
6
7
from functools import reduce

def check1(data1: bytes, data2: int):
digest = reduce(lambda x, y: x * 131 + y, data1) & 0xFFFFFFFF
if digest == data2:
return 2
return 1

第二种(v2.check_type == 1)伪代码如下:

1
2
3
4
5
def check2(data1: bytes, data2: bytes):
for c in data1:
if c == data2:
return 2
return 1

由第二种功能可以确定 data1 中含有哪些字符,如果 data1 中字符不重复且比较短的话倒是可以结合功能 1 来确定这些字符的排列顺序。

第三种(v2.check_type == 2)实现了一个基于栈的小型 vm,将 data2 作为代码段,data1 作为数据段来运行。其中比较有用的是以下几条:

opcode 指令长度 指令构成 伪代码 描述
0 2 [opcode] [operand] push operand 压入立即数
1 2 [opcode] [operand] push data1[operand] 压入 data1 第 operand 项
3 2 [opcode] [operand] push stack[top] ? stack[top - 1] ; ?为 {+, -, *, /, &, |, ^} 中的一个 将栈顶两个元素弹出,进行运算后将结果压入
5 1 [opcode] return stack[top] == 0 结束虚拟机运行,返回栈顶元素是否为 0

因此,可以构造这样一个爆破思路:将 flag 每个字符与猜测的立即数进行减或者异或运算,结束虚拟机运行,再根据服务器响应的返回值来判断猜测是否正确。

代码实现:

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
from pwn import *
from string import *


def get_data(data: bytes):
p.send(p32(len(data)))
p.send(data)

def add_vec1(name: bytes, string: bytes):
payload = p8(0)
payload += name[:7]
payload = payload.ljust(8, b'\x00')
p.send(payload)
get_data(string)
return u64(p.recv(8))

def check(idx1, idx2):
payload = p8(1)
payload += p8(idx1)
payload += p8(idx2)
payload = payload.ljust(8, b'\x00')
p.send(payload)
return u64(p.recv(8))

def add_vec2(check_type, data):
payload = p8(2)
payload += p8(check_type)
payload += p32(0x899C66D1)
payload += p16(0x5BF3)
p.send(payload)
get_data(data)
return u64(p.recv(8))


charset = "{}" + ascii_letters

push_imm = lambda v: p8(0) + p8(v)
take_flag_i = lambda i: p8(1) + p8(i)
sub = lambda: p8(3) + p8(1)
xor = lambda: p8(3) + p8(6)
compare = lambda: p8(5)

flag = ''
for i in range(100):
for cur in charset:
p = remote("94.74.84.207", 7892)

vm = push_imm(ord(cur))
vm += take_flag_i(i)
vm += sub()
vm += compare()
add_vec2(2, vm)
if check(0, 0) != 1:
p.close()
continue
p.close()

flag += cur
log.success(flag)
if cur == '}':
exit()
break

checkserver

本题基于 netlink 构建了一个 webserver,客户端对应的处理函数是 sub_401D40:

checkserver

将接收到的 HTTP 请求报文传入 do_response 函数(sub_404FB0),再传入 sub_404A10 进行 HTTP 报文解析。

如果请求体中含有 authcookie 键值对,则调用 sub_404530,并传入解析得到的 authcookie 值:

checkserver

地址 0x4045E5 处调用加密函数 sub_402040 对输入进行加密,该函数有非常明显的流加密特征,故可通过测试样例动调获取异或流。加密的结果在 0x4046C1 处与目标数组进行比较。

解题脚本:

1
2
3
4
5
6
7
plain = b'a' * 64
cipher = bytes.fromhex("e1fa74991ffb1908ee2deeb7f0c92a0d76f57237aaf0c20ec9e122f388138a36a3e43076e11ed6f00fdba65d59583631bd129ae5edc191ee1a0deeb51114ba23")
tbl = [plain[i] ^ cipher[i] for i in range(64)]

target = b'\xe6\xf7t\x9f\x05\xab\x1aP\xbf(\xb6\xe6\xa4\x9e\x7f\r"\xacv`\xfd\xa6\x90^\x91\xb4v\xa3\x8dC\x885\xf4\xe0\x37\x6a'
flag = ''.join(map(chr, [tbl[i] ^ target[i] for i in range(len(target))]))
print(flag)

RTTT

无符号的 rustc 程序,但是加密比较简单。程序先接收输入,将输入序列构建成一棵树,再对其进行遍历(sub_DBC0),遍历得到的结果只是字节位置发生了变化,可以通过测试样例找到它们的映射关系。

之后做 RC4 流加密(sub_E310),key 是两个数组异或得到的。

解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from Crypto.Cipher import ARC4

arr1 = bytes.fromhex("7DF484DF4E311BFB2DE658DF549487D7D2D5FBD0")
arr2 = bytes.fromhex("2A91E8BC7E5C7EDB5989788D17C0C1F7E09AC9E2")

key = bytes([arr1[i] ^ arr2[i] for i in range(len(arr1))])
rc4 = ARC4.new(key)

target = bytes.fromhex("34C2652DDAC6B1AD47BA06A93BC1CCD7F12924392AC015027E10667B5EEA5ED05946E1D66E5EB2466B31")
dec = rc4.decrypt(target).decode()

old = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP"
new = "yJzLkHwDxaCAtnsPipmIBfhljdGbeOqKNcEMugvFor"
flag = ''

for i in range(len(dec)):
flag += dec[new.index(old[i])]

print(flag)