第四题 KeyGenMe 分析 by lantie@15PB
[toc]
目录(MarkDown自动生成的目录截图):
一. 收集信息与初步分析
0. 运行程序
有弹出信息框,猜测是MessageBoxA
1. 使用PEID 初步分析程序
VC6.0编写的程序
有MFC42.dll 是MFC程序的动态库,动态编译的VC6 MFC程序
2. 动态调试,找到按钮事件
使用OD调试运行程序,MessageBoxA(VC6默认是ascii版)下断,输入用户名和密码试探
断下之后,查看调用堆栈(使用Alt+K键或是点击工具栏k),找到自己模块,查看代码
可以发现有一些字符串
可见可能是按钮事件的代码,跟踪代码到这个函数的上一层,如果是mfc42模块说明这个函数的开始就是按钮事件的开始,如果不是,继续再往上分析
跟踪之后发现到了mfc42模块,也找到了按钮事件的特征,说明是有字符串的代码所在的函数就是按钮事件的代码,脱到函数开始,然后开始分析!~
按钮事件起始地址:00401410
二. 动静结合-暴力破解hash代码解密正确代码
3. 动态调试,分析按钮事件代码
由于本题目采用的是共享库编译,且MFC DLL是序号导出的函数,所以直接在OD中看反汇编不是太方便,为了能更快速的分析代码,可以使用IDA Pro的强大功能 F5 静态分析,与OD动态调试相结合
① 先使用IDA F5 查看代码,猜测信息
可以发现按钮函数里只有一个是函数 sub_4015E0
是自己的函数,参数是2个,所以猜测是用户名和密码。
② 使用OD动态调试 跟踪,验证自己的猜测
跟踪代码可以发现 调用 call 4015E0
的堆栈信息是用户名和密码
③ 再次使用IDA F5 查看 函数 sub_4015E0
的代码
对F5之后的伪代码 进行修改,由于上个代码可以发现传递的参数是字符串,且在上一个函数中有CString对象的拷贝赋值,所以参数类型应该是CString&,修改参数类型,以及参数名称
修改完参数和参数名称之后仔细分析一下伪代码 仔细分析可以发现这段代码中有两个函数很关键,一个是解密函数sub_4015a0
,一个是求hash的函数sub_401550
,有两个值很重要,一个是解密的起始地址(伪代码中的data_start
),一个是解密长度(伪代码中的len
)。 下一步就是验证分析的结果已经查看关键的信息的值和调用情况
④ 使用 OD 动态跟踪 call 4015E0
的代码
动态跟踪查看 call 402000
的结果eax 根据eax=402010 大概猜测是代码地址,所以解密的是代码 查看其反汇编和内存区段信息可以看出是在代码段.text1
中 根据区段名称,可以猜测可能是专门添加的区段。 再继续分析下面的代码,发现 call 402ac0
的返回值也是代码地址00402ac0
再继续单步跟踪,可以看出以下计算的值就是要解密的大小
到此,关键的值已经分析出来了,就是
起始地址:402010
解密大小:0x0AB0
剩下就是分析关键了两个call :
CALL 004015A0
解密函数
CALL 00401550
求hash值
由于求hash值,这个操作不可逆,所以只能使用暴力破解,暴力破解需要不停的执行代码,所以需要将解密函数和求hash的函数从IDA中抠出来,写代码暴力破解。
4. 使用IDA F5 抠出解密代码和求hash代码
① 抠解密函数
使用 IDA f5 查看解密函数,并修改参数和参数类型 可以看出以上代码没有引用其他的变量或是函数,所以将其直接复制到VS中即可。
② 抠求hash函数
求hash函数的伪代码中有函数调用,也有全局变量,这些需要都统计
需要将引用的函数以及变量全部扣出来,才可以。所以先分析引用的函数以及变量的功能并为其命名,然后再抠代码 查看IDA的伪代码,可以分析出如下结果
InitHashTable函数的代码分析如下:
分析完代码中的引用函数和变量之后,最后需要确定变量g_hashTbale
的大小。从InitHashTable
函数中可以看出g_hashTbale
的大小应该是数据段中g_hashTbale
的地址到g_IsInit
的地址。在IDA中查看两个变量的地址
g_hashTbale
的地址
g_IsInit
的地址 计算大小为 404550
-404150
= 0x400
,所以可以修改伪代码中的循环条件为 v1 < 0x400
,完整修改后的代码稍后给出。 到此需要抠的代码基本分析完毕,但要做一个暴力破解hash值的程序还需要将加密的数据一并抠出来,这个比较方便的就是从OD中抠出。
5. 编写代码-暴力破解hash值
① 编写代码时的一些坑
循环条件 暴力破解hash值,需要先建立循环,循环的起始和结束条件很重要,因为完整的遍历4字节是非常慢的,由于这个值是密码的前4位,可知其应该是在数字、大小写字母中间,所以起始值是 0x30303030
,结束值是 0x7A7A7A7A
,刚好将数字、大小写字母全部包含进来。
循环体的逻辑 循环体的逻辑应该是: ① 将源代码拷贝到新的缓冲区中 ② 解密缓冲区代码 ③ 求缓冲区的hash值 ④ 判断求出的hash值与 0xAFFE390F
是否一致,不是继续 ⑤ 一致输出当前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();
}
优化循环 循环的时候,其实有些是可以优化的,优化有2
在循环时每次值中的每一个字节如果>=0x3a且<<=40 是不需要判断的,这部分是标点符号
在循环时每次值中的每一个字节如果 <= 0x30 或者 >= 0x7a,都是可以忽略的,因为不是字母、数字。
我们只需要将以上两种优化加入循环,暴力破解的速度就会增加很多。优化的代码如下:
// 过滤掉该过滤的信息
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;
}
② 运行效果
三. 分析解密之后的检测函数,写出注册机
6. 从OD中dump内存中解密成功的代码
使用OD动态跟踪程序,当输入的密码前4位是前面算出来的字符串BEEF
时,内存中会正确解密代码,然后完成对用户名和密码前4位之后的验证和判断,验证函数就是之前分析的call 402010
。代码解密之后的部分代码截图: 解密完成后,我们可以使用OD中的Ollydump插件dump已经解密的整个内存信息。然后再使用OD分析。
7. 使用IDA分析代码
使用 F5 查看代码 很多函数调用无法分清到底哪个函数是做什么的,所以只能先动态调试,尝试多组用户名和密码,,观察CALL调用时参数和返回值等信息。
8. 再次使用OD分析代码
经过多组用户名和密码的尝试,OD与IDA动静结合分析,终于发现 402010
处代码的一些函数的功能。
00402300
处函数,是自己定义的类的构造函数(判断依据:有ecx传参,返回值是this指针,对缓冲区初始化)
00402A40
处函数,是自己定义的类的成员函数(对ecx指向的空间进行赋值),功能是将传入的十进制字符串转为16进制
00402800
处函数,对用户名的16进制形式进行二次修改,即为用户名计算值
004021a0
处函数,对密码去除前4位之后的字符串进行修改,即为密码计算值
00402330
处函数,对象的memcmp, 判断用户名计算值与密码计算值是否一致,如果一致则正确,否则则失败
由于本文是后面补的,分析是在1年多以前了,所以一些OD动态调试记录的注释已经丢失,所以没办法将详细的过程分析,有的是思路和代码,供大家参考
对于以上来看,最关键的就是00402800
处函数 与 004021a0
处函数。两个函数最终计算出的来的值必须是一致的才算是正确。所以需要仔细分析两个函数的代码。为了计算大数方便,我使用了python来编写这两个函数的等价代码。
00402800
处函数,此函数的功能就是对用户名的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;
004021a0
处函数,此函数的主要功能就是将传入的密码每两个字节转为一个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;
换句话说,由于上面的函数只支持0-F的16进制值,可以简化为一张表,这张表对应 0-F 中的值。代码如下:
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'}
9. 完整的注册机代码
# -*- 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();
10. 一组正确的用户名和序列号
四. 总结
题目设计 这个题目有一个设计的非常巧妙的地方,那就是密码的前4位作为了解密代码的key,如果不对就无法解密,由于需要暴力破解出前4位密码,瞬间增加了这个题目的难度。而后解密代码的算法也比较巧妙,用户名和密码都参加了计算,非常适合有一点强度的练手。
分析收获 经过分析这个题目,从逆向工具OD、IDA的使用,到VS、python的代码编写,方方面面都涉及到了,真是一个综合性的练习!收获大大的!
硬道理 逆向分析就是练!练!练!,只有打不死的小强,没有练不出来的技术!
时间与广告 2017年9月7日,lantie@15PB 15PB信息安全教育专注于信息安全官网:http://www.15pb.com.cn
致谢 感谢看雪提供平台!
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
上传的附件: