看雪论坛
发新帖
1

[原创]一个有趣的CrackMe的分析与注册机的编写 by lantie@15PB

蓝铁 2017-9-8 23:20 706

第四题 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
  • 致谢
    感谢看雪提供平台!
上传的附件:
本主题帖已收到 1 次赞赏,累计¥2.00
最新回复 (9)
1
蓝铁 2017-9-8 23:22
2
看雪的MarkDown  不能自动生成目录,这是一个Bug?
8
kanxue 2017-9-9 00:04
3
蓝铁 看雪的MarkDown 不能自动生成目录,这是一个Bug?
目前还没这功能,以后可以考虑增加
2
木瓜枫叶 2017-9-9 08:45
4
看看学习下
11
爱琴海 2017-9-9 09:35
5
详细,条理清晰,支持。
gaoweb 2017-9-9 10:18
6
   
1
君子谬 2017-9-9 10:30
7
老西
1
noNumber 2017-9-9 18:08
8
666写得很详细。
5
Tennn 2017-9-9 20:57
9
支持  ....
AperOdry 2017-9-10 11:26
10
谢谢分享,让我们这些菜逼又对分析的思路深入一步了,唯今苦叹数学差矣,让我对密码学入门简直太难
返回



©2000-2017 看雪学院 | Based on Xiuno BBS | 微信公众号:ikanxue
Time: 0.013, SQL: 11 / 京ICP备10040895号-17