这是很早之前的CTF比赛,已经有大佬发过WP了。最近一直拿各种CM练习,硬盘里存了一些分析笔记,也没好意思发出来,毕竟别人已经写过,总有种拾人牙慧的嫌疑。
就当是自己学习的记录吧,感谢那些曾经帮到过我的分享文章。拿出来或许还会帮到一些像我一样的小白,因为某个点过不去而苦苦思索的,到处寻求帮助。共勉!
代码风格比较乱,可能有些地方说的也不太正确,若有大佬发现,谢谢指正!
流程分析
跟踪发现,有一个作者自己添加的异常
.text:0040154E int 3
向上找SHE异常处理的回调。下列代码在栈中构造一个EXCEPTION_REGISTRATION结构
.text:00401440 sub_401440 proc near
.text:00401440 var_1C = dword ptr -1Ch
.text:00401440 ms_exc = CPPEH_RECORD ptr -18h
.text:00401440
.text:00401440 ; __unwind { // __except_handler4
.text:00401440 push ebp
.text:00401441 mov ebp, esp
.text:00401443 push 0FFFFFFFEh
.text:00401445 push offset stru_407F78
.text:0040144A push offset __except_handler4
.text:0040144F mov eax, large fs:0
.text:00401455 push eax
.text:00401456 sub esp, 0Ch
.text:00401459 push ebx
.text:0040145A push esi
.text:0040145B push edi
.text:0040145C mov eax, ___security_cookie
.text:00401461 xor [ebp+ms_exc.registration.ScopeTable], eax
.text:00401464 xor eax, ebp
.text:00401466 push eax
.text:00401467 lea eax, [ebp+ms_exc.registration]
.text:0040146A mov large fs:0, eax
就在__except_handler4回调函数哪里下断点,异常触发后来到这里,有一个回到用户代码
738EACEC - FFE6 jmp esi ; Project1.00401485
跟踪几次来到sub_404BA0,这里就开始验证输入了
变形MD5
下面这段就是变形的MD5,其实就是把Magic Number给修改了
输入的Key经过变形的MD5计算的结果与预定义的值进行比较,不同就ExitProcess()。
预定义的MD5结果是:0FCh, 0E4h, 0AAh, 83h, 0E4h, 0ABh, 0E9h, 0Dh, 0D0h
.text:00404C1A mov edx, g_szKey
.text:00404C20 lea ecx, [ebp+var_E0]
.text:00404C26 push esi ; key_len
.text:00404C27 mov [ebp+var_DC], 0
.text:00404C31 mov [ebp+var_E0], 0
.text:00404C3B mov [ebp+var_D8], 76543210h
.text:00404C45 mov [ebp+var_D4], 0FEDCBA98h
.text:00404C4F mov [ebp+var_D0], 89ABCDEFh
.text:00404C59 mov [ebp+var_CC], 1234567h ;注意Magic number并不是标准的
.text:00404C63 call sub_4058F0
.text:00404C68 add esp, 4
.text:00404C6B lea ecx, [ebp+var_E0]
.text:00404C71 call sub_4059B0 ; 内部有MD5计算
.text:00404C76 xor esi, esi
.text:00404C78 nop dword ptr [eax+eax+00000000h]
.text:00404C80
.text:00404C80 loc_404C80: ; CODE XREF: sub_404BA0+FB↓j
.text:00404C80 mov al, [ebp+esi+var_88]
.text:00404C87 cmp al, ds:byte_407690[esi]
.text:00404C8D jz short loc_404C97
.text:00404C8F push 0 ; _DWORD
.text:00404C91 call ExitProcess
.text:00404C97
.text:00404C97 loc_404C97: ; CODE XREF: sub_404BA0+ED↑j
.text:00404C97 inc esi
.text:00404C98 cmp esi, 10h
.text:00404C9B jl short loc_404C80
其实没必要纠结这部分,在进行算法分析时直接修改跳转跳过即可,因为不是标准的MD5,所以仅存的希望“MD5在线解密”也破灭了
等到最后分析出真正的Key后,可以再来验证这一部分
另类迷宫分析
变形MD5验证之后就是关键验证算法了
代码的大致功能:
- 输入的key字符串,每2个字符为一组
- 每组的第一个字符只能是”TVWX”,这4个字符是方向
- 每组的第二个字符,只能是“BCDFGHJKMPQRTVWXY2346789”中的一个,该字符距离字符串最后一个字符“9”的距离表示向某个方向移动的值(和矩阵坐标相关)
- 最终验证通过的条件是两个全局标志位g_byte == 0x36 && g_nFlag == 0x10F
.text:00404820 StartAddress proc near
.text:00404820
.text:00404820 i = dword ptr -8
.text:00404820 nLocation = byte ptr -1
.text:00404820 lpThreadParameter= dword ptr 8
.text:00404820
.text:00404820 push ebp
.text:00404821 mov ebp, esp
.text:00404823 sub esp, 0Ch
.text:00404826 push ebx
.text:00404827 push esi
.text:00404828 push edi
.text:00404829 push eax
.text:0040482A push ebp
.text:0040482B mov ebp, esp
.text:0040482D push 0
.text:0040482F push 0FFEEFFEEh
.text:00404834 push 0EEFFEEFFh
.text:00404839 mov eax, large fs:0
.text:0040483F push eax
.text:00404840 mov large fs:0, esp
.text:00404847 pop eax
.text:00404848 mov large fs:0, eax
.text:0040484E pop eax
.text:0040484F pop eax
.text:00404850 pop eax
.text:00404851 test eax, eax
.text:00404853 jz short loc_404857
.text:00404855 inc ebx
.text:00404857
.text:00404857 loc_404857:
.text:00404857 pop eax
.text:00404858 mov ebp, eax
.text:0040485A pop eax
.text:0040485B mov esi, g_szKey
.text:00404861 xor ebx, ebx
.text:00404863 mov ecx, esi
.text:00404865 mov [ebp+nLocation], 0
.text:00404869 mov [ebp+i], ebx ; 计数
.text:0040486C lea edx, [ecx+1]
.text:0040486F nop
.text:00404870
.text:00404870 loc_404870:
.text:00404870 mov al, [ecx]
.text:00404872 inc ecx
.text:00404873 test al, al
.text:00404875 jnz short loc_404870
.text:00404877 sub ecx, edx ; strlen(key)
.text:00404879 jz loc_4049A3
.text:0040487F nop
.text:00404880
.text:00404880 loc_404880:
.text:00404880 lea edi, [esi+ebx] ; ebx用来计数
.text:00404883 xor esi, esi
.text:00404885
.text:00404885 loc_404885:
.text:00404885 movsx eax, byte ptr [edi+esi*2]
.text:00404889 push eax ; Val
.text:0040488A push offset Str ; "BCDFGHJKMPQRTVWXY2346789"
.text:0040488F call ds:strchr
.text:00404895 movsx ecx, byte ptr [edi+esi*2+1]
.text:0040489A mov ebx, eax
.text:0040489C push ecx ; Val
.text:0040489D push offset Str ; "BCDFGHJKMPQRTVWXY2346789"
.text:004048A2 call ds:strchr
.text:004048A8 add esp, 10h
.text:004048AB mov ecx, eax
.text:004048AD test ebx, ebx
.text:004048AF jz short loc_4048D2
.text:004048B1 test ecx, ecx
.text:004048B3 jz short loc_4048D2
.text:004048B5 mov eax, offset Str ; "BCDFGHJKMPQRTVWXY2346789"
.text:004048BA sub bl, al ; 距离第一个元素的距离
.text:004048BC mov eax, (offset Str+17h) ; "9"
.text:004048C1 sub al, cl ; 距离最后一个元素的距离
.text:004048C3 shl bl, 4
.text:004048C6 or bl, al
.text:004048C8 mov [ebp+esi+nLocation], bl
.text:004048CC inc esi
.text:004048CD cmp esi, 1
.text:004048D0 jb short loc_404885 ; 就执行1次,但不知道编译器怎么给搞成循环了
.text:004048D2
.text:004048D2 loc_4048D2:
.text:004048D2
.text:004048D2 movzx eax, [ebp+nLocation]
.text:004048D6 mov bl, [ebp+nLocation]
.text:004048D9 shr eax, 4
.text:004048DC and bl, 0Fh
.text:004048DF add eax, 0FFFFFFF4h ; switch 4 cases
.text:004048E2 cmp eax, 3
.text:004048E5 ja loc_40496A ; ja比较两个无符号数
.text:004048EB jmp ds:off_4049CC[eax*4] ; switch jump
.text:004048F2 ; ---------------------------------------------------------------------------
.text:004048F2
.text:004048F2 case_2:
.text:004048F2
.text:004048F2 mov cl, bl
.text:004048F4 call sub_404B00
.text:004048F9 jmp short loc_404972
.text:004048FB ; ---------------------------------------------------------------------------
.text:004048FB
.text:004048FB case_0:
.text:004048FB
.text:004048FB mov cl, bl
.text:004048FD call sub_404AA0
.text:00404902 jmp short loc_404972
.text:00404904 ; ---------------------------------------------------------------------------
.text:00404904
.text:00404904 case_1:
.text:00404904
.text:00404904 push eax
.text:00404905 add esp, 2 ; 这种堆栈的处理,感觉像是函数内联了,case_1和case_3都有
.text:00404908 mov eax, esp
.text:0040490A sub eax, 2
.text:0040490D nop
.text:0040490E mov esp, eax
.text:00404910 pop eax
.text:00404911 mov edx, g_nFlag
.text:00404917 movzx eax, bl
.text:0040491A sub edx, eax
.text:0040491C jmp short loc_404936
.text:0040491E ; ---------------------------------------------------------------------------
.text:0040491E
.text:0040491E case_3:
.text:0040491E
.text:0040491E push eax
.text:0040491F add esp, 2
.text:00404922 mov eax, esp
.text:00404924 sub eax, 2
.text:00404927 nop
.text:00404928 mov esp, eax
.text:0040492A pop eax
.text:0040492B mov edx, g_nFlag
.text:00404931 movzx eax, bl
.text:00404934 add edx, eax
.text:00404936
.text:00404936 loc_404936:
.text:00404936 movzx eax, ds:g_szBuf_1[edx]
.text:0040493D movzx ecx, ds:g_szBuf_2[edx]
.text:00404944 add g_byte, bl
.text:0040494A xor ecx, eax
.text:0040494C movzx eax, ds:g_szBuf_3[edx]
.text:00404953 sub ecx, eax
.text:00404955 mov g_nFlag, edx
.text:0040495B movzx eax, ds:g_szBuf_4[edx]
.text:00404962 sub ecx, eax ; _DWORD
.text:00404964 jnz short loc_40496A
.text:00404966 test bl, bl
.text:00404968 jnz short loc_404978
.text:0040496A
.text:0040496A loc_40496A:
.text:0040496A
.text:0040496A push 0
.text:0040496C call ExitProcess
.text:00404972
.text:00404972 loc_404972:
.text:00404972
.text:00404972 mov edx, g_nFlag
.text:00404978
.text:00404978 loc_404978:
.text:00404978 mov esi, g_szKey
.text:0040497E mov eax, esi
.text:00404980 mov ebx, [ebp+i]
.text:00404983 add ebx, 2
.text:00404986 mov [ebp+i], ebx
.text:00404989 lea edi, [eax+1]
.text:0040498C nop dword ptr [eax+00h]
.text:00404990
.text:00404990 loc_404990:
.text:00404990 mov cl, [eax]
.text:00404992 inc eax
.text:00404993 test cl, cl
.text:00404995 jnz short loc_404990
.text:00404997 sub eax, edi ; strlen(key)
.text:00404999 cmp ebx, eax
.text:0040499B jb loc_404880
.text:004049A1 jmp short loc_4049A9
.text:004049A3 ; ---------------------------------------------------------------------------
.text:004049A3
.text:004049A3 loc_4049A3:
.text:004049A3 mov edx, g_nFlag
.text:004049A9
.text:004049A9 loc_4049A9:
.text:004049A9 cmp g_byte, 36h
.text:004049B0 jnz short loc_4049BA
.text:004049B2 cmp edx, 10Fh
.text:004049B8 jz short loc_4049C2
.text:004049BA
.text:004049BA loc_4049BA:
.text:004049BA push 0 ; _DWORD
.text:004049BC call ExitProcess
.text:004049C2
.text:004049C2 loc_4049C2:
.text:004049C2 pop edi
.text:004049C3 pop esi
.text:004049C4 pop ebx
.text:004049C5 mov esp, ebp
.text:004049C7 pop ebp
.text:004049C8 retn
.text:004049C8 StartAddress endp
.text:004049C8
.text:004049C8 ; ---------------------------------------------------------------------------
.text:004049C9 align 4
.text:004049CC off_4049CC dd offset case_0 ; DATA XREF: StartAddress+CB↑r
.text:004049CC dd offset case_1 ; jump table for switch statement
.text:004049CC dd offset case_2
.text:004049CC dd offset case_3
.text:004049DC align 10h
本来偷懒打算看F5代码的,但是F5代码甚是费解,索性就一行一行的手撸汇编,还原为C吧,就当练习了
这个矩阵的维数也是在逆向中结合全局数组的元素个数和移动关系,一步一步的看出来的
有4个全局的二维数组[17][17],按照一定的规则进行运算可以得到迷宫地图
//BCDFGHJKMPQRTVWXY2346789
//TVWX 这4个字符是方向
BOOL CheckKey()
{
char szBuf[] = "BCDFGHJKMPQRTVWXY2346789";
int nLocation = 0;
int nTmp = 0;
int nIndex = 0;
int nBegLen = 0;
int nEndLen = 0;
char szTmp[4] = { 0 };
int nKeyLen = strlen(g_szKey);
int nBufLen = strlen(szBuf);
for (int i = 0; i < nKeyLen; i += 2)
{
szTmp[0] = g_szKey[i];
char *pRetA = strstr(szBuf, szTmp);
szTmp[0] = g_szKey[i + 1];
char *pRetB = strstr(szBuf, szTmp);
if (pRetA && pRetB)
{
nBegLen = pRetA - szBuf; //距离第一个元素的距离
nEndLen = &szBuf[nBufLen - 1] - pRetB; //距离最后一个元素的距离
}
nBegLen -= 12;
switch (nBegLen)
{
case 0: //T
MoveDown(nEndLen);
break;
case 1: //V
MoveLeft(nEndLen);
break;
case 2: //W
MoveUp(nEndLen);
break;
case 3: //X
MoveRight(nEndLen);
break;
default:
ExitProcess(0);
}
}
if (g_byte != 0x36 || g_nFlag != 0x10F)
{
ExitProcess(0);
}
return TRUE;
}
/*
左移
*/
void MoveLeft(int nNum)
{
g_byte += nNum;
g_nFlag -= nNum;
int i = g_nFlag / 17;
int j = g_nFlag % 17;
char chRet = (g_szBuf_1[i][j] ^ g_szBuf_2[i][j]) - g_szBuf_3[i][j] - g_szBuf_4[i][j];
if (chRet || !nNum)
{
ExitProcess(0);
}
}
/*
右移
*/
void MoveRight(int nNum)
{
g_byte += nNum;
g_nFlag += nNum;
int i = g_nFlag / 17;
int j = g_nFlag % 17;
char chRet = (g_szBuf_1[i][j] ^ g_szBuf_2[i][j]) - g_szBuf_3[i][j] - g_szBuf_4[i][j];
if (chRet || !nNum)
{
ExitProcess(0);
}
}
/*
下移
*/
void MoveDown(int nNum)
{
g_byte += nNum;
g_nFlag += nNum * 17;
int i = g_nFlag / 17;
int j = g_nFlag % 17;
char chRet = (g_szBuf_1[i][j] ^ g_szBuf_2[i][j]) - g_szBuf_3[i][j] - g_szBuf_4[i][j];
if (chRet || !nNum)
{
ExitProcess(0);
}
}
/*
上移
*/
void MoveUp(int nNum)
{
g_byte += nNum;
g_nFlag -= nNum * 17;
int i = g_nFlag / 17;
int j = g_nFlag % 17;
char chRet = (g_szBuf_1[i][j] ^ g_szBuf_2[i][j]) - g_szBuf_3[i][j] - g_szBuf_4[i][j];
if (chRet || !nNum)
{
ExitProcess(0);
}
}
生成Map
通过验证成功的条件为0,可以逆推出地图
char chRet = (g_szBuf_1[i][j] ^ g_szBuf_2[i][j]) - g_szBuf_3[i][j] - g_szBuf_4[i][j];
生成地图的C代码:
void GetMap()
{
char chRet = 0;
printf("地图:\r\n");
for (int i = 0; i < 17; ++i)
{
for (int j = 0; j < 17; ++j)
{
chRet = (g_szBuf_1[i][j] ^ g_szBuf_2[i][j]) - g_szBuf_3[i][j] - g_szBuf_4[i][j];
if (!chRet)
{
printf(" "); //路
g_Map[i][j] = 1;
}
else
{
printf("■"); //墙
}
}
printf("\r\n");
}
}
生成的地图如下所示,从左上角的那个入口,到右下角的那个出口,走法就是Key,
逆推Key序列
当时把C代码撸出来,感觉这个迷宫有点别致。普通的迷宫都是上下左右4个方向,每次只能走一步,这个好像一次可以走好几步,比如一次向左移动5位……这种操作
按照这个思路来逆推key就麻烦了,搞了好久也不知道该如何写
后来实在没招了,就参考了别人的wp。才发现时我想多了,可以先把它当做每次只能移动一步的迷宫,后续再把连续相同方向的移动合并就可以了
参考WP:https://blog.csdn.net/kevin66654/article/details/58585538
具体的实现代码就放到附件中吧,这里就不放了