本文最后更新于:2022年4月13日 下午
                  
                
              
            
            
              
              前前后后参考了几篇博文,感觉自己有些值得分享的东西,就有了这篇文章
题目地址:https://buuoj.cn/challenges#crackMe 
首先,国际惯例,查壳
没壳,拖进IDA里通过String定位到关键代码:
首先是主函数代码:
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 int  wmain ()  {   FILE *v0;    FILE *v1;    char  v3;    char  v4;    char  v5;    char  v6;    char  v7;    char  v8;    char  v9;    char  v10;    char  v11;    printf ("Come one! Crack Me~~~\n" );   v10 = 0 ;   memset (&v11, 0 , 0xFF u);   v8 = 0 ;   memset (&v9, 0 , 0xFF u);   while  ( 1  )   {     do      {       do        {         printf ("user(6-16 letters or numbers):" );         scanf ("%s" , &v10);         v0 = (FILE *)sub_4024BE();         fflush(v0);       }       while  ( !(unsigned  __int8)sub_401000(&v10) );       printf ("password(6-16 letters or numbers):" );       scanf ("%s" , &v8);       v1 = (FILE *)sub_4024BE();       fflush(v1);     }     while  ( !(unsigned  __int8)sub_401000(&v8) );     sub_401090(&v10);     v6 = 0 ;     memset (&v7, 0 , 0xFF u);     v4 = 0 ;     memset (&v5, 0 , 0xFF u);     v3 = ((int  (__cdecl *)(char  *, char  *))loc_4011A0)(&v6, &v4);     if  ( sub_401830((int )&v10, &v8) )     {       if  ( v3 )         break ;     }     printf (&v4);   }   printf (&v6);   return  0 ; }
 
不难看出,程序中存在一个死循环,而需要让代码中的死循环跳出,那么sub_401830和loc_4011A0需要成立的,但是loc_4011A0这个函数的参数都是前面已经确定好的且不可控为0,所以这个函数是不需要去分析的。故sub_401830是我们重点分析的目标。
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 bool  __cdecl sub_401830 (int  a1, const  char  *a2)  {   int  v3;    unsigned  int  v4;    unsigned  int  v5;    unsigned  int  v6;    char  v7;    char  v8;    char  v9;    unsigned  __int8 v10;    unsigned  __int8 v11;    char  v12;    int  v13;    char  v14;    char  v15;    char  v16;    char  v17;    v4 = 0 ;   v5 = 0 ;   v11 = 0 ;   v10 = 0 ;   v16 = 0 ;   memset (&v17, 0 , 0xFF u);   v14 = 0 ;   memset (&v15, 0 , 0xFF u);   v9 = 0 ;   v6 = 0 ;   v3 = 0 ;   while  ( v6 < strlen (a2) )   {     if  ( isdigit (a2[v6]) )     {       v8 = a2[v6] - 48 ;     }     else  if  ( isxdigit (a2[v6]) )     {       if  ( *(_DWORD *)(*(_DWORD *)(__readfsdword(0x30 u) + 24 ) + 12 ) != 2  )         a2[v6] = 34 ;       v8 = (a2[v6] | 0x20 ) - 87 ;     }     else      {       v8 = ((a2[v6] | 0x20 ) - 97 ) % 6  + 10 ;     }     v9 = v8 + 16  * v9;     if  ( !((signed  int )(v6 + 1 ) % 2 ) )     {       *(&v14 + v3++) = v9;       v9 = 0 ;     }     ++v6;   }   while  ( (signed  int )v5 < 8  )   {     v10 += byte_416050[++v11];     v12 = byte_416050[v11];     v7 = byte_416050[v10];     byte_416050[v10] = v12;     byte_416050[v11] = v7;     if  ( *(_DWORD *)(__readfsdword(0x30 u) + 104 ) & 0x70  )       v12 = v10 + v11;     *(&v16 + v5) = byte_416050[(unsigned  __int8)(v7 + v12)] ^ *(&v14 + v4);     if  ( *(_DWORD *)(__readfsdword(0x30 u) + 2 ) & 0xFF  )     {       v10 = -83 ;       v11 = 43 ;     }     sub_401710(&v16, a1, v5++);     v4 = v5;     if  ( v5 >= &v14 + strlen (&v14) + 1  - &v15 )       v4 = 0 ;   }   v13 = 0 ;   sub_401470(&v16, &v13);   return  v13 == 43924 ; }
 
首先,从最后一行我们可以看出,v13需要等于43924,所以打开sub_401470函数
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 _DWORD *__usercall sub_401470@<eax>(int  a1@<ebx>, _BYTE *a2, _DWORD *a3) {   int  v3;    int  v4;    int  v6;    int  v8;    int  v9;    int  v10;    int  v11;    int  v12;    char  v13;    int  v14;    int  v15;    int  v16;    int  v17;    char  v18;    int  v19;    int  v20;    int  v23;    int  v24;    _DWORD *result;    int  v26;    if  ( *a2 == 100  )   {     *a3 |= 4u ;     v4 = *a3;   }   else    {     *a3 ^= 3u ;   }   v3 = *a3;   if  ( a2[1 ] == 98  )   {     _EAX = a3;     *a3 |= 0x14 u;     v6 = *a3;   }   else    {     *a3 &= 0x61 u;     _EAX = (_DWORD *)*a3;   }   __asm { aam }   if  ( a2[2 ] == 97  )   {     *a3 |= 0x84 u;     v9 = *a3;   }   else    {     *a3 &= 0xA u;   }   v8 = *a3;   v10 = ~(a1 >> -91 );   if  ( a2[3 ] == 112  )   {     *a3 |= 0x114 u;     v12 = *a3;   }   else    {     *a3 >>= 7 ;   }   v11 = *a3;   v13 = v10 - 1 ;   if  ( a2[4 ] == 112  )   {     *a3 |= 0x380 u;     v15 = *a3;   }   else    {     *a3 *= 2 ;   }   v14 = *a3;   if  ( *(_DWORD *)(*(_DWORD *)(__readfsdword(0x30 u) + 24 ) + 12 ) != 2  )   {     if  ( a2[5 ] == 102  )     {       *a3 |= 0x2DC u;       v17 = *a3;     }     else      {       *a3 |= 0x21 u;     }     v16 = *a3;   }   if  ( a2[5 ] == 115  )   {     *a3 |= 0xA04 u;     v18 = (char )a3;     v20 = *a3;   }   else    {     v18 = (char )a3;     *a3 ^= 0x1AD u;   }   v19 = *a3;   _AL = v18 - v13;   __asm { daa }   if  ( a2[6 ] == 101  )   {     *a3 |= 0x2310 u;     v24 = *a3;   }   else    {     *a3 |= 0x4A u;   }   v23 = *a3;   if  ( a2[7 ] == 99  )   {     result = a3;     *a3 |= 0x8A10 u;     v26 = *a3;   }   else    {     *a3 &= 0x3A3 u;     result = (_DWORD *)*a3;   }   return  result; }
 
可以知道我们需要a2满足所有的if,v13此时就可以等于43924
也就是v16需要是这样一个BYTE数组: [0x64,0x62,0x61,0x70,0x70,0x73,0x65,0x63],即ddappsec v16的值知道了,我们还需要知道这个值是怎么来的
我们可以从第二部分代码第64行得知,byte_416050是通过与变换后的密码异或得到v16。且我们现在已经知道账号为welcomebeijing,所求的是账号的密码。因此我们需要通过动态调试,获取byte_416050的值
在x32dbg中将汇编断点打在xor上,观察寄存器中的内容,循环看8次就得到我们要的内容了 记录如下: [0x2a,0xd7,0x92,0xe9,0x53,0xe2,0xc4,0xcd]
接下来只需要编写脚本解密即可
1 2 3 4 5 6 7 8 9 10 11 12 13 import  hashlib key = [0x2a , 0xd7 , 0x92 , 0xe9 , 0x53 , 0xe2 , 0xc4 , 0xcd ] text = "dbappsec"  flag = []for  i in  range (len (text)):     flag.append(hex (ord (text[i])^key[i]).replace("0x" ,"" )) final_flag='' .join(each for  each in  flag) md = hashlib.md5(final_flag.encode('utf-8' )).hexdigest()print (md)
 
以上就是和师傅们的WP大同小异的部分。我想重点说明的是反调试部分,也就是那些莫名其妙的__readfsdword。我所看的WP中都没能解答我对这些东西的疑惑,只能自己去找了。
《逆向工程核心原理》第51章或许能解答我们的一些疑惑(侵删)
我们需要重点关注的就是最后一页的提示部分,不难看出,我们题目中出现的几个反调试手段
1 2 3 if  ( *(_DWORD *)(*(_DWORD *)(__readfsdword(0x30 u) + 24 ) + 12 ) != 2  )if  ( *(_DWORD *)(__readfsdword(0x30 u) + 104 ) & 0x70  )if  ( *(_DWORD *)(__readfsdword(0x30 u) + 2 ) & 0xFF  )
 
其实就是寻找PEB结构体中的特定字段来判断是否处于被调试的状态,其中:
1 if  ( *(_DWORD *)(__readfsdword(0x30 u) + 2 ) & 0xFF  )
 
从书中我们可以看出就是我们的字段BeingDebugged,我们的IsDebuggerPresent最后寻找的东西其实就是这个字段
而
1 if  ( *(_DWORD *)(__readfsdword(0x30 u) + 104 ) & 0x70  )
 
这个其实从PEB的结构中我们也不难看出也是反调试中经常出现的字段NtGlobalFlag
最后这个:
1 if  ( *(_DWORD *)(*(_DWORD *)(__readfsdword(0x30 u) + 24 ) + 12 ) != 2  )
 
一步步跟进,首先__readfsdword(0x30u) + 24),这玩意也是经常用在反调试中的字段ProcessHeap,它是一个结构体,那偏移量为12是什么呢?我们再补一张图:
可以看出是字段Flags,如同书中所说,进程处于被调试状态时,它被设置成了特定的值(从我们的题目中我们可以得出这个值在正常情况下应该是2)
至此,我们大体上弄明白了题目中出现的反调试手段。
识别出来了我们需要进行反反调试,绕过反调试代码执行函数的正常逻辑。
这里给出几个比较常见的手段:
其一是动调时手动修改代码,比如汇编下把jz改成jmp/jnz,我改成了jnz,机器码是74改成75
另外一个就是使用插件ScyllaHide了,这里我们只需要使用Basic模块即可(准确来说Hide from PEB就行)
总结一下,题目本身不难。有坑的地方即是那几个反调试的部分会修改那8个值扰乱分析。当识别出并过掉后题目基本就没问题了。
另外一点就是当fs和0x30同时出现在我们的代码中时需要额外注意。这表明PEB要被访问了,出题人要开始整活了。
PEB结构体是反调试手段中最基础也是经常会用到的技术。我们不能局限于只是能识别一些API如IsDebuggerPresent,而是同时要对背后的底层有自己的认识,这样在IDA没能识别出来时我们也能有自己正确的判断。