第一次发帖,由于刚开始学习逆向工程,所以文中可能有些错误或者描述不完善的地方,欢迎大家在评论中批评指正或者提出问题进行讨论。
环境
主机:Windows10 64位 专业版
虚拟机软件:VMWare 15.1.0<br>虚拟机环境:Windows XP
软件:IDA VSCode Notepad++
编程语言:Python3 C/C++
文件:7-3.exe SMC.idc
SMC.zip
(30.98 KB, 下载次数: 31)
概览
使用IDA打开7-3.exe,查看程序逻辑,并对部份意义明确的变量重命名,以便分析。
0x01
首先分析开头的汇编程序可知,程序在一开始要求用户输入字符串,即flag,对输入的flag求取长度,当flag长度length=28(0x1C)
时,程序跳转至loc_40107F
mov edx, [ebp+length]
movsx eax, [ebp+edx+var_69]
cmp eax, '7Dh' //'}'
jnz short loc_4010DD
由上述可知,flag长度为28,并且最后一个字符为:}
0x02
在loc_40109E
中
cmp [ebp+var_70], 43h
jge short loc_4010BC
显然,右侧框图为循环,并且var_70
充当了计数的作用,对byte_414C3C
进行了处理,通过相应的解密函数,我们可以得出解密的结果,可以看出,结果为一个函数,通过IDA的函数构建功能,我们可以得到sub_414C3C
。
上述循环结束后,程序将byte_414C3C
转化为sub_414C3C
。在loc_4010BC
中
mov dword ptr [ebp-6Ch], offset sub_414C3C //转化后汇编代码,截图为转化前
mov esi, esp
push offset unk_414BE0
lea edx, [ebp+flag]
push edx
call dword ptr [ebp-6Ch]
程序调用了解密出的函数sub_414C3C
,传入了flag
和unk_414BE0
两个参数。
我们进入函数sub_414C3C
。
由汇编代码可知arg_0
,即flag
前五位分别需要满足的条件,使用IDA自带的ASCII转字符功能可以得到对应的字符,结果为flag{
。
剩余部分,即为对传入的第二个参数unk_414BE0
的解密和重新调用。通过阅读解密的汇编代码,我们可以通过SMC.idc
中的解密函数xor_setp2()
进行解密,将unk_414BE0
转化为函数sub_414BE0
,由sub_414C3C
的逻辑知,有两个参数传入了sub_414BE0
,分别为flag
和unk_414A84
,其中flag
已不包括前五个字符。
0x03
进入sub_414BE0
进行分析,其中arg_0
即为flag
。
图中存在一个循环,内容为对flag
的前四位的判断,由此我们可以再得到flag
的四位字符。
判断情况为,将的flag的每8位与0xCC
异或,得出的结果分别与var_8
中的对应8位,即两个十六进制数相比,只有四次比较均通过,才能够正常执行函数的剩余部分。需要考虑数据的端存储方式,下面给出解密程序的C语言实现
int decode0x03(){
int ch = 0;
int arr[4] = {0x93,0xA9,0xA4,0x98};
for (int i = 3; i >= 0; i--)
{
ch = (arr[i]^0xCC);
printf("%c",ch);
}
return 0;
}
输出结果为
The_
下面分析函数sub_414BE0
对unk_414A84
的处理情况
和之前分析过得情况相似,均是先将其解密,形成一个可使用的函数,然后再进行调用。同样,我们根据解密的过程使用SMC.idc
中的函数xor_setp3()
进行解密,得到函数sub_414A84
,传入参数为flag
和unk_414A30
,这里的flag
已经不包括前九位了。
0x04
进入函数sub_414A84
,由于数据有些复杂,这里使用IDA的伪代码查看功能进行分析。这里截取部分标志性的代码片段进行分析。
//将字母、数字、部分字符的ASCII码存入数组v20中
v2 = 65;
v3 = 0;
do
*((_BYTE *)&v20 + v3++) = v2++;
while ( v2 <= 90 );
v4 = 97;
do
*((_BYTE *)&v20 + v3++) = v4++;
while ( v4 <= 122 );
v5 = 48;
do
*((_BYTE *)&v20 + v3++) = v5++;
while ( v5 <= 57 );
*(__int16 *)((char *)&v20 + v3) = 0x2F2B;
在此代码块中,我们可以看到,程序初始化了一个数组v20
,并将所有的数字和字母以及一部分字符的ASCII码值存在了v20
中。
//rename "a1" as "flag"
//对每三个字符进行一组处理,共处理flag中的六个字符
do
{
v8 = *(_BYTE *)(flag + v7 + 1);
v9 = *(_BYTE *)(flag + v7 + 2);
v10 = *(_BYTE *)(flag + v7) & 3;
v11 = (unsigned int)*(_BYTE *)(flag + v7) >> 2;
v7 += 3;
*v6 = *((_BYTE *)&v20 + v11);
v6[1] = *((_BYTE *)&v20 + (((unsigned int)v8 >> 4) | 16 * v10));
v6[2] = *((_BYTE *)&v20 + (((unsigned int)v9 >> 6) | 4 * (v8 & 0xF)));
v6[3] = *((_BYTE *)&v20 + (v9 & 0x3F));
v6 += 4;
}
while ( v7 < 6 );
//对flag中的下两个字符进行处理
if ( v7 < 8 )
{
v12 = *(_BYTE *)(v7 + flag);
*v6 = *((_BYTE *)&v20 + ((unsigned int)*(_BYTE *)(v7 + flag) >> 2));
v13 = v12 & 3;
if ( v7 == 7 )
{
v6[1] = *((_BYTE *)&v20 + 16 * v13);
v6[2] = 61;
}
else
{
v14 = *(_BYTE *)(v7 + flag + 1);
v6[1] = *((_BYTE *)&v20 + (16 * v13 | ((unsigned int)*(_BYTE *)(v7 + flag + 1) >> 4)));
v6[2] = *((_BYTE *)&v20 + 4 * (v14 & 0xF));
}
v15 = v6 + 3;
*v15 = 61;
v6 = v15 + 1;
}
此代码块为对flag
的处理过程,对于前六个字符,程序将其均分为两份,即每三个字符为一个部分。首先,将第一个字符存入v10
,第二个字符存入v8
,第三个字符存入v9
。
v11
代表将flag
的第一个字符的二进制表示右移两位,即取左侧六位存入v11
,而后将其在v20
数组中对应的字符存在了数组v6
中;
同理分析得,v6[1]
代表flag
第一个字符的后两位与第二个字符的前四位二进制数拼接后在v20
中对应的字符;
v6[2]
代表flag
第二个字符的后四位与第三个字符的前两位二进制数拼接后在v20
中对应的字符;
v6[3]
代表flag
第三个字符的后六位二进制数拼接后在v20
中对应的字符。
第一轮的处理结束,第二轮将会处理接下来的三位字符,处理方式与上述过程类似,不再重复描述。
当完成前六位字符的处理过后,数组v6
中已经根据数组v20
的映射关系存入了八位字符。开始处理第七和第八位字符。
if
中,同样根据v20
的映射关系,将第七位字符的前六位二进制表示所代表的字符存入v6
中;将第七位字符的后两位与第八位字符的前四位拼接而成的六位二进制数根据数组v20
所对应的字符存入v6
中;最后剩余第八个字符的后四位经过乘4运算在后面补两个零,同样构成一个六位二进制数,再根据数组v20
的对应方式,将对应字符存入v6
;最后再将=
存入v6
通过上面的分析,此函数的加密过程即为Base64的加密过程,通过加密flag
后生成的字符串与所给字符串相比较,只有符合时才能够正常运行后面的程序。
开头有:
mov [ebp+var_10], 68566D63h
mov [ebp+var_C], 304E4562h
mov [ebp+var_8], 3D386C52h
在对flag
的判断部分
可以看到,由于地址的连续性,开头的三个变量var_10
var_C
var_8
与经Base64编码过的flag
进行对比,当所有的对比均通过后才可以正常执行接下来的操作。通过IDA的ASCII转字符功能,我们可以得到
mov [ebp+var_10], 'hVmc'
mov [ebp+var_C], '0NEb'
mov [ebp+var_8], '=8lR'
考虑数据的端存储方式,正确的Base64编码后的字符串应为cmVhbEN0Rl8=
,通过解码,即可得到flag
的一部分,下面给出Python3的解密实现
import base64
undecoded_base64 = b'cmVhbEN0Rl8='
des2 = base64.b64decode(undecoded_base64)
print(str(des2).strip('b').strip('\''))
输出为
realCtF_
同样,在最后也存在对传入参数unk_414A30
的处理,同样,我们根据解密的过程使用SMC.idc
中的函数xor_setp4()
进行解密,得到函数sub_414A30
,传入参数只剩下flag
,这里的flag
已经不包括前十七位了。
0x05
进入函数sub_414A30
进行分析,下为开头的数据定义。
var_C= dword ptr -0Ch
var_8= dword ptr -8
var_4= word ptr -4
var_2= byte ptr -2
arg_0= dword ptr 8
------------部分省略------------
mov [ebp+var_C], 7574766Bh
push esi
push edi
mov [ebp+var_8], 68344360h
or edx, 0FFFFFFFFh
mov [ebp+var_4], 6F22h
可知var_C
var_8
var_C
地址是连续的。
loc_414A68
部分即为flag
与var_C
的对比过程,可知flag
的每一个字符需要与var_C
存在一个关系
flag[i] = var_C[i] - 1;
根据比较过程和已知数据并考虑过数据的端处理情况后,这里给出解密函数的C语言实现
int decode0x05(){
int str[10] = {0x6B,0x76,0x74,0x75,
0x60,0x43,0x34,0x68,
0x22,0x6F};
for (int i = 0; i < 10; i++)
{
str1[i] = str1[i]-1;
printf("%c",str1[i]);
}
return 0;
}
输出为
just_B3g!n
测试
综合以上的分析,我们可以给出程序的flag
flag{The_realCtF_just_B3g!n}
总结
在逆向的过程中,要注意数据的端存储方式,以保证正确识别flag
内容,避免由字符顺序带来的不必要的干扰。
其次,不能过分依赖伪代码,有些情况下伪代码可能会因为表示方式而对解决问题产生一定的误导作用。譬如说,我在解决0x03部分的问题时就是被伪代码迷惑,由于表达与理解的差异而无法正确解决,最后还是通过查看汇编代码正确厘清了逻辑关系,并能够完成解密;0x05部分同样也是如此,相对于伪代码,汇编代码的表述更加准确,而不容易出现理解的错误。同时,在分析汇编代码时,能够更好地理解数据在内存中的存储方式,帮助我们理解伪代码中的变量使用,其作用也是很大的,例如在分析0x04时,就是通过伪代码得出Base64
这一重要突破点的。因此要灵活地使用。