首页
社区
课程
招聘
[推荐]看雪·安恒 2020 KCTF 春季赛 | 第七题设计思路及解析
发表于: 2020-5-6 18:25 6323

[推荐]看雪·安恒 2020 KCTF 春季赛 | 第七题设计思路及解析

2020-5-6 18:25
6323



第七题《杯弓蛇影》历时3天,共有1873人围观,最终共有6支战队成功破解。



4月28日,金左手战队的 ccfer 率先攻破题目,遥遥领先。


受到激励,其他战队也不甘心落后,紧追猛赶,辣鸡战队、雨落星辰等战队陆续攻破此题。


接下来让我们一起来看一下这道题的设计思路和详细解析吧。


赛题点评



本题点评由打打酱油战队 梦游枪手 提供: 


此题关键点是 自写的虚拟机 执行关键代码,如果对虚拟机运算过程不够熟悉很容易忽略其对计算结果的影响。之后,只要耐心的重构出算法,计算出答案的过程并不困难。




出题团队简介




本题出题战队 void(0):




团队简介:

xh1998:彩蛋专家已退休,现为国家一级捣蛋专家。
RBtree:现为科锐第三阶段成员,bug制造专家,熟练掌握国家八大菜系,希望有一天实现挖掘机炒bug。
xxy5:武汉科锐三阶段学员,不断努力的的二进制狗。



设计思路

简介


大家好! 我们又见面了! 继《传家之宝》和《南充茶坊》之后,这一次我们奉上实战系列作品——《杯弓蛇影》。
 


故事背景简介


春夏秋冬,寒来暑往,日复一日,这人间循环往复空换时光流逝,似乎少了点乐趣。
无所事事便没有好事。
不知从哪儿传出的“巳蛇将出”的谣言,传说巳蛇亦蛇亦弓,入水为蛇,踪迹全无,出水为弓,毙敌百步。竟也有人捕风捉影、信以为真,一传十,十传百,事实变得更加扑朔迷离,人心惶惶。
遂智者言,地势坤、风入松、松如浪,一景一画皆有其意,善用者,良马也。


脱壳

使用的反调试:异常,硬件断点检测,内存检验,NtQueryInformationProcess,简单的反dump等。

这些反调试有的会造成导入表加密,有的会使代码段错乱,造成异常,有的会修改算法,需要找出所有的并且绕过这些反调试。

内存校验数据选择放在了 紧跟在DOS头后得无效数据。

清楚掉所有的反调试后,导入表保护:清除了源程序的导入表,使用了iat混淆,你需要修复导入表。
 

VM识别解析



模拟存储识别


1. 代码空间

Code用于模拟在一开始new出来


1. 代码标识 CD        8字节

2. 代码sizeof          8字节

2. 代码大小             8字节


进行拷贝赋值。

2. 环境寄存器[9]

unsigned long long ER[9];//9个计算机环境寄存器 初始化的事后使用:

#define  TE 0       //Text End

#define  HS 1       //Heap Start

#define  HE 2       //Heap End

#define  SS    3       //Stack Start

#define  TOP 4       //Stack End

//3个指针寄存器

#define  IP 5       //指令指针                (指令运行后必然使用 ER[5])

#define  SP 6       //栈指针

#define  FP 7       //过程调用帧栈指针

...


3.  寄存器[10];

类型 unsigned long long

寄存器[1] - 寄存器[9]

GR[1]      -  GR[9]


指令解析


ps:(有些操作会随着SetProperty属性更改而进行修改)


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
指令操作
[默认编号0]:LEA(取地址)
[默认编号1]:SetProperty  寄存器,属性值(立即数)()更改不同属性的值
[默认编号2]:SetProperty  寄存器,属性值(立即数)()更改不同属性的值
[默认编号3]:LOADDI
[默认编号4]:LOADQI
[默认编号5]:LOADF4I
[默认编号6]:LOADF8I
[默认编号7]:LOADB
[默认编号8]:LOADW
[默认编号9]:LOADD              [寄存器],[内存]
[默认编号10]:LOADQ
[默认编号11]:取地址
[默认编号12]:-
[默认编号13]:STOREBI
[默认编号14]:STOREWI
[默认编号15]:STOREDI[寄存器],立即数 
[默认编号16]:STOREQI
[默认编号17]:STOREF4I
[默认编号18]:STOREF8I
[默认编号19]:STORERB                          [内存][内存] SetProperty 属性为准
[默认编号20]:STORERW
[默认编号21]:STORERD
[默认编号22]:STORERQ
[默认编号23]:-待扩展
[默认编号24]:-待扩展
[默认编号25]:MOVB                              [内存][内存]   
[默认编号26]:MOVW
[默认编号27]:MOVD
[默认编号28]:MOVQ
[默认编号29]:-待扩展
[默认编号30]:-待扩展
[默认编号31]:MOVRR                              [寄存器][寄存器] 
[默认编号32]:MOVRF4
[默认编号33]:MOVRF8
  
  
[默认编号34]:PUSHFP
[默认编号35]:PUSHB
[默认编号36]:PUSHW
[默认编号37]:PUSHD
[默认编号38]:PUSHQ
[默认编号39]:PUSHF4
[默认编号40]:PUSHF8
[默认编号41]:POPFP
[默认编号42]:POPB
[默认编号43]:POPW
[默认编号44]:POPD
[默认编号45]:POPQ
[默认编号46]:待扩展
[默认编号47]:待扩展
[默认编号48]:INCR
[默认编号49]:DECR
[默认编号50]:ADDR
[默认编号51]:SUBR
[默认编号52]:MULR
[默认编号53]:DIVR
[默认编号54]:--
[默认编号55]:--
[默认编号56]:--
[默认编号57]:--
[默认编号58]:--
[默认编号59]:--
[默认编号60]:--
[默认编号61]:--
[默认编号62]:AND
[默认编号63]:OR
[默认编号64]:XOR
[默认编号65]:NOT
[默认编号66]:SHRA
[默认编号67]:SHRL
[默认编号68]:SHL
[默认编号69]:CALL
[默认编号70]:RET
[默认编号71]:MOVSF
[默认编号72]:LDFPOB
[默认编号73]:LDFPOW
[默认编号74]:LDFPOD
[默认编号75]:LDFPOQ
[默认编号76]:JMPI
[默认编号77]:JNZ
[默认编号78]:JNS
[默认编号79]:JNL
[默认编号80]:JZ
[默认编号81]:JS
[默认编号82]:JL
[默认编号83]:COMP
[默认编号84]:COMPI
[默认编号85]:OUTO
[默认编号86]:OUTD
[默认编号87]:OUTH
[默认编号88]:OUTC
[默认编号89]:--
[默认编号90]:--
[默认编号91]:NOP

例子strlen:


1
2
3
4
5
6
7
8
9
10
11
12
13
LEA       R1,HELLO
MOVRR     R3,R1
L1:
    LOADB  R2,R1
    JZ      END
    INCR    R1
    JMPI    L1
END: 
    SUBR    R1,R3 
    OUTD    R1
    HALT
    ; 字符串 AA AA AA 00
HELLO: DB AA AA AA 00


二进制解析CODE:


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
unsigned AnsiChar data[70] = {
//标识符
    0x430x440x000xCC, 0xCC, 0xCC, 0xCC, 0xCC,  
//code 大小                  
   0x2A, 0x000x000x00,0x000x000x000x00,  
//data 数据大小                    
    0x010x000x000x000x000x000x000x00,    
//lea 寄存器[1],地址                
   0x000x01, [0x000x000x000x000x000x000x000x00], 
//MOVRR 寄存器3,寄存器1      
   0x1F, 0x030x01,     
//loadb 寄存器2,寄存器1                                                          
   0x070x020x01,    
//jz  地址                                                           
   0x500x24
0x000x000x000x000x000x00,0x00,  
//INCR              
   0x300x01,   
//jmp跳转                                                                                                
   0x4C, 0x0D,                                                                       
   [0x000x000x000x000x000x000x00,] 
//SUBR 寄存器1,寄存器3
   0x330x010x03,  
//OUTD                                                              
   0x56,0x01,          
//结束符                                                                
   0x5C,  
//数据字符串                                                                               
   0xAA, 0xAA, 0xAA, 0x00
};

指令混淆识别:

(1)非混淆代码会 与外界交互

(2)前面会执行多个pushd 指令 popd指令结束


算法破解


此题使用了一种面向多明文的数据对称加密算法。

该算法能够对同一个密文,使用相同的计算过程,在不同的密钥作用下,得出既定的不同明文。

该算法能够为“必须在不同环境下表现出不同功能的程序设计”提供一种安全的实现方案。只有拥有合适密钥才能解出理想代码,否则不仅看不到理想代码,甚至不能确定所谓的理想代码是否真的存在。
 
为了符合参赛出题要求,我们将‘一批密钥’和‘每个密钥应该解出什么样的明文’固定在了CrackMe里面(我们认为,就算公开了),还将解密算法固定在了CrackMe里面(我们认为也算公开了),然后要求攻击者给出正确的密文,即:序列号(实际上是在挑战攻击者找出正确的加密算法)
 

密码算法描述


假设给定一组密钥masterkey[i]和一组对应的明文m[i]。
 


明文预处理


 
0x01

对输入的n个明文m[i](i=1,2,...,n)均填充到等长长度且为L的倍数,令结果为m_p[i](L为分组密码中分组的长度,本题L为32bit)。

即:填充到长度为ceil(maxlen(m[1],m[2],…,m[n])/L)*L;其中ceil()函数返回不小于给定数字的最小整数。
 
0x02

对填充后的m_p[i]分别采用Hash算法(本题选用的是SHA-256),分别得到m_h[i];
 
0x03

m_p[i]分别循环地与m_h[i]异或,生成m_hp[i];
 
0x04

将m_hp[i]分别与m_h[i]拼接,得到m_hpd[i];
 
0x05

将m_hpd[i]分别以Lbit为分组长度进行分组(此题L为32bit)。

m_hpd[i]=m_hpd[i][0] || m_hpd[i][1] || ... || m_hpd[i][length/L] (length为每个明文m_hpd[i]的长度)。


密钥预处理


 
0x01

随机生成salt(每个salt的长度为Lbit)。
 
0x02

将n个salt分别连接在n个密钥口令masterkey[i]后面得到k[i]。
 
0x03

对k[i]进行Hash生成key[i](此题选用的是SHA-256)。
 
0x04

对key[i]分别以Lbit为分组长度进行分组(此题L为32bit)

key[i]=key[i][0] || key[i][1] || ... || key[i][length/L] (length为每个分组key[i]的长度)
 
0x05

对分组后的不同的key[i]对应的相同位置的分组检查是否有相等,若有,则重复12345步。
 


加密


 
0x01

随机产生多个长度为Lbit的数据offset(此题L为32bit)
 
0x02

生成密文的函数为:
c=Enc(key[1],...key[n],m_hpd[1],...mhpd[n])
=(R_11 || R_21 || ... || R_n1 || offset[1]) || (R_12 || R_22 || ... || R_n2 || offset[2])...(R_1m || R_2m || ... || R_nm || offset[m])

其中m为明文的分组组数

R为加密得到的密文数据块

n为输入的明文和密钥个数

offset为加密时生成的随机数


0x03

在每一个分组中:
在第i个分组:
c_i=Enc(key[1][i%t],key[2][i%t],...,key[n][i%t],m_hpd[1][i],m_hpd[2][i],...,m_hpd[n][i])
=R_1i || R_2i || ... || R_ni || offset[i]
       

其中n为输入的明文和密钥个数

t为密钥的分组组数


 对n个坐标
(key[1][i%t],m_hpd[1][i]^offset[i])
(key[2][i%t],m_hpd[2][i]^offset[i])
....
(key[n][i%t],m_hpd[n][i]^offset[i])
 
采用拉格朗日插值定理所生成的系数为R_1i,R_2i,...,R_ni,offset[i]为随机数,以确保坐标的第2个值都小于p。(其中所有的运算均为模p运算,p为小于2^L的最大素数)
     
0x04

对所有分组加密完成,产生的所有系数以及offset组成最终的密文。
 


解密



0x01

对给定的n个密钥用加密时同样的方法对密钥进行预处理并分组,得到key[i]=key[i][0] || key[i][1] || ... || key[i][length/L](i=1,2,...,n)
 
0x02

设k为key[i],解密函数为:m=Dec(k,c);

其中,第j个分组明文的解密方法为:
m[i]=Dec(k[j],c[j])
=Dec(k[j],R_1j,R_2j, ...,R_nj,offset[j])
=(R_1j*k[j]^n-1+R_2j*k[j]^n-2+...+R_nj)^offset[j]

其中,R_1j,R_2j,...,R_nj分别为第j分组的密文数据块,key[j]中的j需要对t取模, t为密钥的分组组数。

*,+均为模p运算,p的取值与加密时相同。
        
对所有分组进行解密后,将解密得到的明文分组拼接在一起得到完整的明文。
 
0x04

对拼接在一起的明文进行分离,得到m_hp[i]与m_h[i]。
 
0x05

将m_hp[i]循环与m_h[i]进行异或得到m_p[i]。
 
0x06

将m_p[i]进行Hash运算(SHA256)得到的结果与m_hp[i]进行比较验证。

如相等则表示第i个明文解密成功,如不相等则表示解密失败。
 
0x07

以上步骤重复n次,便可成功解密得到所有的n对明文。
       

出题


此题用Username作为种子,利用KDF函数(实际上是SHA-256的变种)生成salt和offset分别用于上述算法所需的对密钥进行预处理和加密过程。

对我们选取的3对明文和密钥利用以上方法进行加密(即n=3,L=32bit)。

加密算法产生的系数(即以上算法产生的密文去掉offset)便为序列号。
     

crackme运行过程 


CM.EXE会对用户输入的Username作为种子,利用KDF函数生成salt和offset分别用于上述算法所需的对密钥进行预处理和解密过程。

对输入的序列号作为密文,用正确的3对密钥对密文进行解密,若能在上述算法中验证解密成功且解密得到的明文和我们公开的明文一致,则序列号正确。

 PS:由于本人疏忽,在第一次出题时只按照上述算法的解密中的步骤0x06判断hash值是否相等验证序列号是否正确,遗漏了将解密得到的明文和公开的明文对比是否一致的判断,因此出现了bug,中途修改了题目,望各位大佬见谅!

用于加密产生密文的三对明文和密钥公开如下:(英文逗号为分割符,逗号前为密钥,逗号后为明文)

这杀软好多呀,好像是个VM,我是个任务管理器。这机器里文件修改时间分布广,找包含关键词的文件。伊娃找到了理想植物,赶紧带回去给船长。(附件下载地址:https://bbs.pediy.com/thread-258821.htm)


解析过程


本题解题思路由打打酱油战队 梦游枪手 提供: 



文章分析的是修复多解的版本。


程序有反调试(检测Context,NtQueryInformationProcess等),有壳,反调试可以用sharpod插件绕过,直接在GetSystemTimeAsFileTime下断运行,回溯到OEP,再手动修IAT,脱壳即可。脱壳以后直接拉IDA分析。


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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  int v4; // eax
  int v5; // et2
  int v6; // ecx
  int result; // eax
  char *v8; // eax
  int *v9; // esi
  int v10; // edi
  unsigned __int8 v11; // cl
  unsigned __int8 v12; // cl
  char v13; // bl
  unsigned __int8 v14; // bl
  char v15; // dl
  unsigned __int8 v16; // dl
  char v17; // dl
  unsigned __int8 v18; // dl
  char v19; // dl
  unsigned __int8 v20; // dl
  char v21; // dl
  unsigned __int8 v22; // dl
  char v23; // dl
  unsigned __int8 v24; // dl
  char v25; // dl
  unsigned __int8 v26; // dl
  __int128 *v27; // edi
  __int64 v28; // kr00_8
  int v29; // eax
  int v30; // ebx
  char *v31; // esi
  int v32; // eax
  char *v33; // ecx
  int v34; // ecx
  int v35; // eax
  int i; // eax
  bool v37; // zf
  int v38; // edx
  int v39; // esi
  int v40; // ecx
  int v41; // ecx
  int v42; // [esp-Ch] [ebp-2F84h]
  int v43; // [esp-8h] [ebp-2F80h]
  unsigned __int8 v44; // [esp+10h] [ebp-2F68h]
  int v45; // [esp+10h] [ebp-2F68h]
  unsigned __int8 v46; // [esp+14h] [ebp-2F64h]
  unsigned __int8 v47; // [esp+18h] [ebp-2F60h]
  unsigned __int8 v48; // [esp+1Ch] [ebp-2F5Ch]
  unsigned __int8 v49; // [esp+20h] [ebp-2F58h]
  int v50; // [esp+28h] [ebp-2F50h]
  int v51; // [esp+2Ch] [ebp-2F4Ch]
  int v52; // [esp+3Ch] [ebp-2F3Ch]
  char v53[21]; // [esp+44h] [ebp-2F34h]
  __int64 v54; // [esp+59h] [ebp-2F1Fh]
  int v55; // [esp+61h] [ebp-2F17h]
  __int16 v56; // [esp+65h] [ebp-2F13h]
  char v57; // [esp+67h] [ebp-2F11h]
  DWORD vmenv[40]; // [esp+68h] [ebp-2F10h]
  __int128 v59; // [esp+120h] [ebp-2E58h]
  __int128 v60; // [esp+130h] [ebp-2E48h]
  __int64 v61; // [esp+140h] [ebp-2E38h]
  char v62[80]; // [esp+148h] [ebp-2E30h]
  char v63[1000]; // [esp+198h] [ebp-2DE0h]
  _OWORD vmcode[671]; // [esp+580h] [ebp-29F8h]
  
  v53[4] = 0;
  v54 = 0i64;
  *(_OWORD *)&v53[5] = 0i64;
  v55 = 0;
  v56 = 0;
  v57 = 0;
  memset(v63, 0, sizeof(v63));
  printf(aPleaseInputUse);
  scanf(aS, &v53[4]);
  printf(aPleaseInputSer);
  scanf(aS, v63);
  initusernamekey(&v53[4]);                     // username生成usernamekey
  v3 = strlen(v63);
  v5 = v3 % 24;
  v4 = v3 / 24;
  v6 = v4;
  v51 = v4;
  if ( v5 )
  {
    printf(aSorryTheSerial);
LABEL_3:
    system(aPause);
    result = 0;
  }
  else
  {
    if ( v4 > 0 )
    {
      v8 = &v63[1];
      v50 = v6;
      v9 = serial;
      do
      {
        v10 = 3;
        do
        {
          v11 = *(v8 - 1);
          if ( v11 <= 0x39u )
            v12 = v11 - 48;
          else
            v12 = v11 - 55;
          v13 = *v8;
          if ( (unsigned __int8)*v8 <= 0x39u )
            v14 = v13 - 48;
          else
            v14 = v13 - 55;
          v15 = v8[1];
          if ( (unsigned __int8)v15 <= 0x39u )
            v16 = v15 - 48;
          else
            v16 = v15 - 55;
          v44 = v16;
          v17 = v8[2];
          if ( (unsigned __int8)v17 <= 0x39u )
            v18 = v17 - 48;
          else
            v18 = v17 - 55;
          v46 = v18;
          v19 = v8[3];
          if ( (unsigned __int8)v19 <= 0x39u )
            v20 = v19 - 48;
          else
            v20 = v19 - 55;
          v47 = v20;
          v21 = v8[4];
          if ( (unsigned __int8)v21 <= 0x39u )
            v22 = v21 - 48;
          else
            v22 = v21 - 55;
          v48 = v22;
          v23 = v8[5];
          if ( (unsigned __int8)v23 <= 0x39u )
            v24 = v23 - 48;
          else
            v24 = v23 - 55;
          v49 = v24;
          v25 = v8[6];
          if ( (unsigned __int8)v25 <= 0x39u )
            v26 = v25 - 48;
          else
            v26 = v25 - 55;
          v8 += 8;
          *v9 = v26 | (16 * (v49 | (16 * (v48 | (16 * (v47 | (16 * (v46 | (16 * (v44 | (16 * (v14 | (16 * v12)))))))))))));
          ++v9;
          --v10;
        }
        while ( v10 );
        --v50;
      }
      while ( v50 );
      v6 = v51;
    }                                           // hexstrtodata
    v27 = &xmmword_45AA50;
    v52 = v6 - 8;
    v28 = 4i64 * v6 - 32;
    v29 = 0;
    v45 = 0;
LABEL_35:
    v30 = *(_DWORD *)v27;
    v31 = &aVm[v29];                            // 字符串跟username生成的数据拼接到一起,加密
    v32 = strlen(&aVm[v29]);
    v31[v32 + 3] = v30;
    v33 = &v31[v32];
    v33[2] = BYTE1(v30);
    v33[1] = BYTE2(v30);
    *v33 = HIBYTE(v30);
    v33[4] = 0;
    v34 = 0;
    v35 = 0;
    while ( v31[v35] )
    {
      if ( !v31[v35 + 1] )
      {
        ++v34;
        break;
      }
      if ( !v31[v35 + 2] )
      {
        v34 += 2;
        break;
      }
      if ( !v31[v35 + 3] )
      {
        v34 += 3;
        break;
      }
      if ( !v31[v35 + 4] )
      {
        v34 += 4;
        break;
      }
      v35 += 5;
      v34 += 5;
      if ( v35 >= 2000 )
        break;
    }
    encrypt(v31, (int)constkey, v34);
    initvmcode(vmcode);                         // 初始化虚拟机代码
    vmenv[4] = 0;
    memset(&vmenv[8], 0, 0x48u);
    memset(&vmenv[26], 0, 0x50u);
    v59 = 0i64;
    v60 = 0i64;
    v61 = 0i64;
    memset(v62, 0, sizeof(v62));
    initenv((int)vmenv, vmcode, 10737);         // 初始化虚拟机环境
    vmenv[28] = (DWORD)serial;
    vmenv[29] = (int)serial >> 31;
    vmenv[30] = (DWORD)constkey;
    vmenv[31] = (int)constkey >> 31;
    vmenv[32] = (DWORD)usernamekey;
    vmenv[33] = (int)usernamekey >> 31;
    vmenv[34] = (DWORD)text;                    // 虚拟机输出
    vmenv[35] = (int)text >> 31;
    vmenv[38] = (DWORD)flagresult;              // 虚拟机输出
    vmenv[39] = (int)flagresult >> 31;
    vmstart(vmenv);                             // 执行虚拟机解密
    xordecrypt(v42, v43, v52);                  // text xor flagresult 结果存放到key
    encrypt(key, (int)encresult, v28);
    for ( i = strlen(key) - 1; i > 0; --i )
    {
      v37 = key[i] == (char)0x80;               // 末尾的0x80替换为0
      key[i] = 0;
      if ( v37 )
        break;
    }
    v38 = strlen(key);
    v39 = 0;
    v40 = 0;
    if ( v38 > 0 )                              // 比较
    {
      while ( key[v40] == truekey[v45 + v40] )
      {
        if ( ++v40 >= v38 )
          goto LABEL_55;
      }
      v39 = 1;
    }
LABEL_55:
    v41 = 0;
    while ( flagresult[v41] == encresult[v41] )
    {
      ++v41;
      if ( v41 >= 8 )
      {
        if ( v39 == 1 )
          break;
        if ( vmenv[7] )
          j_j_j___free_base((void *)vmenv[7]);
        v27 = (__int128 *)((char *)v27 + 4);
        v29 = v45 + 256;
        v45 += 256;
        if ( (int)v27 >= 0x45AA5C )
        {
          printf(aCongratulation);
          goto LABEL_3;
        }
        goto LABEL_35;
      }
    }
    printf(aSorryTheSerial);
    system(aPause);
    if ( vmenv[7] )
      j_j_j___free_base((void *)vmenv[7]);
    result = 0;
  }
  return result;
}
程序大致流程为:将serial从hex字符串解码,username加密生成usernamekey,usernamekey的一部分添加到三个字符串的尾部
(一共三轮加密,分别使用"这杀软好多呀,好像是个VM","这机器里文件修改时间分布广","伊娃找到了理想植物"),再加密生成constkey。

再把usernamekey、serial、constkey作为vm虚拟机的参数,执行虚拟机解密流程。text跟flagresult进行异或,最后比较text和flagresult是否与预设结果相同。
 
usernamekey,constkey跟username有关,不过我们只要求出"KCTF"的序列号就行。

抓取"KCTF"相关的usernamekey和constkey,人肉004016EC处的虚拟机即可。



0040E3D6 为vmdispatcher,edi为vmenv,跟踪handler,观察vmenv的变化,就能分析出vm的大致流程了。有些handler有反调试,所以脱壳了也要开着sharpod。

004016EC的解密流程转写成python大致如下:


def testdecrypt(roundnum):
    constroundkey = constkey[roundnum]
    for i in range(5):
        r = (serial[i * 3] + serial[i * 3 + 1] * constroundkey[i] +
             ((constroundkey[i]**2) * serial[i * 3 + 2])) % 0xFFFFFFFB
        r ^= usernamekey[i]
        text.append(r)
    for i in range(5, 13):
        r = (serial[i * 3] + serial[i * 3 + 1] * constroundkey[i % 8] +
             ((constroundkey[i % 8]**2) * serial[i * 3 + 2])) % 0xFFFFFFFB
        r ^= usernamekey[i]
        flagresult.append(r)


text和flagresult均为虚拟机输出,可以在后面的流程找到真正的结果。


修改程序流程,在00401705 下断点,输入KCTF和官方提供的真码,停下后,0045B270替换为正确的text("我是个任务管理器","找包含关键词的文件","赶紧带回去给船长"),末尾的一个"00"要改成"80",步过00401712,在0045A230就能看到flagresult了。



拿到这三轮解密的flagresult,再异或对应轮数的text(超过text下标的则忽略这步),得到异或前的text,再异或对应位置的usernamekey,得到m。

结合"KCTF"的usernamekey和constkey,可以列出下面的方程。


1
2
m = flagresult[i]^text[i]^usernamekey[i]
(xi + yi * constkey[r][i%8] + zi * constkey[r][i%8]**2) mod 0xFFFFFFFB == m


其中i表示当前位置,r表示轮数,xi=serial[3i],yi=serial[3i+1],zi=serial[3i+2]


当i=0,r=0时,方程为:
(x0 + y0 0x835904E1 + z0 0x835904E1**2) mod 0xFFFFFFFB == 0x1CC4FA98

写代码列出三轮的方程组,python代码如下,测试testdecrypt的代码也包含在里面了。


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
# KCTF
serial = [
    0xe84de727, 0xb4c7223f, 0xc4c8b34f, 0x9d4e4221, 0x225dd4a2, 0x6a95e624, 0xe5eb4526, 0x9de64c0a, 0x9ed50b44, 0xbba723f9, 0x878e4b4d, 0xd8841263, 0x453dc515, 0x7b401d6b, 0x5af4f140, 0x1193faa1,
    0x433ada48, 0x145a358a, 0xbce1b843, 0x9c7f5d39, 0x0c31987e, 0x39bb056e, 0x1a21b92b, 0x8de2358e, 0xcec1ff6c, 0x206cf8c1, 0x7c46c891, 0x44ba8da0, 0xec483438, 0x9ffa54b3, 0x2c8d3174, 0xe97ac3c2,
    0x024783d0, 0xdfdc2bc9, 0x524b9c81, 0xb40f78f2, 0xe184a49b, 0x2292b4d7, 0x9a58ef0b
]
constkey = [[
    0x835904e1, 0xc834944b, 0x027a19e0, 0xfeb308a3, 0x621ff195, 0x4b705c5c, 0x3eb5d9d0, 0x9a5c73f4
],
    [
    0xccffa00f, 0x490c46ed, 0xf78be5a1, 0x81a56274, 0x165deb5c, 0xf6f46796, 0x44de5146, 0x00e4984c
],
    [
    0xdf77dfa8, 0xae0ed8d8, 0x064da354, 0x4c8b95cc, 0xf934ca39, 0xc4e9de04, 0x18ee2793, 0x945ac9c2
]]
usernamekey = [
    0xd9c3e463, 0x11c9af78, 0x6485bf9e, 0xff4bd05d, 0x65769726, 0xf5c38988, 0xbf3a2423, 0x4b718cc0, 0xc70d8f49, 0xdfc73315, 0x74470070, 0x94b89f71, 0x0e60f6b1, 0x2051a122, 0x1f061047, 0x4ced9e38
]
textr = [[3305578235L, 3274609060L, 2824850596L, 382260195L, 2418685203L],
  
         [2670343418L, 3143641187L, 3172232216L, 4193180192L, 2540967463L],
         [3311948515L, 4048345354L, 1943049539L, 810068865L, 2159988552L]]
flagresultr = [[198562876L, 2077776234L, 1722709368L, 3592754452L, 271201555L, 1745403541L, 14394681L, 417773631L],
               [1257800710L, 30183867L, 32037074L, 1277826276L, 730502695L, 1053176226L, 2658935662L, 3990671698L],
               [2108157719L, 1169449682L, 3145018811L, 2229540901L, 12504904L, 855930893L, 1706174636L, 1402764800L]]
  
  
def testdecrypt(roundnum):
    constroundkey = constkey[roundnum]
    for in range(5):
        r = (serial[i * 3] + serial[i * 3 + 1] * constroundkey[i] +
             ((constroundkey[i]**2) * serial[i * 3 + 2])) % 0xFFFFFFFB
        r ^= usernamekey[i]
        text.append(r)
    for in range(5, 13):
        r = (serial[i * 3] + serial[i * 3 + 1] * constroundkey[i % 8] +
             ((constroundkey[i % 8]**2) * serial[i * 3 + 2])) % 0xFFFFFFFB
        r ^= usernamekey[i]
        flagresult.append(r)
  
  
def testencrypt(roundnum):
    constroundkey = constkey[roundnum]
    for in range(5):
        r = textr[roundnum][i] ^ usernamekey[i]
        rc.append(r)
    for in range(8):
        r = flagresultr[roundnum][i] ^ usernamekey[i + 5]
        rc.append(r)
  
  
for in range(3):
    text = []
    flagresult = []
    truetext = []
    rc = []
    testdecrypt(j)
    for in range(5):
        truetext.append(text[i] ^ flagresult[i])
    truetextstr = ''
    for in truetext:
        truetextstr += hex(i)[2:-1]
    # print truetextstr
    testencrypt(j)
    for in range(len(rc)):
        print ('(x%d + y%d * 16^^%08X + z%d * 16^^%08X ^2)  == 16^^%08X ,') % (
            i, i, constkey[j][i % 8], i, constkey[j][i % 8], rc[i])

输出的方程改改,丢给wolfram:



解得:

x0 == 3897419559 && x1 == 2639151649 && x10 == 747450740 &&

x11 == 3755748297 && x12 == 3783566491 && x2 == 3857401126 &&

x3 == 3148293113 && x4 == 1161676053 && x5 == 294910625 &&

x6 == 3168909379 && x7 == 968557934 && x8 == 3468820332 &&

x9 == 1153076640 && y0 == 3032949311 && y1 == 576574626 &&

y10 == 3917136834 && y11 == 1380686977 && y12 == 580039895 &&

y2 == 2649115658 && y3 == 2274249549 && y4 == 2067799403 &&

y5 == 1127930440 && y6 == 2625592633 && y7 == 438417707 &&

y8 == 544012481 && y9 == 3964154936 && z0 == 3301487439 &&

z1 == 1788208676 && z10 == 38241232 && z11 == 3020912882 &&

z12 == 2589519627 && z2 == 2664762180 && z3 == 3632534115 &&

z4 == 1526001984 && z5 == 341456266 && z6 == 204576894 &&

z7 == 2380412302 && z8 == 2085013649 && z9 == 2683983027


整理得到flag
E84DE727B4C7223FC4C8B34F9D4E4221225DD4A26A95E624E5EB45269DE64C0A9ED50B44BBA723F9878E4B4DD8841263453DC5157B401D6B5AF4F1401193FAA1433ADA48145A358ABCE1B8439C7F5D390C31987E39BB056E1A21B92B8DE2358ECEC1FF6C206CF8C17C46C89144BA8DA0EC4834389FFA54B32C8D3174E97AC3C2024783D0DFDC2BC9524B9C81B40F78F2E184A49B2292B4D79A58EF0B

附件是脱壳的程序,方便分析。(附件下载地址:https://bbs.pediy.com/thread-259237.htm



[注意]看雪招聘,专注安全领域的专业人才平台!

收藏
免费
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册