From ChaMd5安全团队核心成员 Poyoten
比赛时由于第二天有事,第三题re没做,所以也就没有写WP。但是后来看了大佬们的WP,可能是题比较简单,所以写得也比较简略,我等菜鸟根本看不懂。于是乎就想尝试分析下这次比赛的逆向题,以希望对和我一样的小白能有所帮助。
(如果想要获得更好的观看效果,建议把本文分享出去,复制其链接在电脑上观看。)
这题是一个dll文件,IDA载入,直接查看export,如下图,除了入口和两个tls,还有三个导出函数encrypt
,encrypt_str
,get_string
。
这三个导出函数名也太敏感了,直接先看这三个函数,直接f5看伪代码,如图
主逻辑就在get_string
函数中,先将24个字节的目标验证字串复制到栈区,然后接收输入,将输入与常量0xEFBEADDE
通过encrypt_str
进行处理,处理完的输入与目标验证字串进行比较。而encrypt_str
的作为只是调用encrypt
对输入进行每两位一处理。
encrypt
函数就是核心算法所在了,对两字节的字串使用key1
进行加密或变换。先用字串第二个字节计算出第一个字节的新值,再用第一个字节的新值计算第二个字节的新值,如此循环,直至退出循环,最后把变换后的两字节新值写入到输入的相应位置中。 由于做题当时并未看出这是什么算法,又考虑到encrypt是对两个字节进行变换加密,对其它字串并不影响,而且其中的变动因素也只有2个字节的输入。所以思路很明显,我们可以两位两位的爆破,输入位数可以从上面的分析得出:24字节。
在使用伪代码还原算法的过程中,由于核心算法处三处使用了movsx
,致使还原出来的算法总是有问题。于是直接使用asm
,代码如下:
#include <stdio.h>
#include <stdio.h>
#include <memory.h>
typedef unsigned char BYTE;
BYTE unk_6E282000[] = { 0xBF,0xF1,0x6A,0x2C,0x10,0x0B,0x16,0x59,0xBA,0x3A,0x8C,0x49,0x05,0x1B,0x04,0xE2,0x85,0xD5,0xC2,0xFC,0xD7,0x9B,0xE9,0x42,0x0 };
int encrypt( BYTE * a3,BYTE *a4)
{
BYTE v4; // eax@1
int result; // eax@3
BYTE a1, a2;
v4 = 0xB9 ;
_asm {
push edi
push esi
push ebp
mov edi, a4
mov eax, a3
push eax
mov cl, [eax]
mov dl, [eax + 1]
mov al, 0B9h
nop
loc_6E281228 :
movsx esi, dl
mov ebx, eax
and ebx, 3
mov bl, [edi + ebx]
add ebx, eax
mov ebp, esi
sar ebp, 5
shl esi, 4
xor esi, ebp
add esi, edx
xor ebx, esi
add ecx, ebx
movsx ebx, cl
mov esi, ebx
sar esi, 5
shl ebx, 4
xor esi, ebx
add esi, ecx
movsx ebx, al
sar ebx, 0Bh
and ebx, 3
mov bl, [edi + ebx]
add ebx, eax
xor esi, ebx
add edx, esi
sub eax, 47h
cmp al, 0D9h
jnz loc_6E281228
pop eax
mov [eax], cl
mov [eax + 1], dl
pop ebp
pop esi
pop edi
}
result = *a3;
return result;
}
int encrypt_str(BYTE *a1, signed int a2, BYTE *a3)
{
int result; // eax@1
int v6; // ebx@
result = a2 / 2;
if (a2 / 2 > 0)
{
v6 = 0;
do
{
result = encrypt(a1, a3);
v6 += 2;
} while (v6 != a2);
}
return result;
}
int main()
{
int result = 0; // eax@1
BYTE v2[25]; // [sp+1Eh] [bp-3Eh]@1
BYTE v1[4] = { 0xDE, 0xAD ,0xBE, 0xEF };
int count = 0;
bool flag;
do {
for (BYTE i1 = 0x20; i1 <= 0x7f; i1++)
{
for (BYTE i2 = 0x20; i2 <= 0x7f; i2++)
{
BYTE a[2];
flag = false;
a[0] = i1;
a[1] = i2;
encrypt_str(a, 2, v1);
if (a[0] == unk_6E282000[count] && a[1] == unk_6E282000[count + 1]) {
printf("%c%c", i1,i2);
flag = true;
break;
}
}
if (flag) break;
}
if (!flag) {
printf("fail..."); return 0;
}
count += 2;
} while (count < 24);
return result;
}
最后得出flag:zctf{ha_hAha_d1l_exp0r7} 后来听别人说,别人又听大佬说核心算法是XTEA加密算法,去查了查资料,果然是,传送门(http://blog.csdn.net/gsls200808/article/details/48243019)。
这是一个console程序,先试运行下,如图:
拖进IDA,shift+f12查看字串,如图:
输入提示、出错提示、成功提示字串都有,查看引用这些字串的位置,只有一处引用,伪代码如下:
程序逻辑很简单,有4个条件判断:第一是检查输入字串长度小于32;第二是输入前4个字符为"ZCTF";第三是loc_4026D0
返回为真;第四是sub_402800
返回为真。根据此处代码,可知输入形如ZCTF{XXXXXXXXXX}
,其中的XXXXXXXXXX
为第三、第四个函数的参数。
在查看loc_4026D0
和sub_402800
处代码时,发现有混淆,如下图:
上图中的.text:004026E0 call sub_4026F5
和.text:00402808 call sub_40282E
采用了相同手法,call之后,返回地址入栈,再将返回地址pop到esi中,实际上的操作将此条命令后的地址传送给esi
。
除此之外,loc_4026D0
处还有个switch结构,里面6个调用函数的返回地址都作了修改。
如上图中的.text:00402688 add dword ptr [ebp+4], 1
就将返回地址+1,实际上sub_402680
的返回地址是.text:00402740 jmp short loc_402782
,其余5个函数的情况类似。
用IDA去除混淆后,伪代码如下:
在sub_4026D0
中,主要是一个switch结构,程序走向由a2
中各字符的低4位-1控制,退出循环只有两个途径:一是sub_402490
返回为false,二是进入default选支。
细看case 0-5的调用函数,其作用都是loc_4026E5
字串的某两位(一位ascii非0,一位ascii为0)交换。sub_402490
的作用是检查loc_4026E5
中两个相邻的非0字符低位的是否都小于高位的,如果是则返回true,循环继续,否则就校验失败。default
是检查loc_4026E5
的6-10个字符不为空,并返回true。
loc_4026E5
的初始状态为:
如果sub_4026D0
要返回true,则最后loc_4026E5
状态应该为:
再对比case0-5的调用函数作用,很明显,这是一个汉诺塔游戏的步数求解。loc_4026E5
每5个字节分别表示一个柱子,共三个柱子:A,B,C。"AA"、"BB"、"CC"、"DD"、"EE"表示5个尺寸从小到大的盘子。
而switch值与调用函数作用的对应关系为: case 0 A->B case 1 A->C case 2 B->A case 3 B->C case 4 C->B case 5 C->A
手动解算后,case的序列为053254104123104524104,即输入串中XXXXXXXXXX
的ascii码低4位依次为164365215234215635215。
sub_402800
则更好理解,根据sub_402430
中的幻数及具体算法很容易就能确定是md5算法。最后与loc_40280D
处的字串比较,loc_40280D
处的值为:
到此,思路就明晰了,只能爆破。但是有21位字符只知其ascii低4位值,每个字符有6种可能性,那就是6**21=21936950640377856,数目过于庞大。考虑到,通常游戏算法类的算法中,相同操作或程序流向对应相同的字符。那我们不妨先假设相同的ascii低4位对应相同的字符进行枚举,则枚举次数最多为6**6=46656。
# -*- coding:utf8 -*-
from hashlib import md5
md5_s = '0f2e7e447593ec9af3463e9c8745b892'
def main():
# 164365215234215635215
# 053254104123104524104
table = [0x20,0x30,0x40,0x50,0x60,0x70]
i = [1,2,3,4,5,6]
for i[0] in table:
for i[1] in table:
for i[2] in table:
for i[3] in table:
for i[4] in table:
for i[5] in table:
tmp = [1,2,3,4,5,6]
for j in xrange(6):
tmp[j] = chr(i[j]+j+1)
tmp_str = tmp[0]+tmp[5]+tmp[3]+tmp[2]+tmp[5]+tmp[4]+tmp[1]+\
tmp[0]+tmp[4]+tmp[1]+tmp[2]+tmp[3]+tmp[1]+tmp[0]+\
tmp[4]+tmp[5]+tmp[2]+tmp[4]+tmp[1]+tmp[0]+tmp[4]
if md5(tmp_str).hexdigest() == md5_s:
print 'ZCTF{%s}' %tmp_str
return True
return False
if __name__ == '__main__':
if main() :
print 'ok'
else:
print 'false'
XXXXXXXXXX
解出来就是A&$#&5rA5r#$rA5rA5
。