selph
selph
发布于 2022-07-20 / 226 阅读
0
0

新160个CrackMe练习:018-crackme_0006

018-crackme_0006

算法难度:⭐⭐⭐⭐

爆破难度:⭐

信息收集

运行情况:

image

查壳与脱壳:

汇编写的程序,无壳

image

查字符串:

存在提示字符串:

image

调试分析

这个程序计算比较复杂,这里通过x86dbg+IDA结合进行分析

找到验证逻辑

直接从oep开始分析:

汇编写的程序,这里是一个窗口过程,参数里这个是过程函数,处理窗口消息的函数

image

一般自己创建窗口写窗口过程函数都是类似这样的:参数uMsg是消息号,根据消息来进行不同的操作

LRESULT CALLBACK MyWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){
    switch(uMsg){
        case WM_CREATE:
            //content
            return 0;
        case WM_DESTORY:
            //content
            return 0;
        default:
            return DefWindowProc(hwnd,uMsg,wParam,lParam);
    }
    return 0;
}

分析思路很简单,找到Check的那个分支,下断点向下分析

首先这是消息号0x111的分支,0x111是WM_COMMAND消息,对于这个消息,它的参数是命令号,也就是反汇编里的这个arg_8,然后根据命令号进行下一层的跳转

这里需要跳转,跳转之后就是在判断命令号,命令号在eax里,这里如果eax=0x3f0,则是about按钮的事件,check按钮事件位于命令号0x3ee

image

所以直接在0x4011ca下一行0x4011d0下断点即可,从这里往下就是验证逻辑了

第一段运算

首先是第一段运算,基于机器特征的校验码:

通过自写函数去获取卷序列号,分别获取C盘和D盘的,然后进行一段浮点运算,中间经历的这些自写的位移函数在此处无意义

image

这里来看一下这些自写函数:首先是获取卷序列号的:通过Win32 API获取信息,直接返回

image

然后是左移函数:从参数获取值,然后左移指令进行运算,通过eax返回

image

循环左移函数:使用循环位移运算指令实现的

image

然后Add就是两数相加,没啥好看的,最后这个浮点运算很关键:将获取到的两个卷序列号分别类型转换成浮点型,通过浮点运算,分别计算两个数的平方,然后开根号,最后转换回十进制:

image

第二段运算

第二段运算是基于用户名的运算,首先判断了用户名长度,必须大于4字节,然后调用了一个自写函数对用户名计算了一个结果,然后进行位移操作,与或操作之后得到一个新的值,这个值与第一段运算的结果有关,最后保存起来第二段计算的结果

image

看看这个自写函数:循环遍历每一个字节,然后累乘起来,使用cld扩展指令,溢出32位的部分会保存到edx里,这里会把高32和低32位结果加起来

image

第三段运算

第三段运算使用到了第二段运算计算的结果,对这个结果除以10取余数,用余数作为索引依次从固定字符串里取值,每次取值之后,再对刚刚那个结果除以4求商,并把商保存起来下次循环的时候用,这里的取余数和求商依然是自写函数,功能简单就不展开描述了

image

最后计算出来一个字符串就是真的序列号了

校验

最后就是拿第三段运算计算出来的字符串和用户输入比对,比对一样了就是成功

image

暴力破解

验证流程图大概如下,红线标的是要走的路,在验证逻辑里,对于需要跳转的,就改jmp,对于不需要跳转的就清空改nop,对于这种简单的逻辑这么操作比较无脑可行hhhh

image

注册机编写

#include <iostream>
#include <Windows.h>

DWORD GetVolumeSerialNumber(const char* lpRootPathName) {
    CHAR FileSystemNameBuffer[128]; 
    DWORD FileSystemFlags; 
    DWORD VolumeSerialNumber; 
    CHAR VolumeNameBuffer[128]; 
    GetVolumeInformationA(
        lpRootPathName,
        VolumeNameBuffer,
        0x80u,
        &VolumeSerialNumber,
        (LPDWORD)0xFF,
        &FileSystemFlags,
        FileSystemNameBuffer,
        0x80u);
    return VolumeSerialNumber;
}

DWORD bit_move(DWORD val, int n) {
    DWORD size = sizeof(val) * 8;
    n = n % size;
    return (val >> (size - n) | (val << n));//左移
}

DWORD floatdeal(DWORD a1,DWORD a2) {
    int res = 0;
    __asm {
        fwait;
        fninit;
        fild dword ptr[a1];
        fld st(0);
        fmulp st(1), st(0);
        fild dword ptr[a2];
        fld st(0);
        fmulp st(1), st(0);
        faddp st(1), st(0);
        fsqrt;
        fistp dword ptr[res];

    }
    return res;
    //    return (DWORD)sqrt((float)a1 * (float)a1 + (float)a2 * (float)a2);
}

int main()
{
    DWORD volumeSerialNumber_C = 0;
    DWORD volumeSerialNumber_D = 0;
    DWORD tmp = 0;
    DWORD tmp2 = 1;
    LONGLONG tmp2_1 = 1;
    char name[20] = {0};
    char serial[20] = { 0 };
    const char* arr = "071362de9f8ab45c";

    std::cin >> name;
    if (strlen(name) < 4) return 0;
    // 第一段运算
    volumeSerialNumber_C = GetVolumeSerialNumber("C:\\");
    volumeSerialNumber_D = GetVolumeSerialNumber("D:\\");
    tmp = floatdeal(volumeSerialNumber_C, volumeSerialNumber_D);
    // 第二段运算
    for (int i = 0; name[i]; i++)
    {
        tmp2_1 *= name[i];
        tmp2 = tmp2_1 & 0x00000000FFFFFFFF;
        tmp2 += (tmp2_1 & 0xffffffff00000000) >> 32;
    }

    tmp2 = bit_move(tmp2,1);
    tmp2 |= tmp;
    tmp2 &= 0x0FFFFFFF;
    // 第三段运算
    DWORD i = 0;
    do
    {
        serial[i++] = arr[tmp2 % 0x10];
        tmp2 /= 4;

    } while (tmp2);
    std::cout << "序列号:";
    std::cout << serial << std::endl;
    system("pause");
}


结果

image

总结

这是目前为止分析160个CM里遇到最复杂的校验算法了,分析了好久,这个程序主要有两个难点:

第一个难点在于程序直接启动了窗口过程,所以需要找到验证逻辑出现的地方才能开始下断点,分析程序执行流程即可去跟踪消息号即可

第二个难点在于使用了8个自写函数,要了解验证过程,需要知道自写函数做了什么事情,其中有的函数使用了浮点数运算,这一块不熟悉估计要查一会文档了

做完之后再回头看,嘛,也不过如此

参考资料


评论