本文最后更新于: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没能识别出来时我们也能有自己正确的判断。