首页
论坛
课程
招聘
[分享]记录一次010editor的逆向分析的学习过程
2021-12-4 10:51 16434

[分享]记录一次010editor的逆向分析的学习过程

2021-12-4 10:51
16434

关于010editor的逆向分析

  1. 暴力破解
  2. 浅析注册算法
  3. 通过用户名算出对应的密码位
  4. 写出注册机

1.暴力破解

首先随便输入点内容,点击检查许可,会提示不可用的名字或密码

 

img

 

在od中搜索字符串,CTRL+F查找Invalid name,会搜到一堆提示的字符串

 

img

 

进入invalid name 的位置,在这里发现了一个跳转

 

img过去到跳转的位置,发现一个跳转

 

img

 

保存修改到可执行文件,再次打开,随便输点什么,就可以通过。只是并没有激活产成功,每次打开都要点一下

 

img

2.简单分析算法

通过栈回溯找到上级函数,在反汇编窗口跟随

 

img

 

在这里发现了好几个比较,通过干预的方式跳转到每一个走向,推断出了第一个是检查许可,第二个是退出程序,第三个是打开帮助窗口,对应注册窗口的三个按钮

 

image-20211203184641365

 

在上面打一个断点,再次运行,进入第一个call,发现下面有句请输入一个名字,但是跳过去了,这里应该是判断名字是不是空的

 

img

 

再往下有一个请输入密码,应该是判断有没有输密码

 

img

 

这一段不知道干嘛的但是根据程序走向来看这个地方之后就跳转到了关键的那个比较跳转,这里应该就是关键的函数了

 

img

 

回过头来分析一下爆破时遇到的那几个跳转

 

image-20211203185752264

 

下面这个地方比较了EDI的值,但是它是从上面跳转下来的,再次跟踪过去

 

image-20211203190015389

 

这个地方把EAX的值给了EDI,上面有一个CALL,EAX一般是函数返回值,那么上面的函数应该就是关键的判断,这个位置正好是之前看到的那几个不知道是干嘛的CALL

 

image-20211203190146306

 

在这两个call下断点,重新运行程序。这次输入的密码就从1到a依次输入,目的是为了后面方便看位置

 

image-20211203190712915

 

点击检查许可,在刚刚的断点断了下来这个地方将eax当参数传进了call,查看eax的值,在堆栈窗口跟随一下,发现这个数据像一个地址,在数据窗口跟随,这里正是我们输入的密码。

 

image-20211203191437843

 

进到这个call内部,发现它的内容很少,并没有什么有用的信息,继续运行到下一个call,这个call传进去一个ecx,根据经验,ecx应该是个对象的地址,数据窗口跟随发现了几个地址。

 

image-20211203192052772

 

分别跟随这几个地址,分别发现了我们输入的用户名以及密码。那么基本就可以确定是这个call验证了账号和密码了。我们进入到这个call的内部具体的分析一下

 

image-20211203192204415

 

image-20211203192252858

 

根据名字来看下面这两个call应该是做了非空判断

 

image-20211203192619007

 

接着,根据传入的参数,我们进行数据跟随。下面两个call分别访问了密码和用户名的位置,再下面有一个字符串操作的call。不过不是我们重点分析的对向

 

image-20211203192943736

 

再往下,就到了重要的位置

 

image-20211203193339298

 

我们看一下ebp-21的位置是什么。在数据窗口按ctrl+G输入ebp-21,跳转到这个位置,我们发现正好是我们的密码。它把44放进了BL里面。那么应该是按字节来划分的。我们把我们的密码按照k[0]-k[9]来划分,这个位置对应的是k[3].

 

image-20211203193836342

 

接下来按照相同的方法找到操作了哪些数字。

 

image-20211203194420894

 

这个地方比较了k[3]是否为0x9c,在下面也有AC和FC,这个地方可能是在 比较许可证的类型,期限版或者是永久版,这里以0x9c这个版本为主要分析的版本,这里需要手动把密码的k3位置改为9c或者干预程序流程让他可以接着往下走,我们就可以接着往下分析。

 

image-20211203194654819

 

推算过程不是很难,但是需要一步一步地细心的观察。下面有两个call-

 

image-20211203194842394

 

第一个call的参数在上面的push,进去后发现它是对参数进行了一些操作。

 

image-20211203195452858

 

image-20211203195526410

 

第二个call和上面的功能类似

 

image-20211203202439300

 

下面有几个比较来判断跳转,根据我们推导出来的公式,结合判断条件来写一个代码验证.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include<windows.h>
#include<time.h>
 
int main() {
    //创建随机数
    srand(time(NULL));
    byte k[10] = { 0 };
 
    //我们要随机出来一些数来运算,从而找出符合判断条件的数值
    while (true)
    {
        //k0和k6是第一个call的结果
        byte k0 = rand() % 0xFF;
        byte k6 = rand() % 0xFF;
        byte al = (k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7;
        if (al>0)
        {
            k[0] = k0;
            k[6] = k6;
            break;
        }
    }
    //
    while (true)
    {
        //k1,k7,k2,k5是第二个call的运算
        byte k1 = rand() % 0xFF;
        byte k7 = rand() % 0xFF;
        byte k2 = rand() % 0xFF;
        byte k5 = rand() % 0xFF;
 
        DWORD ESI = (0x100 * (k1 ^ k7 & 0xFF) + k2 ^ k5 & 0xFF) & 0xFFFF;
        DWORD EAX = (((ESI ^ 0x7892) + 0x4d30) ^ 0x3421) & 0xFFFF;
 
        if (EAX%0XB==0&&EAX/0XB<=0X3EB)
        {
            k[1] = k1;
            k[7] = k7;
            k[5] = k5;
            k[2] = k2;
            break;
        }
    }
    //k3必须是9c
    k[3] = 0x9c;
    printf("%02X%02X-%02X%02X-%02X%02X-%02X%02X",k[0],k[1], k[2], k[3], k[4], k[5], k[6], k[7], k[8], k[9]);
    system("pause");
}

运行我们写的程序,得到一个字符串E416-A69C-0081-14B4。输入进密码框,运行到比较的位置,就会顺利经过

 

image-20211203203717852

 

我们成功的进入到了下一个位置,我们接着分析,在第一个call,将一个局部变量的地址当参数,然后调了一个call,这里只是将用户名的字符串转为acill的字符串

 

image-20211203204251417

 

接着将字符串以及两个参数传进一个 call,结果返回的还是用户名。接着下一个call将用户名传进去,并把返回值给了edx

 

image-20211203205231130

 

下面将dl也就是edx最低位的字节和ebp-20的这个数据比较,我们在数据窗口发现ebp-20这个地方放的是我们密码的k[4]-k[7]的数据,接着将edx的数据给ecx,将ecx向右移动8位,也就是一个字节。取cl,就是原来edx'的第三个字节,以此类推把kK[4]-k[7]的值和edx相比较。也就是上面函数的返回值。

 

image-20211203210328669

 

那么函数的功能就是传入一个用户名,返回四个字节的数据,并和密码的kK[4]-k[7]进行比较。如果不一致就会验证失败,函数最后将0x2D返回

 

image-20211203211335579

3.用户名对应密码的转换

我们看看这个关键函数如何将用户名转换成密码的,进去这个call一探究竟,网上关于010editor的分析也有很多,但这里都是直接去IDA把汇编转换成C语言代码,我也看了下,ida翻译出来的代码确实看不明白,这里我们直接自己分析一下这个算法的流程。

 

进去这个call

 

image-20211205120726697

 

首先这个位置定义了局部变量,一共16个字节大小,然后把用户名地址给了edx

 

image-20211205121131987

 

然后又从edx给到esi,通过比较al是不是零,让esi自增,循环完用esi-edi得到的就是循环的次数也就是字符串的长度,

 

image-20211205121414368

 

下面有两个地方是比较关键的地方。第一个是一个库函数,C语言的库函数,将小写字母转化为大写字母。第二个是一个比较跳转。判断传进来的参数然后跳转

 

image-20211205122625969

 

再往下就是各种运算,里面的运算涉及一个数组,因为太大,我们还是从od复制出快

 

image-20211203215614118

 

最后的位置是向上跳转,很明显这个是一个循环,根据上面我们判断过的,那么这个循环是为了将每一位用户名进行运行算,循环次数就是用户名的长度。

 

image-20211205122943530

 

那么,基本的逻辑结构我们已经清楚了。下面就是将这部分代码写进注册机里了,值得注意的一点是汇编中的eax*4实际上在我们用C语言数组的时候,就没必要乘以4了,因为汇编中是以地址加字节数来取值,4个字节才对应一个数,所以会乘以4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <stdio.h>
#include<windows.h>
#include<time.h>
 
//数组过长,节省空间就不放进来了。
DWORD arr[] = {......};
//通过用户名得到对应的密码位
DWORD getResult(const char* name, int n1, int n2, DWORD val);
int main() {
 
    //创建随机数
    srand(time(NULL));
    byte k[10] = { 0 };
 
    //设置一个用户名
    char userName[] = { "van1" };
    //生成用户名关联密码
    DWORD result = getResult(userName, 1, 0, 0x3e8);
    //设置k4-k7的值
    k[4] = result <<24>>24;
    k[5] = result <<16>>24;
    k[6] = result <<8>>24;
    k[7] = result >>24;
 
    //我们要随机出来一些数来运算,从而找出符合判断条件的数值
    while (true)
    {
        //k0和k6是第一个call的结果
        byte k0 = rand() % 0xFF;
        byte k6 = k[6];
        byte al = (k0 ^ k6 ^ 0x18 + 0x3D) ^ 0xA7;
        if (al>10)
        {
            k[0] = k0;
            break;
        }
    }
    //
    while (true)
    {
        //k1,k7,k2,k5是第二个call的运算
        byte k1 = rand() % 0xFF;
        byte k7 = k[7];
        byte k2 = rand() % 0xFF;
        byte k5 = k[5];
 
        DWORD ESI = (0x100 * (k1 ^ k7 & 0xFF) + k2 ^ k5 & 0xFF) & 0xFFFF;
        DWORD EAX = (((ESI ^ 0x7892) + 0x4d30) ^ 0x3421) & 0xFFFF;
 
        if (EAX%0XB==0&&EAX/0XB==0X3E8)
        {
            k[1] = k1;
            k[2] = k2;
            break;
        }
    }
    //k3必须是9c
    k[3] = 0x9c;
    printf("%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X",k[0],k[1], k[2], k[3], k[4], k[5], k[6], k[7], k[8], k[9]);
    system("pause");
}
 
//从用户名计算出密码对应位
DWORD getResult(const char* name, int n1, int n2, DWORD val) {
    //开始位置栈顶上移了16个字节
    DWORD loc1, loc2, loc3, loc4;
    loc1 = loc2 = loc3 = loc4 = 0;
    DWORD edx;
    DWORD ecx;
    DWORD eax;
    loc1 = 0;
    //求长度
    int len = strlen(name);
    DWORD ebx = val;
    loc4 = loc3 = 0;
    ecx = n2;
    ebx = val;
    ebx = ebx << 4;
    ebx -= val;
    ecx = ecx << 4;
    ecx += n2;
 
    for (int i = 0; i < len; i++) {
        edx = toupper(name[i]);
        ecx = arr[edx] + loc1;
        if (n1 != 0) {
            eax = (edx + 0xd) & 0xff;
            ecx = ecx ^ arr[eax ];
            eax = edx + 0x2f;
            eax = eax & 0xff;
            ecx = ecx * arr[eax ];
            eax = loc2 & 0xff;
            ecx = ecx + arr[eax ];
            eax = ebx & 0xff;
            ecx = ecx + arr[eax ];
            eax = loc3;
            eax = eax & 0xff;
            ecx = ecx + arr[eax ];
            eax = ecx;
            loc1 = eax;
        }
        else
        {
            eax = edx + 0x3f;
            eax = eax & 0xff;
            ecx = ecx ^ arr[eax];
            eax = edx + 0x17;
            eax = eax & 0xff;
            ecx = eax * arr[eax];
            eax = loc2 & 0xff;
            ecx = ecx + arr[eax];
            eax = ebx & 0xff;
            ecx = ecx + arr[eax];
            eax = loc4;
            eax = eax & 0xff;
            ecx = ecx + arr[eax];
            eax = ecx;
        }
        loc3 += 0x13;
        loc2 += 9;
        ebx += 0xd;
        loc4 += 7;
 
    }
    return eax;
}

运行程序获得密码,输入我们生成的密码。再次运行程序

 

image-20211204090922583

 

我们的密码可以正确的通过前面的验证,但是会弹出网络验证

 

image-20211204091439454

4.去除网络验证

重新运行程序,输入验证码。在原来的验证密码的函数之后还有一个类似的函数,先步过看一下返回值,发现它把返回值修改为了113,正确的返回值应该是DB

 

image-20211204100217182

 

我们进到函数里面一探究竟。发现这个地方有个比较,相同就会跳转,不相同就会返回113.我们先让他强制跳转过去。

 

image-20211204101504206

 

然后就会一路运行到返回正确的返回值的地方,这里吧DB给eax并且返回

 

image-20211204100556676

 

出了这个函数,继续往下走,发现还有一个和之前一样的比较,如果一样就跳到后面最后的验证,不一样回去执行下面的call,在这里会程序会卡住好几秒,应该是在链接服务器获取信息。我们直接修改掉这个跳转,让它无条件跳转

 

image-20211204101602968

 

image-20211204101158130

 

弹出许可证接受界面,成功!!!

总结

​ 觉得去除网络验证也就是相当于一个爆破的过程。为什么我还要写注册机呢?写完注册机到最后还是要爆破掉。但是,写注册机与爆破网络验证的目的并不是为了破解掉这个软件,而是去分析这个软件运行的一个过程,理解它的思路。学习分析一个程序的思路和常用手段。并不是单纯的为了破解而去破解的。我们要的是一个学习的过程。

 

​ 思路还是前辈们的思路,感谢各位为安全行业的付出。这里只是记录分享一下学习的过程。分析的过程中还是有一些不太好去理解的东西。因为时间原因,在通过用户名来求证密码的后四位的时候也是没有自己去分析。对汇编的掌握还是有很多陌生的地方。也很有可能因为不知道这个指令的作用而错过某些关键点。

 

​ 路还很长,我们一起走下去。

特别致谢

15PB薛老师的思路


【看雪培训】目录重大更新!《安卓高级研修班》2022年春季班开始招生!

最后于 2021-12-5 12:49 被wx_Van_Zovy编辑 ,原因: 更新了用户名对应为密码的算法解析
收藏
点赞2
打赏
分享
最新回复 (2)
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_lptgynwu 活跃值 2021-12-4 12:42
2
0
010editor好惨,被无数人蹂躏。。。
雪    币: 613
活跃值: 活跃值 (559)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
wx_Van_Zovy 活跃值 2021-12-5 12:50
3
0
mb_lptgynwu 010editor好惨,被无数人蹂躏。。。
哈哈,学习对象。用来学习的
游客
登录 | 注册 方可回帖
返回