看雪Re_翩若惊鸿

  • A+

简介:

看雪上一道Re的crackme题目,参考了[email protected]老师的帖子。是一个很有意思的程序,可以学到很多。

参考链接:https://bbs.pediy.com/thread-221038.htm

一、初步试探

1、初步试探之——运行

```
运行程序,尝试各种用户名和密码组合,测试报错格式及关键字

弹框,MessageBoxA函数
```

image1.png

2、初步试探之——查壳

基本信息:
VC6.0编写的程序
PE文件格式
EXE32位文件

image2.png

image3.png
查看导入表信息:
MFC程序动态链接库:MFC42.dll

image4.png

3、初步试探之——定位

```
动态调试,找到程序入口点

对MessageBoxA函数下断,溯源
![image5.png](/img/sin/M00/00/4A/wKg0C1-mOgKAIVmDAAEEzxOC6KI786.png)
![image6.png](/img/sin/M00/00/4A/wKg0C1-mOgWAaxJIAAC1-R9bRdA685.png)

事件按钮函数与报错关键字

在该段开头即为程序OEP
```

image7.png

按钮事件起始地址:00401410

二、动静结合

1、IDA分析之——F5

```
IDA定位至起始地址:401410,查看可获取基本信息

只有sub_4015E0为自己所写函数,其余均为系统生成
同时看到username、password两个疑似用户名和密码的参数
![image8.png](/img/sin/M00/00/4A/wKg0C1-mOlWAbYPpAAClSlTBMWU285.png)
可用OD动态调试,验证猜想

调用call 4015E0函数
栈中信息为用户名和密码
```

image9.png

IDA分析4015E0处代码

image10.png
data_start:解密的起始地址
len:解密长度
sub_4015A0:解密函数
sub_404550:求hash函数

2、OD调试之——代码跟踪

```
动态跟踪call 4015E0处的代码

动态跟踪call 402000的结果eax
![image11.png](/img/sin/M00/00/4A/wKg0C1-mOoSACow1AACEW39C_hs107.png)
402010为402000调用完之后返回解密的起始地址

eax=402010是代码地址,所以解密的是代码

查看反汇编和内存区段信息可以看出在代码段.text1中
![image12.png](/img/sin/M00/00/4A/wKg0C1-mOpCAOC9MAABrOEK_xvU616.png)
.text1 如果足够了解PE结构,就可以发现这是一个专门添加的区段

继续分析,call 402AC0的返回地址为402AC0

由后续分析可以得知:
402010:代码起始地址
402AC0:代码结束地址
![image13.png](/img/sin/M00/00/4A/wKg0C1-mOp6AEEitAACbWxRyUVM616.png)
继续往下,运行至401614处,查看EBP寄存器值
AB0:待解密的空间大小
![image14.png](/img/sin/M00/00/4A/wKg0C1-mOq2AE6CtAAB79kHOA5A198.png)
关键信息:
起始地址:402010
解密大小:0x0AB0
关键CALL:
CALL 4015A0 //解密函数
CALL 401550 //求hash值
```

3、IDA分析之——提取代码

```
解密函数:4015A0

可根据程序中的变量和类型进行参数及参数类型的修改
如:默认为v1、v2,可修改为username、password
![image15.png](/img/sin/M00/00/4A/wKg0C1-mOsqASnUtAABRXAv_0HY778.png)
求hash函数:401550

全局变量:
g_IsInit:dword_404550
函数:
InitHashTable:sub_401500
![image16.png](/img/sin/M00/00/4A/wKg0C1-mOtmAZPkPAAB35rQ_EYQ942.png)
InitHashTable函数代码分析
![image17.png](/img/sin/M00/00/4A/wKg0C1-mOuiAc5nwAACJA8i20Wc076.png)
确定变量g_hashTable的大小

从InitHashTable函数中可知,g_hashTable的大小应为数据段中g_hashTable的地址到g_IsInit的地址

g_hashTabe地址
![image18.png](/img/sin/M00/00/4A/wKg0C1-mOv2AcEmuAABcs58UEAw417.png)
g_IsInit地址
![image19.png](/img/sin/M00/00/4A/wKg0C1-mOwqAZyNjAABLiVpUqVQ649.png)
计算大小:404550-404150 = 0x400
伪代码中循环条件可设为:v1 < 0x400
```

4、代码分析之——暴力破解

```
编写代码注意事项:
循环条件:
暴力破解hash值,需要先建立循环,循环的起始和结束条件很重要,因为完整的遍历4字节是非常慢的,由于这个值是密码的前4位,可知其应该是在数字、大小写字母中间,所以起始值是0x30303030,结束值是0x7A7A7A7A,刚好将数字、大小写字母全部包含进来

循环体逻辑:
1、将源代码拷贝到新的缓冲区中
2、解密缓冲区代码
3、求缓冲区的hash值
4、判断求出的hash值与0xAFFE390F是否一致,不是继续
5、一致输出当前16进制以及字符信息,字符就是密码的前4位

// 1. 将源代码拷贝到新的缓冲区中
memcpy(g_deCode, g_byCode, 0xab0);
// 2. 解密缓冲区代码
decode_code(g_deCode, 0xab0, i);
// 3. 求缓冲区的hash值
DWORD dwHash = Calc_CRC32(0, g_deCode, 0x00000AB0);
// 4. 判断求出的hash值与 0xAFFE390F 是否一致,不是继续
if (0xAFFE390F == dwHash)
{ // 5. 一致输出当前16进制以及字符信息,字符就是密码的前4位
printf("right ! 0x%08x \n", i);
byte pByte = (byte)&i;
printf("right ! %c %c %c %c \n", pByte[0], pByte[1], pByte[2], pByte[3]);
getchar();
}

优化循环
在循环时每次值中的每一个字节如果>=0x3a且<=0x40是不需要判断的,这部分为标点符号
![image20.png](/img/sin/M00/00/4A/wKg0C1-mOzKAYTeIAAAuXi6kLZs956.png)
优化代码:

// 过滤掉该过滤的信息
if ( (i & 0xFF) >=0x3A && (i & 0xFF) <=0X40 ||
(i & 0xFF) < 0x30 || (i & 0xFF) > 0x7A)
{
continue;
} else if ((i>>8 & 0xFF) >= 0x3A && (i >> 8 & 0xFF) <= 0X40 ||
(i >> 8 & 0xFF) < 0x30 || (i >> 8 & 0xFF) > 0x7A

)

{
continue;
}
else if (( i >> 16 & 0xFF) >= 0x3A && (i >> 16 & 0xFF) <= 0X40 ||
(i >> 16 & 0xFF) < 0x30 || (i >> 16 & 0xFF) > 0x7A
)
{
continue;
}
else if ((i >> 24 & 0xFF) >= 0x3A && (i >> 24 & 0xFF) <= 0X40 ||
(i >> 24 & 0xFF) < 0x30 || (i >> 24 & 0xFF) > 0x7A)
{
continue;
}

完整暴力破解hash值代码

include

include

// hash表数组
int g_hashTable[0x400] = {0};
// 是否初始化标志
bool g_IsInit = false;
// hash表的key
unsigned int g_key = 0xEDB88320;
// 用于放解密代码的缓冲区
byte g_deCode[0x00000AB0] = { 0 };
// 源程序 00402010处开始的代码,使用OD数据转换插件,拷贝出来
byte g_byCode[0x00000AB0] = {
0x33, 0xc0, 0xc3, 0x68, 0x45, 0x7f, 0xab, 0xfb, 0xf8, 0x3f, 0xab, 0x9f, 0x59, 0x6f, 0xcf, 0x16,
}; // 此处省略完整数组代码

// 解密缓冲区函数
unsigned int __cdecl decode_code(byte *mem_code, unsigned int nLen, unsigned int password_left_4)
{
unsigned int result; // [email protected]

result = 0;
password_left_4 ^= 0xD9EE7A1B;
if (nLen)
{
    do
    {
        mem_code[result] ^= *((byte *)&password_left_4 + (result & 3));
        ++result;
    } while (result < nLen);
}
return result;

}

// 初始化hash表函数
unsigned int InitHashTable()
{
int key; // [email protected]
unsigned int v1; // [email protected]
int *pData; // [email protected]
unsigned int result; // [email protected]
signed int v4; // [email protected]

key = g_key;
g_IsInit = 1;
v1 = 0;
pData = g_hashTable;
do
{
    *pData = v1;
    result = v1;
    v4 = 8;
    do
    {
        result = ((result & 1) != 0 ? key : 0) ^ (result >> 1);
        --v4;
    } while (v4);
    *pData = result;
    ++pData;
    ++v1;
} while (v1 < 0x400);
return result;

}

// 计算CRC32
int __cdecl Calc_CRC32(int nFlag, byte *mem_code, int nLen)
{
int v3; // [email protected]
unsigned int i; // [email protected]

if (!g_IsInit)
    InitHashTable();
v3 = 0;
for (i = ~nFlag; v3 < nLen; ++v3)
    i = g_hashTable[(unsigned __int8)i ^ mem_code[v3]] ^ (i >> 8);
return ~i;

}

int main()
{
clock_t start, finish;
double totaltime;
start = clock();

for (unsigned int i = 0x30303030; i < 0x7A7A7A7A; i++)
{
    // 过滤掉该过滤的信息
    if ( (i & 0xFF) >=0x3A && (i & 0xFF) <=0X40 ||
        (i & 0xFF) < 0x30 || (i & 0xFF) > 0x7A)
    {
        continue;
    } else if ((i>>8 & 0xFF) >= 0x3A && (i >> 8 & 0xFF) <= 0X40  ||
        (i >> 8 & 0xFF) < 0x30 || (i >> 8 & 0xFF) > 0x7A

        )
    {
        continue;
    }
    else if (( i >> 16 & 0xFF) >= 0x3A && (i >> 16 & 0xFF) <= 0X40 ||
        (i >> 16 & 0xFF) < 0x30 || (i >> 16 & 0xFF) > 0x7A
        )
    {
        continue;
    }
    else if ((i >> 24 & 0xFF) >= 0x3A && (i >> 24 & 0xFF) <= 0X40 ||
        (i >> 24 & 0xFF) < 0x30 || (i >> 24 & 0xFF) > 0x7A)
    {
        continue;
    }
    // 1. 将源代码拷贝到新的缓冲区中
    memcpy(g_deCode, g_byCode, 0xab0);
    // 2. 解密缓冲区代码
    decode_code(g_deCode, 0xab0, i);
    // 3. 求缓冲区的hash值
    DWORD dwHash = Calc_CRC32(0, g_deCode, 0x00000AB0);
    // 4. 判断求出的hash值与 `0xAFFE390F` 是否一致,不是继续
    if (0xAFFE390F == dwHash)
    { // 5. 一致输出当前16进制以及字符信息,字符就是密码的前4位
        printf("right ! 0x%08x \n", i);
        byte* pByte = (byte*)&i;
        printf("right ! %c %c %c %c \n", pByte[0], pByte[1], pByte[2], pByte[3]);
        finish = clock();
        totaltime = (double)(finish - start) / CLOCKS_PER_SEC;
        printf("此程序的运行时间为 %d 分, %d 秒 ! \n" , (long)totaltime/60, (int)totaltime%60);

        getchar();
    }
}

return 0;

}
```

image21.png

三、分析代码

1、OD调试之——dump内存

```
使用OD动态跟踪函数,当输入的密码前4位为BEEF时,内存中会正确解密代码,然后完成对用户名和密码前4位之后的验证和判断

验证函数起始地址:call 402010
```

image22.png

2、动静结合之——函数解析

image23.png
```
IDA结合OD,多次组合判断函数作用

402300处函数:自己定义的类的构造函数
402A40处函数:自己定义的类的成员函数(对ecx指向的空间进行赋值),功能是将传入的10进制字符串转换为16进制
402800处函数:对用户名的16进制形式进行二次修改,即为用户名计算值
4021A0处函数:对密码去除前4位之后的字符串进行修改,即为密码计算值
402330处函数:对象的memcmp,判断用户名计算值与密码计算值是否一致

402800处函数:对用户名的16进制数据和常量字符串201510261314的16进制数据进行混合计算

def CalcUser(num1,num2):
n1 = num2;
num3 = 0;
num4 = 0;
arr = [];
while n1 != 0:
n = n1 & 0xffffffff;
#print hex(n);
num3 = n * num1;
num3 += num4;
#print hex(num3);
n = num3 & 0xffffffff;
arr.insert(0, n);
n1 >>= 32;
num3 >>= 32;
if num3 != 0:
num4 = num3;
return arr;

4021A0处函数:将传入的密码每两个字节转为一个16进制数据,并与0x86进行异或,传入的值不为0-F即返回0

def CalcPassAndXor(passwd):
array = bytearray(passwd);
size = len(array);
arr = [];
i = 0;
while i < size:
ch1 = array[i];
if ch1 >= 0x30 and ch1 <= 0x39:
ch1 -= 0x30;
elif ch1 >= 0x61 and ch1 <=0x66:
ch1 -= 0x57;
elif ch1 >= 0x41 and ch1 <=0x46:
ch1 -= 0x37;
# print ch1;
i+=1;
if i == size:
break;
ch2 = array[i];
if ch2 >= 0x30 and ch2 <= 0x39:
ch2 -= 0x30;
elif ch2 >= 0x61 and ch2 <= 0x66:
ch2 -= 0x57;
elif ch2 >= 0x41 and ch2 <= 0x46:
ch2 -= 0x37;

  ch3 =  ch1 << 4 | ch2;
  ch3 ^= 0x86;
  arr.append(ch3);
  i += 1;

return arr;
```

3、完整注册机编写

```

-- coding: utf-8 --

author="[email protected]"

将10进制字符串转为16进制

def calc(name,count=10):
num = 0;
array = bytearray(name);
for i in range(0, len(name)):
by = array[i] - 0x30;
#print hex(by);
if num > 0:
num *= count;
num += by;
return num;

将10进制字符串转为16进制,或将16进制进行一些运算

def calc2(bytearr,count=0x10):
num = 0;
array = bytearray(bytearr);
for i in range(0, len(bytearr)):
if num > 0:
num *= count;
by = array[i];
by -= 0x30;
if by >=80:
by = 256-by;
print hex(by);
if num > by:
num -= by;
else:
num += by;
if num == 0:
num += by;
return num;

对两个数进行计算,以4字节为单位

def CalcUser(num1,num2):
n1 = num2;
num3 = 0;
num4 = 0;
arr = [];
while n1 != 0:
n = n1 & 0xffffffff;
#print hex(n);
num3 = n * num1;
num3 += num4;
#print hex(num3);
n = num3 & 0xffffffff;
arr.insert(0, n);
n1 >>= 32;
num3 >>= 32;
if num3 != 0:
num4 = num3;
return arr;

对密码进行转换。

def CalcPassAndXor(passwd):
array = bytearray(passwd);
size = len(array);
arr = [];
i = 0;
while i < size:
ch1 = array[i];
if ch1 >= 0x30 and ch1 <= 0x39:
ch1 -= 0x30;
elif ch1 >= 0x61 and ch1 <=0x66:
ch1 -= 0x57;
elif ch1 >= 0x41 and ch1 <=0x46:
ch1 -= 0x37;
# print ch1;
i+=1;
if i == size:
break;
ch2 = array[i];
if ch2 >= 0x30 and ch2 <= 0x39:
ch2 -= 0x30;
elif ch2 >= 0x61 and ch2 <= 0x66:
ch2 -= 0x57;
elif ch2 >= 0x41 and ch2 <= 0x46:
ch2 -= 0x37;

    ch3 =  ch1 << 4 | ch2;
    ch3 ^= 0x86;
    arr.append(ch3);
    i += 1;
return arr;

注册机

def CalcPass():
username = raw_input('please input name: ');
num1 = calc2(username,10);
#print 'sum:' + hex(num1);
num2 = calc2("201510261314",10);
#print 'sum:' + hex(num2);
arr = CalcUser(num1,num2)
password = "BEEF";
for i in range(len(arr)):
dic = {'0': 'B6', '1': 'B7', '2': 'B4', '3': 'B5', '4': 'B2', '5': 'B3', '6': 'B0', '7': 'B1', '8': 'BE',
'9': 'BF', 'A': 'BC', 'B': 'BD', 'C': 'BA', 'D': 'BB', 'E': 'B8', 'F': 'B9'}

    string1 = hex(arr[i]).upper();
    for i in range(2, len(string1)):
        ch = string1[i];
        if ch == 'L':
            continue;
        # print dic[ch];
        password += dic[ch];
print "password: " + password;

if name == 'main':
CalcPass();
```

image24.png

四、总结

这个题目从OD、IDA等分析工具的使用,到VS、python代码的编写,是一个很综合的题目。动态调试和静态分析可以说是完美的结合了起来,方方面面的能力都可以得到锻炼。强烈推荐自己去学习一遍。

五、感谢

最后,非常感谢 [email protected] 师傅的帖子,写的异常详细,考虑到了各种问题。帖子的链接已放在文章首部,强推!!!