简介:
看雪上一道Re的crackme题目,参考了lantie@15PB老师的帖子。是一个很有意思的程序,可以学到很多。
参考链接:https://bbs.pediy.com/thread-221038.htm
一、初步试探
1、初步试探之——运行
运行程序,尝试各种用户名和密码组合,测试报错格式及关键字
弹框,MessageBoxA函数
2、初步试探之——查壳
基本信息:
VC6.0编写的程序
PE文件格式
EXE32位文件
查看导入表信息:
MFC程序动态链接库:MFC42.dll
3、初步试探之——定位
动态调试,找到程序入口点
对MessageBoxA函数下断,溯源
事件按钮函数与报错关键字
在该段开头即为程序OEP
按钮事件起始地址:00401410
二、动静结合
1、IDA分析之——F5
IDA定位至起始地址:401410,查看可获取基本信息
只有sub_4015E0为自己所写函数,其余均为系统生成
同时看到username、password两个疑似用户名和密码的参数
可用OD动态调试,验证猜想
调用call 4015E0函数
栈中信息为用户名和密码
IDA分析4015E0处代码
data_start:解密的起始地址
len:解密长度
sub_4015A0:解密函数
sub_404550:求hash函数
2、OD调试之——代码跟踪
动态跟踪call 4015E0处的代码
动态跟踪call 402000的结果eax
402010为402000调用完之后返回解密的起始地址
eax=402010是代码地址,所以解密的是代码
查看反汇编和内存区段信息可以看出在代码段.text1中
.text1 如果足够了解PE结构,就可以发现这是一个专门添加的区段
继续分析,call 402AC0的返回地址为402AC0
由后续分析可以得知:
402010:代码起始地址
402AC0:代码结束地址
继续往下,运行至401614处,查看EBP寄存器值
AB0:待解密的空间大小
关键信息:
起始地址:402010
解密大小:0x0AB0
关键CALL:
CALL 4015A0 //解密函数
CALL 401550 //求hash值
3、IDA分析之——提取代码
解密函数:4015A0
可根据程序中的变量和类型进行参数及参数类型的修改
如:默认为v1、v2,可修改为username、password
求hash函数:401550
全局变量:
g_IsInit:dword_404550
函数:
InitHashTable:sub_401500
InitHashTable函数代码分析
确定变量g_hashTable的大小
从InitHashTable函数中可知,g_hashTable的大小应为数据段中g_hashTable的地址到g_IsInit的地址
g_hashTabe地址
g_IsInit地址
计算大小: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是不需要判断的,这部分为标点符号
优化代码:
// 过滤掉该过滤的信息
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 <windows.h>
#include<time.h>
// 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; // eax@1
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; // ebp@1
unsigned int v1; // edi@1
int *pData; // ecx@1
unsigned int result; // eax@2
signed int v4; // esi@2
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; // ecx@3
unsigned int i; // eax@3
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;
}
三、分析代码
1、OD调试之——dump内存
使用OD动态跟踪函数,当输入的密码前4位为BEEF时,内存中会正确解密代码,然后完成对用户名和密码前4位之后的验证和判断
验证函数起始地址:call 402010
2、动静结合之——函数解析
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__="lantie@15PB"
# 将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();
四、总结
这个题目从OD、IDA等分析工具的使用,到VS、python代码的编写,是一个很综合的题目。动态调试和静态分析可以说是完美的结合了起来,方方面面的能力都可以得到锻炼。强烈推荐自己去学习一遍。
五、感谢
最后,非常感谢 lantie@15PB 师傅的帖子,写的异常详细,考虑到了各种问题。帖子的链接已放在文章首部,强推!!!