# 三个白猫条条大路通罗马系列2之二进制题分析

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2016-05-25T04:29:00Z
Canonical: https://wepostx.com/topics/272

> 乌云历史资料归档
>
> **原始作者：** netwind
> **原始编号：** superkieran-wooyundrops:1217
> **原始发布时间：** 2016-05-25 12:29
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

# 0x00 前言

该题目是一道算法题，算法涉及二次HASH加密及验证。本人通过分析，认为只有用暴力枚举的方法才能得到认证码。经过长时间跟踪，通过研究加密算法，分析出了通过枚举的方式猜解得到认证码的方法。本文将先对该题进行二进制逆向分析，然后进行算法破解分析。

# 0x01 逆向

直接将exe拖入IDA中，然后查找字符串Your code 和Wrong code

![原文图片](/media/2016/06/0442ac07831801ef07ca92edb6f9eec6)

找到的每处所在的函数都按F2设置一个断点：

![原文图片](/media/2016/06/1a239d2644674d075e5d5c0bd64b6e8a)

选择local win32 debugger，然后点击开始按钮进行调试：

![原文图片](/media/2016/06/a03707a0dcdd984572023f8ff4866475)

这时候程序中断在我们设置的断点处：

![原文图片](/media/2016/06/751edb301866270d0521d54cc2adca1e)

上图中，如果界面显示的是汇编代码，直接按一下F5就切换到了伪C代码里了。
上面的代码具体是干什么的，我们暂时不去关心，这时按F9让程序继续执行，然后输入认证码，随便输入一串字符就好，输长一点，回车之后到达了我们之前下断的wrong code这里，IDA反编译的伪C代码如下：

```text
#!c int __cdecl sub_431970(int a1, int a2) { memset(&v4, 0xCCu, 0x108u); //这里memset是我肉眼识别出函数功能后，做的标记， v6 = strlen(secret[0]);// secret[0] 一个内存中固定的字符串 v5 = a2; for ( i = 0; i < 0x10; i += 4 ) { v7 = 0; for ( j = i; j < i + 4; ++j ) { for ( k = 0; ; ++k ) { v2 = strlen(secret[0]); if ( k >= v2 ) break; if ( secret[0][k] == *(_BYTE *)(j + a1) ) //a1 由b@@ [email protected] @ [email protected] @@X转换得到 { v7 <<= 6; v7 += k; break; } } } *(_BYTE *)v5 = v7 >> 16; *(_BYTE *)(v5 + 1) = BYTE1(v7); *(_BYTE *)(v5 + 2) = v7; v5 += 3; } return sub_42FDC0(); }
```
通过阅读上面的代码，以及跟踪分析，首先发现字符串a1是把我们输入的认证码依次替换掉b@@[email protected]@[email protected]@@X中的@得到的。另外字符串secret[0]是固定的值为：R9Ly6NoJvsIPnWhETYtHe4Sdl+MbGujaZpk102wKCr7/ODg5zXAFqQfxBicV3m8U
通过查看内存值，便一目了然：

![原文图片](/media/2016/06/b9405156d8f463943c4a0dea6ce5c512)

![原文图片](/media/2016/06/8bb2c50dc0f9ae1663cb6d2b20c21528)

通过分析算法发现他的HASH加密算法如下：
把a1字符串每4个一组，如果字符a1[i]是字符串secret[0]里的字符，那么

```text
#!c V7=0 Hash= v7<<6+k //k是a1[i]在字符串secret[0]里的下标。
```
如果a1[i+1]依然满足条件
那么hash= hash<<6+k；依次进行，经过4个字符后 V7从0开始 再hash后面四个字符。
把最终得到的4组hash值以16进制形式存入内存中，每组HASH得到一个3字节的数据，4组完成后共得到12字节数据。
这里随便输入的认证码，本次HASH运算完，得到的12字节数据如下：

![原文图片](/media/2016/06/d652a1f12c50474b2d38e6dbfa843a22)

然后第一次HASH结束。
在函数尾部下断点，F9 一次，再F8后来到了 程序最开始的函数中，也就是第一次我们断下的函数里：

![原文图片](/media/2016/06/e1e5b99d224883452aa3f04e0fd96674)

继续按F8，进入了下面的函数：

```text
#!c int __cdecl sub_431E10(int a1) { memset(&v2, 0xCCu, 0x114u); v9 = a1; v8 = strlen(a1); strlen(secret[0]) = strlen(secret[0]); if ( v8 != 12 ) 这里检查我们上面得到的HASH后的内容是否是12字节 { sub_43019E("Wrong code!\n"); sub_4302C0(1u); } for ( i = 0; i < v8; ++i ) { v5 = 0; for ( j = 0; j < strlen(secret[0]); ++j ) { if ( secret[0][j] == *(_BYTE *)(i + v9) ) //这里检查上面HASH后的每一个字节对应的字符是否在 //字符串secret[0]里 { v5 = 1; break; } } if ( !v5 ) { sub_43019E("Wrong code!\n"); //如果不是 就挂了。 sub_4302C0(1u); } } v3 = malloc(16); memset(v3, 0, 16); sub_430045(v9, v3); sub_42F3F2(v3); return sub_42FDC0(); }
```
如果上面认证全部通过，按F9后，会进入HASH加密的函数：
这个时候需要进行加密的就是上面得到的第一次HASH加密后的12字节数据。
对这12字节加密，每4个一组，共3组，每组得到3字节数据，这样第二次HASH后就得到了9字节的数据。按F9后到达这里：

```text
#!c int __cdecl sub_431CA0(int a1) { memset(&v2, 0xCCu, 0xF0u); v6 = a1; v5 = strlen(a1); if ( v5 != 9 ) //首先判断第二次HASH后的字节长度是否为9，不是就错了。 { sub_43019E("Wrong code!\n"); sub_4302C0(1u); } for ( i = 0; i < v5; ++i ) { if ( (signed int)*(_BYTE *)(i + v6) < 48 || (signed int)*(_BYTE *)(i + v6) > 57 ) //检查第二次HASH后的//每个字节是否都为数字 //字符 { sub_43019E("Wrong code!\n"); sub_4302C0(1u); } } for ( i = 0; i < v5; ++i ) { for ( j = i + 1; j < v5; ++j ) { if ( *(_BYTE *)(i + v6) == *(_BYTE *)(j + v6) ) //检查第二次HASH后的每个字节是否彼此重复。 { sub_43019E("Wrong code!\n"); sub_4302C0(1u); } } } sub_42F1D1(v6); //如果上面都通过了 ，就到达了胜利的彼岸了！ return sub_42FDC0(); }
```
以上就是整个二进制算法的逆向过程，这里我们搞清楚了他的加密算法，下面就是尝试破解认证码了。

# 0x02 算法破解

针对第一部分HASH算法，我计划是从R9Ly6NoJvsIPnWhETYtHe4Sdl+MbGujaZpk102wKCr7/ODg5zXAFqQfxBicV3m8U里任意选取两个（根据需要也可能是1个）字符分别填充到b@@t [email protected] @[email protected] n@@X，并分组进行HASH加密，再验证HASH结果是否满足条件，如果满足条件，那么就保存一下选取的字符以及HASH后的密文。
具体算法如下：

```text
#!c char m2[17]="b@@ [email protected] @ [email protected] @@X",*p,*q; int in1=0,in2=0,in3=0,in4=0; q=m2; get(q,0,1,2); //分组取hash 参数分别为字符串 序号 m2里需要填充的字符坐标 q+=4; get(q,1,1,0); q+=4; get(q,2,0,2); q+=4; get(q,3,1,2); //共4组，到这里infos就得到了所有满足第一次HASH结果的可能的值
```
针对第二次HASH加密，我们把get()函数获得的所有满足条件的密文字符串进行组合，然后对每组字符串再次尝试hash加密，把得到的结果进行检查是否每个字符都是0-9内并且互不重复，把满足条件的结果记录下来。
最后完整破解认证码的代码如下：

```text
#!c #include <stdio.h> #include <string.h> #include <windows.h> char m1[70]="R9Ly6NoJvsIPnWhETYtHe4Sdl+MbGujaZpk102wKCr7/ODg5zXAFqQfxBicV3m8U"; char sz[11]="0123456789"; struct info { char *hash; //存放第一次hash后的值 char *mw; //存放hash前明文 }; info infos[4][1000]; long hash(char *s) { int j=0,v7=0,k=0; v7 = 0; for ( j = 0; j < 4; ++j ) { for ( k = 0; ; ++k ) { if ( k >= strlen(m1)) break; if ( m1[k] == s[j] ) { v7 <<= 6; v7 += k; break; } } } return v7; } int check(char a,char *s) //检查字符a是否在字符串s里 { int i=0; for (i=0;i<strlen(s);i++) { if (s[i]==a) { return 1; } } return 0; } int check2(char *s) //第二次hash 每次hash后检查一下 是否在0-9间，同时检查一下是否有重复数字 { int v7=hash(s); char *v5; v5=(char *)malloc(4); v5[0] = char(v7 >> 16); v5[1] = char(v7 >> 8); v5[2] = char(v7); v5[3]='\0'; if ( !check(v5[0],sz) || !check(v5[1],sz) || !check(v5[2],sz) ) { return 0; } else if(v5[0]==v5[1]||v5[0]==v5[2]||v5[1]==v5[2]) { return 0; } else return 1; } void get(char *s,int index,int in1,int in2) //暴力枚举 获得满足第一次hash结果的 hash密文 和明文 { char *p,*q,char ms[3]; char *v5; int i0=0,i=0,j=0,v7=0,k=0,num=0; p=q=s; for (i0=0;i0<64;i0++) { p[in1]=m1[i0]; for ( i = 0; i < 64; i ++ ) { if (in2!=0) { p[in2]=m1[i]; } else i=65; v7=hash(p); v5=(char *)malloc(4); v5[0] = char(v7 >> 16); v5[1] = char(v7 >> 8); v5[2] = char(v7); v5[3]='\0'; if ( check(v5[0],m1) && check(v5[1],m1) && check(v5[2],m1) ) { infos[index][num].hash=(char *)malloc(4); strcpy(infos[index][num].hash,v5); ms[0]=p[in1]; if (in2==0) { ms[1]='\0'; } else { ms[1]=p[in2]; ms[2]='\0'; } infos[index][num].mw=(char *)malloc(3); strcpy(infos[index][num].mw,ms); num++; delete v5; } } if (i<64) { break; } } } void main() { char m2[17]="b@@ [email protected] @ [email protected] @@X",*p,*q; int in1=0,in2=0,in3=0,in4=0; q=m2; get(q,0,1,2); //分段hash 参数分别为字符串 序号 m2里需要填充的字符坐标 q+=4; get(q,1,1,0); q+=4; get(q,2,0,2); q+=4; get(q,3,1,2); //到这里infos就得到了所有满足第一次HASH结果的可能的值 char temp1[15],temp2[15],temp3[15],temp4[15]; for(in1=0;;in1++) //暴力枚举 所有组合，查找满足第二次条件的值 { if (!infos[0][in1].hash) { break; } strcpy(temp1,infos[0][in1].hash); for (in2=0;;in2++) { strcpy(temp2,temp1); if (!infos[1][in2].hash) { break; } strcat(temp2,infos[1][in2].hash); p=temp2; if (!check2(p)) { continue; } for (in3=0;;in3++) { strcpy(temp3,temp2); if (!infos[2][in3].hash) { break; } strcat(temp3,infos[2][in3].hash); p=temp3+4; if (!check2(p)) { continue; } for (in4=0;;in4++) { strcpy(temp4,temp3); if (!infos[3][in4].hash) { break; } strcat(temp4,infos[3][in4].hash); p=temp4+8; if (!p||!check2(p)) { continue; } else { p=temp4; char result[10]; result[0]='\0'; for (int i=0;i<3;i++) { int v7=hash(p); char *v5=(char *)malloc(4); v5[0] = char(v7 >> 16); v5[1] = char(v7 >> 8); v5[2] = char(v7); v5[3]='\0'; strcat(result,v5); p+=4; } for (i=0;i<strlen(result);i++) { char tt=result[i]; result[i]='a'; if (check(tt,result)) { break; } result[i]=tt; } if (i==strlen(result)) { puts("got it!!!"); printf("%s%s%s%s\n",infos[0][in1].mw,infos[1][in2].mw,infos[2][in3].mw,infos[3][in4].mw); } } } } } } }
```

# 0x03 验证

破解代码运行后结果如下：

![原文图片](/media/2016/06/5385265e38575933186a92b3e27ec35a)

得到了两个结果：
分别输入两个结果，显示如下：

![原文图片](/media/2016/06/799eea43a736924dc8f29eccbbef6cad)

第一个结果，输入后没任何反应，也不提示错误。第二个输入后成功得到flag。
分析认为这个加密算法有2个解，但是最终能解密FLAG的只有第二个才可以。

# 0x04 总结

本题主要是通过两次HASH运算 并分别进行验证的方式加密；两次都可以根据已知限制条件，分别采用暴力枚举的方式进行暴力破解。虽然破解代码量有点大，但是破解速度并没有收到影响，执行破解代码后能马上得到结果！
文中有分析不当或不周全的地方，希望大家批评指正！

## Replies
