August 3, 2022

QWB2022-GameMaster & EasyRe

应该是今年在家的最后一天了,暑假结束了呜呜呜呜呜呜。–2022 8.3 23:06

GameMaster

0x00 Daily Shell Check

无壳32位 .NET 程序,拉进dnspy32

image-20220803230750970

双击程序,用20块赢到2000万

0x01 Analyze

程序有亿点长,但其实关注关键位置就好了

首先比较在意的是 gamemessage 这个附件,这个大小估计就是加密的什么文件,而且在 BlackjackConsole.exe 用到了这个文件

注意存储到了 Program.memory

image-20220803231200988

随后就是一排按键的不同效果,有趣的是当我们按 esc 的时候会有个特殊操作

image-20220803231437213

在这个 verifyCode 又调用了 goldFunc,调用测试发现,按我们输入这些字符串再按 ESC 就是这个input

image-20220803231525287

而在长长的 goldFunc 中有三个值得注意的 AchivePoint,对我们的 gamemessage 存储的东西进行了操作

(第三个没有,就不截图了)

image-20220803231723399

image-20220803231808014

然而我并不会动调取(呜呜呜有谁来教教我)

因为只是异或和AES解密,直接手动来好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes

with open("gamemessage","rb") as f:
data = f.read()
dataArr = [t for t in data]

for i in range(len(dataArr)):
dataArr[i] ^= 34
data = bytes(dataArr)

key = [66,114,97,105,110,115,116,111,114,109,105,110,103,33,33,33]
key = bytes(key)

cipher = AES.new(key, AES.MODE_ECB)

p = cipher.decrypt(data)
# print(p)

filename = "DEC.exe"
with open(filename, 'wb') as f:
f.write(p)

0x02 GameMessage

写出来的文件,拉进010,往下找MZ,把上面都删掉

image-20220803232306264

再次拉进dnspy

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
using System;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace T1Class
{
// Token: 0x02000002 RID: 2
public class T1
{
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
private static void Check1(ulong x, ulong y, ulong z, byte[] KeyStream)
{
int num = -1;
for (int i = 0; i < 320; i++)
{
x = (((x >> 29 ^ x >> 28 ^ x >> 25 ^ x >> 23) & 1UL) | x << 1);
y = (((y >> 30 ^ y >> 27) & 1UL) | y << 1);
z = (((z >> 31 ^ z >> 30 ^ z >> 29 ^ z >> 28 ^ z >> 26 ^ z >> 24) & 1UL) | z << 1);
bool flag = i % 8 == 0;
if (flag)
{
num++;
}
KeyStream[num] = (byte)((long)((long)KeyStream[num] << 1) | (long)((ulong)((uint)((z >> 32 & 1UL & (x >> 30 & 1UL)) ^ (((z >> 32 & 1UL) ^ 1UL) & (y >> 31 & 1UL))))));
}
}

// Token: 0x06000002 RID: 2 RVA: 0x00002110 File Offset: 0x00000310
private static void ParseKey(ulong[] L, byte[] Key)
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
Key[i * 4 + j] = (byte)(L[i] >> j * 8 & 255UL);
}
}
}

// Token: 0x06000003 RID: 3 RVA: 0x0000215C File Offset: 0x0000035C
public T1()
{
try
{
string environmentVariable = Environment.GetEnvironmentVariable("AchivePoint1");
string environmentVariable2 = Environment.GetEnvironmentVariable("AchivePoint2");
string environmentVariable3 = Environment.GetEnvironmentVariable("AchivePoint3");
bool flag = environmentVariable == null || environmentVariable2 == null || environmentVariable3 == null;
if (!flag)
{
ulong num = ulong.Parse(environmentVariable);
ulong num2 = ulong.Parse(environmentVariable2);
ulong num3 = ulong.Parse(environmentVariable3);
ulong[] array = new ulong[3];
byte[] array2 = new byte[40];
byte[] array3 = new byte[40];
byte[] array4 = new byte[12];
byte[] first = new byte[]
{
101,
5,
80,
213,
163,
26,
59,
38,
19,
6,
173,
189,
198,
166,
140,
183,
42,
247,
223,
24,
106,
20,
145,
37,
24,
7,
22,
191,
110,
179,
227,
5,
62,
9,
13,
17,
65,
22,
37,
5
};
byte[] array5 = new byte[]
{
60,
100,
36,
86,
51,
251,
167,
108,
116,
245,
207,
223,
40,
103,
34,
62,
22,
251,
227
};
array[0] = num;
array[1] = num2;
array[2] = num3;
T1.Check1(array[0], array[1], array[2], array2);
bool flag2 = first.SequenceEqual(array2);
if (flag2)
{
T1.ParseKey(array, array4);
for (int i = 0; i < array5.Length; i++)
{
array5[i] ^= array4[i % array4.Length];
}
MessageBox.Show("flag{" + Encoding.Default.GetString(array5) + "}", "Congratulations!", MessageBoxButtons.OK);
}
}
}
catch (Exception)
{
}
}
}
}

发现3个环境变量不知(变量就是我们的存储的金额,打到正确的金额就是我们的flag),其他值都是已知的,然而这个check手逆有点麻,直接上Z3

0x03 GetFlag!

算法照抄即可,这一个个换行的数据强迫症患者表示很难受

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

enc = [101, 5, 80, 213, 163, 26, 59, 38, 19,
6,
173,
189,
198,
166,
140,
183,
42,
247,
223,
24,
106,
20,
145,
37,
24,
7,
22,
191,
110,
179,
227,
5,
62,
9,
13,
17,
65,
22,
37,
5]

num = -1
sol = Solver()
xx = BitVec('xx', 64)
yy = BitVec('yy', 64)
zz = BitVec('zz', 64)
key = [BitVec('key%d' % i, 8) for i in range(len(enc))]

x = xx
y = yy
z = zz
for i in range(320):
x = (((x >> 29 ^ x >> 28 ^ x >> 25 ^ x >> 23) & 1) | x << 1)
y = (((y >> 30 ^ y >> 27) & 1) | y << 1)
z = (((z >> 31 ^ z >> 30 ^ z >> 29 ^ z >> 28 ^ z >> 26 ^ z >> 24) & 1) | z << 1)
if ( i % 8 == 0 ):
num += 1
key[num] = ZeroExt(56, key[num])
key[num] = ((key[num] << 1) | ( (z >> 32 & 1 & (x >> 30 & 1)) ^ (((z >> 32 & 1) ^ 1) & (y >> 31 & 1)))) & 0xFF
for i in range(len(enc)):
sol.add(key[i] == enc[i])

assert sol.check() == sat
ans = sol.model()

s = [ans[xx].as_long(), ans[yy].as_long(), ans[zz].as_long()]
k = [0] * 12

def GetData(s, k):
for i in range(3):
for j in range(4):
k[i * 4 + j] = (s[i] >> j * 8 & 255)

GetData(s, k)
data = [60,
100,
36,
86,
51,
251,
167,
108,
116,
245,
207,
223,
40,
103,
34,
62,
22,
251,
227]
for i in range(len(data)):
print(chr(data[i] ^ k[i % len(k)]), end = "")

GetFlag!

image-20220803234233024

EasyRe

0x00 Daily Shell Check

无壳64位

image-20220803235131091

0x01 Analyze EasyRe

先把finger恢复符号表,然后看我们的主函数

MAIN

fork函数在主进程返回的是子进程的PID,在子进程返回的是0

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
double v3; // xmm0_8
double v4; // xmm1_8
double v5; // xmm2_8
double v6; // xmm3_8
double v7; // xmm6_8
double v8; // xmm7_8
__int64 v9; // r8
__int64 v10; // r9
double v11; // xmm4_8
double v12; // xmm5_8
__int64 v14; // [rsp+0h] [rbp-20h]
unsigned int v15; // [rsp+1Ch] [rbp-4h]

openRe3();
v15 = fork(); // 返回子进程ID

if ( v15 )
{
sub_401F2F(v15);
remove("re3");
}
else
{
sys_ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL, v9, v10, argv);
sub_44B680("re3", "re3", v3, v4, v5, v6, v11, v12, v7, v8, *(v14 + 8), 0LL);
}
return 0;
}

401F2F

而主进程与子进程通信与控制主要通过 ptrace 函数,可以看看这篇

https://bbs.pediy.com/thread-265924.htm#msg_header_h2_1

函数原型

1
long ptrace(enum _ptrace_request request,pid_t pid,void * addr ,void *data);

ptrace最重要的就是第一个参数,借用该文中的图

image-20220803235602533

光标在第一个参数上按M,搜索 ptrace,即可方便审计

image-20220804000116267

于是阅读可知,也可以直接动调更清楚数据的走向,因为部分变量是结构体的原因其实F5的代码并不能直接确定

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
__int64 __fastcall sub_401F2F(unsigned int Pid)
{

sys_wait(Pid, 0LL, 0); // 等待子进程来信号
sys_ptrace(PTRACE_CONT, Pid, 0LL, 0LL, v1, v2, v20);// 重新运行
some = f1(Pid, Pid, v3, v4, v5, v6);
len = sub_4017E5(); // 将一组数据调整完存放到arr
arr = v8;
len1 = len;
v25 = v8;

for ( i = 0; i <= 2; ++i )
{
v26 = 0;
sys_wait(Pid, &v26, 0);
arr = v26 & 0x7F;
if ( (v26 & 0x7F) == 0 )
break;

sys_ptrace(PTRACE_GETREGS, Pid, 0LL, context, v10, v11, v21);// 复制通用寄存器
exAddr = v28 - 1;
childData = sys_ptrace(PTRACE_PEEKTEXT, Pid, v28 - 1, 0LL, v12, v13, v22);// 读取子进程的数据
if ( childData == 0xCAFEB055BFCCLL )
{
v33 = exAddr;
SMC(Pid, exAddr, some, len1, v25, v15);
v28 = exAddr + 10;
sys_ptrace(PTRACE_SETREGS, Pid, 0LL, context, v16, v17, v23);
}


else if ( childData == 0xCAFE1055BFCCLL )
{
RESMC(Pid, v33, exAddr, some, len1, v25);
v28 = exAddr + 10;
sys_ptrace(PTRACE_SETREGS, Pid, 0LL, context, v18, v19, v23);
}

arr = sys_ptrace(PTRACE_CONT, Pid, 0LL, 0LL, v14, v15, v23);
}
return arr;
}

那么目前,对该函数的认识有

0x02 RE3

找到 start 函数,对未定义的数据按C,再对start函数开头按p,恢复后找到main函数,熟悉的setjmp longjmp,梦回*CTF

那么我们可以获得的信息是

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
__int64 __fastcall main(int argc, char **argv, char **a3)
{

v14 = 1;
if ( argc != 2 )
return 0LL;
AdjustInput(argv[1]);
RecordData(inputArr, lineData, colData);


v13 = _setjmp(env);
if ( v13 <= 24 )
{
v11 = _setjmp(v5);
if ( v11 < byte_50A0[25 * v13] )
{
if ( byte_50A0[25 * v13 + 1 + v11] != *(&savedregs + 25 * v13 + v11 - 0x28F) )// 比较行
v14 = 0;
longjmp(v5, v11 + 1);
}
longjmp(env, v13 + 1);
}


v12 = _setjmp(v6);
if ( v12 <= 24 )
{
v10 = _setjmp(v7);
if ( v10 < byte_5320[25 * v12] )
{
if ( byte_5320[25 * v12 + 1 + v10] != *(&savedregs + 25 * v12 + v10 - 0x50F) )// 比较列
v14 = 0;
longjmp(v7, v10 + 1);
}
longjmp(v6, v12 + 1);
}
if ( v14 == 1 )
puts("GOOD JOB");
return 0LL;
}

那么关键就是这个 RecordData 函数到底发生什么事情了

image-20220804002135047

那么此时一个大概的想法就出来了

  1. 主进程等待子进程运行
  2. 子进程除法int 3 等待主进程调试
  3. 主进程判断异常类型(这就是后面的值)
  4. 主进程进行 SMC 处理

0x03 SMC

那么我们该如何得到 SMC 后的文件呢,一种方法是直接逆SMC函数,另一种方法是动调获取

Analyze SMC

通过调试(由于主子进程的关系IDA会报些错,直接全部YES即可)和静态分析可知

该原始值进行MD5后,再从第8个字节取倒叙后和主进程的值异或

image-20220804003204202

(由于准备录视频解题就不贴动调过程了 XD)

那么我们只要取出这异或的值,再手动 patch RE3即可

于是在异或的值下个断点,再右键Edit breakpoint,打印出来即可

image-20220804003418508

于是去子进程 Shift + F2 选择 python,run 一下得到我们的正确文件

1
2
3
4
5
6
7
8
9
10
xorKey = {8723: 2533025110152939745, 8739: 5590097037203163468, 8755: 17414346542877855401, 8771: 17520503086133755340, 8787: 12492599841064285544, 8803: 12384833368350302160, 8819: 11956541642520230699, 8835: 12628929057681570616, 8851: 910654967627959011, 8867: 5684234031469876551, 8883: 6000358478182005051, 8899: 3341586462889168127, 8915: 11094889238442167020, 8931: 17237527861538956365, 8947: 17178915143649401084, 8963: 11176844209899222046, 8979: 18079493192679046363, 8995: 7090159446630928781, 9011: 863094436381699168, 9027: 6906972144372600884, 9043: 16780793948225765908, 9059: 7086655467811962655, 9075: 13977154540038163446, 9091: 7066662532691991888, 9107: 15157921356638311270, 9123: 12585839823593393444, 9139: 1360651393631625694, 9155: 2139328426318955142, 9171: 2478274715212481947, 9187: 12876028885252459748, 9203: 18132176846268847269, 9219: 17242441603067001509, 9235: 8492111998925944081, 9251: 14679986489201789069, 9267: 13188777131396593592, 9283: 5298970373130621883, 9299: 525902164359904478, 9315: 2117701741234018776, 9331: 9158760851580517972}

addr = 0x2213

while True:
data = get_qword(addr)
key = xorKey[addr]
dec = data ^ key
idc.patch_qword(addr, dec)
addr += 16

(再把异常数值类型 patch 成 nop 即可)

Debugger SMC

这个思路出自 FallWind 师傅,只能说是曲线救国,太强了!

首先我们想 attach 我们修复后的子进程有几个困扰点

所以曲线救国的方法就是让主进程结束,子进程不结束

  1. patch EasyRe 的RE3资源文件,让在需要SMC的函数的结尾写个死循环
  2. patch RESMC函数 的异或值为0,让主进程不再加密回去
  3. patch 401F2F函数的 for 循环为2,这样主进程就不用等待子进程的结束值

这样执行完主程序两个异常处理就结束,然而我们的子进程还卡着,IDA直接attach上

(一会还要录视频今天有点晚了,就先不贴了)

0x04 GetFlag!

恢复后的函数,一眼丁真这是数织游戏(事后才知道

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
// positive sp value has been detected, the output may be wrong!
void __fastcall sub_21F9(__int64 input, __int64 data1, __int64 data2)
{
int offsett; // [rsp+20h] [rbp-28h]
int statee; // [rsp+24h] [rbp-24h]
char count2; // [rsp+28h] [rbp-20h]
int jj; // [rsp+2Ch] [rbp-1Ch]
int j; // [rsp+30h] [rbp-18h]
int offset; // [rsp+34h] [rbp-14h]
int state; // [rsp+38h] [rbp-10h]
char lineCount; // [rsp+3Ch] [rbp-Ch]
int ii; // [rsp+40h] [rbp-8h]
int i; // [rsp+44h] [rbp-4h]

for ( i = 0; i <= 24; ++i )
{
ii = 0;
lineCount = 0;
state = 0;
offset = 1;
while ( ii <= 24 )
{
if ( *(25 * i + ii + input) ) // 记录1的连续,设置状态值
{
++lineCount;
state = 1;
}
else
{
if ( state ) // 出现0时保存1个个数
{
*(data1 + 25LL * i + offset) = lineCount;
lineCount = 0;
++offset;
}
state = 0;
}
if ( ++ii == 25 && state ) // 行结尾是1,记录
{
*(data1 + 25LL * i + offset) = lineCount;
lineCount = 0;
++offset;
}
}
*(25LL * i + data1) = offset - 1; // 行的开头记录当前行的offset
}


for ( j = 0; j <= 24; ++j ) // 记录列
{
jj = 0;
count2 = 0;
statee = 0;
offsett = 1;
while ( jj <= 24 )
{
if ( *(25 * jj + j + input) )
{
++count2;
statee = 1;
}
else
{
if ( statee )
{
*(data2 + 25LL * j + offsett) = count2;
count2 = 0;
++offsett;
}
statee = 0;
}
if ( ++jj == 25 )
{
if ( statee )
{
*(data2 + 25LL * j + offsett) = count2;
count2 = 0;
++offsett;
}
}
}
*(25LL * j + data2) = offsett - 1;
}
}

于是很明显我们RE3的主函数的对比数据不对,于是这时候就去init段还要个preinit段找

同样的手法是通过偏移来进行了混淆,手动计算即可,最后拿到真实数据

image-20220804005601281

于是再去在线网站跑 https://handsomeone.github.io/Nonogram/

GetFlag!

image-20220804005641970

Reference Article

http://1.12.239.117:8090/archives/qwb2022-easyre

http://blog.leanote.com/post/xp0int/2022-%E5%BC%BA%E7%BD%91%E6%9D%AF%E5%88%9D%E8%B5%9B-Writeup-By-Xp0int#title-4

https://bbs.pediy.com/thread-265924.htm#msg_header_h2_1

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

This post is written by P.Z, licensed under CC BY-NC 4.0.