看雪Re_翩若惊鸿 – 作者:SecIN技术社区

原文来自SecIN社区—作者:foxcookie

简介:

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

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

一、初步试探

1、初步试探之——运行

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

弹框,MessageBoxA函数

图片[1]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

2、初步试探之——查壳

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

图片[2]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科图片[3]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

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

图片[4]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

3、初步试探之——定位

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

对MessageBoxA函数下断,溯源

图片[5]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科图片[6]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

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

在该段开头即为程序OEP

图片[7]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

按钮事件起始地址:00401410

二、动静结合

1、IDA分析之——F5

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

只有sub_4015E0为自己所写函数,其余均为系统生成
同时看到username、password两个疑似用户名和密码的参数

图片[8]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

可用OD动态调试,验证猜想

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

图片[9]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

IDA分析4015E0处代码

图片[10]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

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

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

动态跟踪call 4015E0处的代码

动态跟踪call 402000的结果eax

图片[11]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

402010为402000调用完之后返回解密的起始地址

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

查看反汇编和内存区段信息可以看出在代码段.text1中

图片[12]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

.text1	如果足够了解PE结构,就可以发现这是一个专门添加的区段

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

由后续分析可以得知:
	402010:代码起始地址
	402AC0:代码结束地址

图片[13]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

继续往下,运行至401614处,查看EBP寄存器值
AB0:待解密的空间大小

图片[14]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

关键信息:
	起始地址:402010
	解密大小:0x0AB0
关键CALL:
	CALL 4015A0		//解密函数
	CALL 401550		//求hash值

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

解密函数:4015A0

可根据程序中的变量和类型进行参数及参数类型的修改
如:默认为v1、v2,可修改为username、password

图片[15]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

求hash函数:401550

全局变量:
g_IsInit:dword_404550
函数:
InitHashTable:sub_401500

图片[16]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

InitHashTable函数代码分析

图片[17]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

确定变量g_hashTable的大小

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

图片[18]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

g_IsInit地址

图片[19]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

计算大小: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是不需要判断的,这部分为标点符号

图片[20]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

优化代码:

// 过滤掉该过滤的信息
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;
}

图片[21]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

三、分析代码

1、OD调试之——dump内存

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

验证函数起始地址:call 402010

图片[22]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

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

图片[23]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

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();

图片[24]-看雪Re_翩若惊鸿 – 作者:SecIN技术社区-安全小百科

四、总结

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

五、感谢

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

来源:freebuf.com 2020-11-17 18:40:08 by: SecIN技术社区

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论