首页
社区
课程
招聘
[讨论]关于010Editor的分析
2021-12-2 18:39 13988

[讨论]关于010Editor的分析

2021-12-2 18:39
13988

信息收集

1、常规扫描收集信息

 

image-20211130143455109

 

image-20211130143528481

 

image-20211130143623852

 

2、资源查看

 

image-20211130143756407

 

3、spy++查看其窗口回调函数地址

 

image-20211130145447740

 

4、OD转到地址分析函数

  1. 假定参数并设置消息断点

image-20211130145704008

  1. 点击按钮触发断点(To Be Continued…)

image-20211130145819123

  1. 转到创建窗口这里尝试 (To Be Continued…)

image-20211130190410355

 

4、查看调用 (To Be Continued…)

 

​ 发现来自主模块的调用很多,这里只能一个一个尝试,看哪里有有用的信息。

 

image-20211130190735345

 

5、在尝试输入用户名和密码失败后,会弹出一个对话框告诉你密码错误的提示语

 

​ 其实这个字符串也是个很好的切入点,我们可以在IDA和OD中寻找有关此字符串的信息。

 

image-20211130210858513

 

6、OD和IDA寻找关键函数

  1. IDA查找字符串

image-20211130211124067 双击它进入到.rdata段发现函数本地,然后Ctrl+x查看交叉引用发现只有一个地方调用了它,很好我们跟进查看

 

image-20211130211343354

 

image-20211130211546700然后IDA最经典的流程图来了,有了它能对我们分析起到帮助。但是一眼看上去好复杂,但是不用怕,不需要每一个框都分析,只用顺着箭头向上找,找到是从哪里跳转过来的就能找到切入点。

 

cmp ebx,93h =====>这里不能相等

 

jnz xxxxx

 

image-20211130211822058

 

cmp edi,20h=====>这里也不能等

 

jz xxxxxxx

 

image-20211130212011162

 

cmp edi,0EDh=====>还是不能等

 

jz xxxxxxxx

 

image-20211130212102673

 

cmp edi,0DBh=====>继续不等

 

jnz xxxxxxx

 

image-20211130212153118

  • [x] 暂时分析了这么多,明天继续

暴力破解

找到关键函数和跳转

今天继续,从昨天找到的比较edi和0xDB这里继续查看,我们打开OD,定位到这个验证的地方看,但是怎么定位到这里呢,我们可以先搜一下字符串,找到Invalid name。。。那一段,然后双击到代码段。然后你就能发现这儿有个跳转,我们可以过去看看能不能找到上一跳

 

image-20211203095326575

 

image-20211203095507979

 

当然也可以试试下面这个比较不相等的时候会跳到哪里去,我们跟过去看看。

 

image-20211203095739795

 

相似的,我把能找到提示的几条跳转的结果都找了一下

 

image-20211203100312748

 

然后敲黑板了,重点来了

 

image-20211203100532960

 

我们找到它的上一跳发现如果这个标胶不相等就会跳到错误的区间,否则就正确了

 

image-20211203100638241

 

继续找上一跳

 

image-20211203101109377

 

image-20211203101159166

 

image-20211203101508292

 

image-20211203101920735

 

image-20211203102613725

 

然后就可以在这个函数上打个断点观察一下参数和返回值了

 

刚开是可以先去看看这个 函数的返回值是怎样的,先不具体去分析,就粗略的看一下发现有三种。

 

image-20211203103802789

 

最上面的返回值是ESI,但是ESI又是啥呢,别急,我们往上翻一下

 

image-20211203103929479

 

发现也有几种情况这个我们先放着吧,看看下面那个函数有什么说法。

 

image-20211203104957387

 

image-20211203105136583

 

跟着跳下来发现分别对应着三种返回这,我们要的就是DB,所以传进来的EAX应该要是2D

 

image-20211203105251699

 

于是现在有一个结论:上面这个函数的返回值应该是2D才能得到正确的结果。

 

image-20211203105603940

NOP掉关键跳转

所以最简单粗暴的破解方法来了,就是把关键跳转那个地方给破坏掉,它无法验证DB那个地方了也就不起作用了。

 

image-20211203110208740

 

image-20211203112329674

 

这个爆破有点low,重新打开程序还是会弹出Register窗口,我们要点击一下Check License按钮还会弹出一个接受密码的对话框,然后点击OK才能打开软件,太麻烦了。因为我们刚刚只是把最外层的关键跳转给改了,里面的密码验证环节特别是关键验证没有变动,所以很麻烦。接下来尝试一下更方便的破解。

直接给定关键值

再次定位到关键函数,这里这里:

 

image-20211203114717456

 

image-20211203140249279

 

估计很多朋友都忘了随即地址在哪改了吧,我们复习一下PE文件的内容,先找到扩展头(OptionHeader)

 

image-20211203140735637

 

然后找到DLL_Characteristics字段里的Dynamic_base,1代表启用,改成0就把他禁止了。

 

image-20211203140815933

 

改好了之后再次打开OD回到刚刚定位的地方把前两条语句修改掉,如图

 

image-20211203141439943

 

然后保存到文件

 

image-20211203144937460

 

image-20211203144955514

 

然后重新打开,就成了!!!

 

image-20211203145015999

注册机的编写探索

分析算法==>关键函数

分析参数

还得先在OD里定位到两个重要函数的地址。

 

image-20211203150900172

 

先看看这个函数的参数,一个ECX和两个立即数,这看起来就是C++的thiscall了,ECX是this指针,这样我们先在数据窗口看一下这个指针指向的数据是啥。

 

image-20211203151450763

 

这是this指针下的第一个偏移是我们输入的用户名

 

image-20211203151858715

 

第二个偏移是我们输入的密码

 

image-20211203152057631

 

第三个偏移

 

image-20211203152149066

进入函数内部分析

真正的的有用的语句在这些下面

 

image-20211203154102308

 

我们看到下面有个OD分析出来的函数,应该是判断是否为空的,为空返回值就是1,否则就是0,为空就直接JNZ跳转走了

 

image-20211203154522961

 

image-20211203154640052

 

继续往下走,第二个遇到的函数就很好说了,看参数把第二个成员给了ecx然后再次检查是否为空,显然就是检查我们输入的序列号了。

 

image-20211203154826723

 

在每次检查完是否为空之后就会有个比较和跳转,如果为空直接就跳走了,那么正确的执行流程应该是直接往下走的,跟进代码看看后面会不会对用户名和序列号进行访问或者修改的操作。

 

image-20211203155641942

 

call完之后发现局部变量果然被修改了,可以和上图对比着看一下

 

image-20211203155912007

 

可见这里应该是把我们输入的密码改成了一个16进制的字节数据

 

image-20211203160135625

 

接下来再看这个ESi里面保存的全局变量是什么值,观察所知是一个999字符串,不知道干嘛的,继续往下分析

 

image-20211203160439492

 

两个参数,一个999字符串的地址,一个用户名的地址

 

image-20211203161146821

 

无事发生,在用户名、序列号、“999”这三个地址的值都未发生改变,推测是创建了一个QString字符串,暂时没什么影响,继续往下走。

 

image-20211203161523237

 

是0到F的ASCII码

 

image-20211203161548259

 

发现这里是个循环

 

image-20211203161941694

 

继续往下看,发现了一个局部变量给BL赋值,跟随数据发现就是我们写入的序列号,它正在取低字节,如果将12这个地址看作数组首地址的话,那么78这里就是偏移了三个单位。

 

这里我们可以假设此时BL存放的是k[3]

 

image-20211203162245702

 

同理,下面BH存放的就是k[5]。

 

友情提示,即将进入最关键的序列号计算环节了,请打起精神啊~

序列号关键算法分析

第一次比较:把BL和0x9C进行对比,不相等的话跳转

 

image-20211203164855498

 

然后跟0xFC对比,不相等则跳转

 

image-20211203165023203

 

然后跟0xAC对比,不相等则跳转

 

image-20211203165102659

 

再然后就进入default处理然后直接结束了

 

image-20211203165146440

 

所以拿小本本记下来,k[3]可能的值有三个0x9C、0xFC、0xAC

分支一:当k[3]等于0x9C的时候
 

每一步给AL赋值的语句我们都要数据跟随一下,查看当前局部变量的值是什么,然后转换成序列号数组看看是怎么算的。

 

image-20211203171158761

 

image-20211203171259240

 

image-20211203181611918

 

image-20211203181811345

 

image-20211203182032472

 

我觉得有必要解释一下这一步的过程,首先查看MOVZX的作用

 

image-20211203182615877

 

image-20211203182925368

 

打个比方,假设AL此时的值是0x00AC,那么经过了MOVZX的操作之后,ECX变成了0x0000 00AC。但是把它转换成C语言怎么办呢?就有了按位与上一个0xFF的操作,可以将高位清0。接下来还是一些赋值的操作,看图

 

image-20211203183316160

 

image-20211203184100400

 

image-20211203184124212

 

回头去看一下这个函数有几个参数?只有一个PUSH

 

image-20211203184314635

 

其实这个函数就是把传进来的值进行了一系列计算而已,那么可以小记一下这个call就是在处理k[0]^k[6]的数据

 

image-20211203184551269

 

下面也有一个call,参数是很长的一串

 

image-20211203185938962

 

然后进去发现这个函数过程也不复杂可以分析一下

 

image-20211203201340011

 

接着就是下面的分析了,这个没啥技巧(当然可能是我技艺不精,有更好的方法欢迎留言~)

 

image-20211203192054983

 

到这里我们可以写一下代码来验证一下到目前为止所分析的内容。先说一下思路哈:

 

首先我们生成一下两个随机数,如果在一个字节所能表示的数范围内满足公式:

 

( ( k[0]\^ k[6]\^0x18 + 0x3D)\^0xA7) & 0xFF > 0

 

那么就将这两个数填充到数组下标为0和6两个元素处。同理如果满足公式

  • ESI = (0x100 * (k[1] ^ k[7] & 0xFF) + ((k[5] ^ k[2]) & 0xFF)) & 0xFFFF
  • EAX = ((ESI) ^ 0x7892 + 0x4D30) ^ 0x3421 & 0xFFFF) / 0xB
  • 判断余数是否为0 ,为0返回商,否则返回0(这里余数要为0且商小于0x3E8)

那么就将这两个随机数填充到数组下标1、2、5、7处

 

生成了一个试试看,能不能绕过那几个跳转

 

image-20211203200138104

 

image-20211203200333201

 

image-20211203200506227

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
// 010EditorCracker.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
 
#include <iostream>
#include <Windows.h>
#include <time.h>
 
int main() {
    // 还原一下我们输进去的序列号 这里我们进入的分支是k[3] == 0x9C
    BYTE byCode[10] = { 0x12,0x34,0x56,0x9C,0x12,0x34,0x56,0x78,0x12,0x34 };
    while (true) {
        // 随机生成两个1字节的数
        BYTE tempCode0 = rand() % 0xFF;
        BYTE tempCode6 = rand() % 0xFF;
        // 最先开始处理的数据 AL = (k[0] ^ k[6] ^ 0x18 + 0x3D) ^ 0xA7
        // 我们也构造一个变量出来让它对应上数组下标为06的元素关系
        BYTE AL = (tempCode0 ^ tempCode0 ^ 0x18 + 0x3D) ^ 0xA7;
        if (AL > 0) {
            byCode[0] = tempCode0;
            byCode[6] = tempCode6;
            break;
        }
    }
 
    // ESI = (0x100 * (k[1] ^ k[7] & 0xFF) + ((k[5] ^ k[2]) & 0xFF)) & 0xFFFF
    // EAX = ((ESI) ^ 0x7892 + 0x4D30) ^ 0x3421 & 0xFFFF / 0xB
    // 判断余数是否为0 ,为0返回商,否则返回0
    // 这里余数要为0
    // 商还要小于0x3E8
    while (true) {
        BYTE tempCode1 = rand() % 0xFF;
        BYTE tempCode2 = rand() % 0xFF;
        BYTE tempCode5 = rand() % 0xFF;
        BYTE tempCode7 = rand() % 0xFF;
 
        DWORD ESI = (0x100 * (tempCode1 ^ tempCode7 & 0xFF) + tempCode5 ^ tempCode2 & 0xFF) & 0xFFFF;
        DWORD EAX = (((ESI ^ 0x7892) + 0x4D30) ^ 0x3421) & 0xFFFF;
        if (EAX % 0xB == 0 && EAX / 0xB <= 0x3E8) {
            byCode[1] = tempCode1;
            byCode[2] = tempCode2;
            byCode[5] = tempCode5;
            byCode[7] = tempCode7;
            break;
        }
    }
 
    // 最后输出一下这个数组元素
    for (BYTE arr : byCode) {
        printf("%02X ", arr);
    }
    return 0;
}
  • [x] 今日至此,明日继续更新~

昨天只是根据假定k[3]的值为0x9C,然后根据这个大前提来通过生成随机数来穷举符合k[0]和k[6],k[1]、k[2]、k[5]、k[7]之间关系的字符序列,从而能够通过一些列验证。

 

今天我们继续完成关键算法的分析并能实现生成能用的用户名和序列号!

 

image-20211204084728367

 

接着往下走,发现有一个局部变量传递给了寄存器并且入栈了,下面就是一个字符串转换的函数,那么就可以认为这个函数的传入和传出参数是同一个,我们可以观察一下这个寄存器的值的变化。而且紧跟着ECX保存的是之前一直没有出现的用户名的地址,这个函数就是将用户名转换一下吗?我们看一下:

 

image-20211204085523325

 

image-20211204085606805

 

image-20211204085700708

 

就是我们输入的用户名,可是第一个字符出现的地方和0x559B878多了个0x10啊,而且前面多的这些数字是干什么用的?或者说有作用吗?我们慢慢分析。只知道现在我们的用户名在这里被改成了ASCII字符串。

 

image-20211204085809795

 

image-20211204090235303

 

image-20211204090259369

 

image-20211204090501757

 

可见这个[EDI+0x20]原封不动的传递了下来,现在要将它入栈准备给下一个call调用了,接着看下面的语句

 

image-20211204090858588

 

image-20211204091119931

 

然后就碰到了一个没见过的指令,SETNE是干啥的?别慌我们查查:

 

image-20211204091232497

 

还是不太懂,算了我们看寄存器的变化吧

 

image-20211204091441283

 

看起来好像是把EAX从0变成了1

 

image-20211204091531981

 

还是不懂这条指令是干什么的,百度,百度,百度!!!重要的事情说三遍:

 

image-20211204093001095

 

看起来似乎是将ZF的值经过什么判断后赋给寄存器,我们写个小demo调试一下看看吧:

 

先是看看在ZF为1的情况下,SETcc咋用:关注EAX的变化!!!

 

image-20211204093324827

 

image-20211204093524119

 

image-20211204093618497

 

image-20211204093756598

 

image-20211204093901481

 

image-20211204093950896

 

总结一下:

  • ZF为1时,SETE让寄存器为1,SETNE让寄存器为0
  • ZF为0时,SETE让寄存器为0,SETNE让寄存器为1
  • 总之SETE就是让寄存器和ZF保存一致,SETNE则相反

回到我们的分析流上来:

 

image-20211204094635631

 

image-20211204094744865

 

然后紧接着就把用户名字符串给入栈然后调用下一个call,莫非这个call对用户名做了什么处理?老样子我们先不进去看它的处理过程,我们观察返回值,也就是EAX是什么:

 

可是这不是一个地址并不知道这是个啥,那没事了,我们继续往下走吧,先标注一下这个函数是对用户名做了什么处理就行。

 

image-20211204095257976

 

接下来把EDX低4个字节和[EBP-0x20]进行比较,我们可以看到数据窗口中显示这个局部变量的值此时是12

 

image-20211204100512287

 

也就是序列号下表为4的那个元素,小记一下:CMP k[4],DL

 

image-20211204100650234

 

BH是序列号下标为5的那个元素,也就是CMP k[5],CL

 

image-20211204101217768

 

同样的套路,下面几个比较都是把密码不同地方和那个函数返回值的不同字节进行比较,相同则往下继续比,不相同则跳走:

 

image-20211204110341551

 

总结一下对应关系:

函数返回值 高字节 次高字节 次低字节 低字节
序列号对应元素 k[7] k[6] k[5] k[4]
 

也就是说k4~k7拼出了计算用户名的运算结果

 

继续往下看,

 

image-20211204104141303

 

image-20211204110408424

 

image-20211204104358848

 

image-20211204104611496

 

image-20211204110435928

 

还记得我们前面写的验证代码吗,函数的返回值是>0的,这里进一步缩小范围了,应该是大于或等于9,我们去把它改了:

 

image-20211204105814810

 

然后分析到这里可以发现其实用户名和序列号之间只有一个对应关系的,k4、k5、k6、k7是用户名的ASCII数组经过计算得出来的一串数据,所以在编写注册机的时候要想办法把这个对应关系给用上。

 

所以我们也要还原一下计算用户名的那个函数,然后分别给序列号的后四个元素赋值就行。(当然还要满足前面的条件)

 

我们来到计算用户名这个函数

 

image-20211204111138959

 

然后去IDA找一下,然后F5,然后COPY一下不就好了吗?

 

我觉得还行~

 

image-20211204112132916

 

不过还有个问题哈,就是这里频繁使用到了一个数组dword_2E64148,我们过去把它也拷贝一份:

 

image-20211204112440023

 

image-20211204112719394

 

当然最关键的算法可不能落下啊:

 

image-20211204112808131

 

image-20211204112925594

 

要想使用这个函数,光有名字可不行啊,还得知道有什么参数,这里我们去OD再看看PUSH了啥

 

image-20211204113214310

 

找一下这个ESI是什么

 

image-20211204113455199

 

image-20211204113517635

 

找到了,所以这个参数是0,然后后面的EAX的值是1,最后一个参数是这一长串的商

 

image-20211204113832186

 

在前面验证的过程中,我们是怎么得到这个商的呢,通过生成随机数来穷举符合公式的小于等于0x3EB的值,如图

 

image-20211204114007620

 

有个小细节哈,在IDA的翻译成C的过程中,将这个函数的第四个参数变成了char型,但是它是一个商,应该是一个数字才对

 

image-20211204114610807

 

而它这里是把结果AX(2字节)扩展成的EAX(4字节)

 

image-20211204114723463

 

这里我们不妨把第四个参数大胆改成2字节的变量

 

image-20211204115202524

 

然后就可以去编码调用一下这个函数了

 

image-20211204120454243

 

然后因为6号元素在这里已经有了确定的值,下面就不需要再去穷举算它的值了。

 

image-20211204120702846

 

还有下面的5号元素、7号元素都不用再枚举了,直接由上面计算出来就行

 

image-20211204120812081

 

image-20211204121005640

 

image-20211204121353598

 

image-20211204122232449

网络验证过程的分析

我们跟踪一下EAX的值的变化,到这里EAX都还是0x2D,是我们想要的值

 

image-20211204123431289

 

可是经过了call之后发现EAX突变了,变成了0x113,说明这个call里面对EAX的值进行了修改或验证

 

image-20211204123555524

 

所以我们这里要跟进这个函数看一下里面做了什么操作:

 

image-20211204124152686

 

image-20211204124313850

 

我们继续往下步过看看能不能通过验证:

 

image-20211204125015431

 

image-20211204124557649

 

很可惜还是不能,继续往下分析吧:

 

image-20211204125132607

 

image-20211204125240775

 

image-20211204125332185

 

所以这个地方有两个思路:

  1. 将验证下的跳转换成jmp直接跳过来,不给验证

image-20211204130100278

 

image-20211204130206664

 

然后下面一条语句是测试EAX的值

 

image-20211204130332074

 

image-20211204130415288

 

如果无符号就跳走

 

image-20211204130537466

 

所以我们可以修改一下这个函数的返回值,这里我们先手动修改一下EAX位1试试,看能不能跳走

 

image-20211204130825804

 

image-20211204130943739

 

image-20211204131123331

 

image-20211204131209301

 

小记一下要想过掉网络验证有两个关键:

  1. 第一个验证环节要直接jmp过去

image-20211204131445652

 

image-20211204131516051

  1. 网络验证的返回值要是1(不能带符号)

image-20211204131615713

 

image-20211204131757999

 

还没完,得满足堆栈平衡,我们往下翻,看看它的ret是多少:

 

拖了半天才找到,网络验证真复杂~

 

image-20211204131852786

 

image-20211204131944995

 

这样就应该没毛病了,保存修改到文件:

 

image-20211204132133123

 

我这里重新算一组新的试试看:

 

image-20211204132417736

 

image-20211204132518950

 

然后就能愉快的玩耍了。

总结一下

算法小结:

  1. 首先它会检测用户名和序列号输入是否为空
  2. 将输入的序列号字符串转变为16进制的字节数组
  3. 验证序列号(byteCode)满不满足一下条件
  • byteCode[3] == 0x9C、0xFC、0xAC ===> 这里我是选取了0x9C这个分支开始分析的
  • 00407644这个函数的返回值也就是((tempCode0 ^ tempCode0 ^ 0x18 + 0x3D) ^ 0xA7)不能为0
  • 004083C8函数的返回值处于0~0x3E8这个区间,也就是
1
2
3
4
ESI = (0x100 * (k[1] ^ k[7] & 0xFF) + ((k[5] ^ k[2]) & 0xFF)) & 0xFFFF
EAX = ((ESI) ^ 0x7892 + 0x4D30) ^ 0x3421 & 0xFFFF / 0xB
EAX % 0xB == 0 && EAX / 0xB <= 0x3E8
这里为了计算方便,我采取了将商定为0x3E8
  1. 然后就是将用户名转化成ascii字符串

  2. 通过一些列计算将用户名计算出一串字符,分别对应byteCode[4~7]这四个字节

    1. 调用了402E50这个函数,有四个参数,分别是用户名字符串、1、0、4083C8函数的返回值

      它的返回值是4字节的一串字符UNEncode

    2. 这一串字符与序列号数组有着对应关系:

      byteCode[4] = UNEncode & 0xFF

      byteCode[5] = UNEncode >> 8 & 0xFF

      byteCode[6] = UNEncode >> 16 & 0xFF

      byteCode[7] = UNEncode >> 24 & 0xFF

  3. 判断函数00407644的返回值是否大于等于9

注册机编写小结:

  1. 一破版:爆破,绕过所有验证
  2. 二破版:穷举出符合一定条件的随机字节能通过部分验证
  3. 三破版:指定用户名,通过用户名计算出来序列号后四个字节,然后再穷举出满足条件的前四个字节
  4. 四破板:绕过网络验证环节,直接通过用户名计算出序列号就可以使用了

[ ] 当然这个软件的分析细节我还远远没有分析完,这是我一路分析下来的思路,可能有点乱,但是已经达到了我的目的,完成了破解,接下来就是注册机的编写。欢迎各路大神带着新鲜的思路前来指点。稍后奉上注册机和源码。

分支:关于用户名的处理过程分析

还是从传入的四个参数说起:[ARG.1]->用户名[ARG.2]->1[ARG.3]->0[ARG.4]->0x3E8

 

image-20211204162644211

 

image-20211204162752960

 

image-20211204163031533

 

image-20211204163058761

 

这个循环取出用户名每一个字节,直到0截断停止,猜测可能会有个操作时拿到长度,不然这个循环就没有意义

 

image-20211204163445163

 

果然在后面就有个检测

 

image-20211204164434719

 

image-20211204165256076

 

下面紧跟又一个循环

 

image-20211204165731474

 

这里有一个比较和跳转,我们跟下去看一下这个跳转是干啥的

 

image-20211205123449572

 

image-20211205123546909

分支一:当[ARG.2]是1的时候

这里有一些列的操作,我们留意一下这个跳转:

 

image-20211205123758227

 

注意,这里有个小细节

 

image-20211205130514510

 

然后我们往下走,分析一轮看看对这个元素做了什么操作

 

image-20211205131835972

 

image-20211205132020585

 

注意:第一轮的数据保存在EAX和ECX中

 

image-20211205132105350

 

我们继续跟着跳,但是到这一句的时候我就产生了不好的预感:这里我们上一轮计算的数据已经被新的元素ASCII码覆盖掉了,也就是说之前算的东西还真没有用?

 

image-20211205132340295

 

但是突然又想到部队的地方,除了这里是计算的数据之外,还有一个地方我们刚刚忽略掉了,就是这里:

 

image-20211205132626158

 

分析到这里,我们就能编码了,把这套逻辑搬运到我们的注册机里,就能完成用户名和对应序列号下标的值的运算了:
image-20211205135803396

 

这里有个大坑!!!

 

这里的寄存器乘以4的操作是因为再汇编代码里,所有的数据都是以字节为单位的,所以BYTE长度乘以4就是4字节的DWORD长度,我们在用高级语言编码的时候就不用乘以这个4了

 

image-20211205164040439

 

image-20211205164428234

 

这里附上代码~

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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// 010Editor注册机完整版控制台版.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
 
#include <iostream>
#include <time.h>
#include <windows.h>
#include <tchar.h>
 
// 密码表
DWORD codeArr[] = {
0x39cb44b8, 0x23754f67, 0x5f017211, 0x3ebb24da, 0x351707c6, 0x63f9774b, 0x17827288, 0x0fe74821, 0x5b5f670f, 0x48315ae8, 0x785b7769, 0x2b7a1547, 0x38d11292, 0x42a11b32, 0x35332244, 0x77437b60,
0x1eab3b10, 0x53810000, 0x1d0212ae, 0x6f0377a8, 0x43c03092, 0x2d3c0a8e, 0x62950cbf, 0x30f06ffa, 0x34f710e0, 0x28f417fb, 0x350d2f95, 0x5a361d5a, 0x15cc060b, 0x0afd13cc, 0x28603bcf, 0x3371066b,
0x30cd14e4, 0x175d3a67, 0x6dd66a13, 0x2d3409f9, 0x581e7b82, 0x76526b99, 0x5c8d5188, 0x2c857971, 0x15f51fc0, 0x68cc0d11, 0x49f55e5c, 0x275e4364, 0x2d1e0dbc, 0x4cee7ce3, 0x32555840, 0x112e2e08,
0x6978065a, 0x72921406, 0x314578e7, 0x175621b7, 0x40771dbf, 0x3fc238d6, 0x4a31128a, 0x2dad036e, 0x41a069d6, 0x25400192, 0x00dd4667, 0x6afc1f4f, 0x571040ce, 0x62fe66df, 0x41db4b3e, 0x3582231f,
0x55f6079a, 0x1ca70644, 0x1b1643d2, 0x3f7228c9, 0x5f141070, 0x3e1474ab, 0x444b256e, 0x537050d9, 0x0f42094b, 0x2fd820e6, 0x778b2e5e, 0x71176d02, 0x7fea7a69, 0x5bb54628, 0x19ba6c71, 0x39763a99,
0x178d54cd, 0x01246e88, 0x3313537e, 0x2b8e2d17, 0x2a3d10be, 0x59d10582, 0x37a163db, 0x30d6489a, 0x6a215c46, 0x0e1c7a76, 0x1fc760e7, 0x79b80c65, 0x27f459b4, 0x799a7326, 0x50ba1782, 0x2a116d5c,
0x63866e1b, 0x3f920e3c, 0x55023490, 0x55b56089, 0x2c391fd1, 0x2f8035c2, 0x64fd2b7a, 0x4ce8759a, 0x518504f0, 0x799501a8, 0x3f5b2cad, 0x38e60160, 0x637641d8, 0x33352a42, 0x51a22c19, 0x085c5851,
0x032917ab, 0x2b770ac7, 0x30ac77b3, 0x2bec1907, 0x035202d0, 0x0fa933d3, 0x61255df3, 0x22ad06bf, 0x58b86971, 0x5fca0de5, 0x700d6456, 0x56a973db, 0x5ab759fd, 0x330e0be2, 0x5b3c0ddd, 0x495d3c60,
0x53bd59a6, 0x4c5e6d91, 0x49d9318d, 0x103d5079, 0x61ce42e3, 0x7ed5121d, 0x14e160ed, 0x212d4ef2, 0x270133f0, 0x62435a96, 0x1fa75e8b, 0x6f092fbe, 0x4a000d49, 0x57ae1c70, 0x004e2477, 0x561e7e72,
0x468c0033, 0x5dcc2402, 0x78507ac6, 0x58af24c7, 0x0df62d34, 0x358a4708, 0x3cfb1e11, 0x2b71451c, 0x77a75295, 0x56890721, 0x0fef75f3, 0x120f24f1, 0x01990ae7, 0x339c4452, 0x27a15b8e, 0x0ba7276d,
0x60dc1b7b, 0x4f4b7f82, 0x67db7007, 0x4f4a57d9, 0x621252e8, 0x20532cfc, 0x6a390306, 0x18800423, 0x19f3778a, 0x462316f0, 0x56ae0937, 0x43c2675c, 0x65ca45fd, 0x0d604ff2, 0x0bfd22cb, 0x3afe643b,
0x3bf67fa6, 0x44623579, 0x184031f8, 0x32174f97, 0x4c6a092a, 0x5fb50261, 0x01650174, 0x33634af1, 0x712d18f4, 0x6e997169, 0x5dab7afe, 0x7c2b2ee8, 0x6edb75b4, 0x5f836fb6, 0x3c2a6dd6, 0x292d05c2,
0x052244db, 0x149a5f4f, 0x5d486540, 0x331d15ea, 0x4f456920, 0x483a699f, 0x3b450f05, 0x3b207c6c, 0x749d70fe, 0x417461f6, 0x62b031f1, 0x2750577b, 0x29131533, 0x588c3808, 0x1aef3456, 0x0f3c00ec,
0x7da74742, 0x4b797a6c, 0x5ebb3287, 0x786558b8, 0x00ed4ff2, 0x6269691e, 0x24a2255f, 0x62c11f7e, 0x2f8a7dcd, 0x643b17fe, 0x778318b8, 0x253b60fe, 0x34bb63a3, 0x5b03214f, 0x5f1571f4, 0x1a316e9f,
0x7acf2704, 0x28896838, 0x18614677, 0x1bf569eb, 0x0ba85ec9, 0x6aca6b46, 0x1e43422a, 0x514d5f0e, 0x413e018c, 0x307626e9, 0x01ed1dfa, 0x49f46f5a, 0x461b642b, 0x7d7007f2, 0x13652657, 0x6b160bc5,
0x65e04849, 0x1f526e1c, 0x5a0251b6, 0x2bd73f69, 0x2dbf7acd, 0x51e63e80, 0x5cf2670f, 0x21cd0a03, 0x5cff0261, 0x33ae061e, 0x3bb6345f, 0x5d814a75, 0x257b5df4, 0x0a5c2c5b, 0x16a45527, 0x16f23945 };
// 用户名加密
DWORD UNEncode(const char* pUsername, int a2, char a3, unsigned __int16 a4) {
    // 计算出用户名的长度
    int UNLen = strlen(pUsername);
    DWORD LOCAL_1 = 0, LOCAL_2 = 0, LOCAL_3 = 0, LOCAL_4 = 0;
    DWORD EAX = 0, EBX = 0, ECX = 0, EDX = 0;
 
    if (UNLen > 0) {
        EBX = a4;
        LOCAL_4 = ECX;
        LOCAL_3 = ECX;
        ECX = a3;
        EBX = EBX << 4;
        EBX -= a4;
        ECX = ECX << 4;
        ECX += a3;
        LOCAL_2 = ECX;
        // 然后就到了最重要的循环计算环节
        for (int i = 0; i < UNLen; i++) {
            // 将字符转换成大写
            EAX = toupper(pUsername[i]);
            EDX = EAX;
            ECX = codeArr[EDX] + LOCAL_1;
            // 两条不同的分支
            if (a2 == 1) {
                EAX = EDX + 0xD;
                EAX & 0xFF;
                ECX = ECX ^ codeArr[EAX];
                EAX = EDX + 0x2F;
                EAX = EAX & 0xFF;
                ECX *= codeArr[EAX];
                EAX = LOCAL_2;
                EAX = EAX & 0xFF;
                ECX += codeArr[EAX];
                EAX = EBX & 0xFF;
                ECX += codeArr[EAX];
                EAX = LOCAL_3;
                EAX = EAX & 0xFF;
                ECX += codeArr[EAX];
                EAX = ECX;
                LOCAL_1 = EAX;
            }
            if (a2 == 0) {
              /*013BD1CE | >  8D42 3F | LEA EAX, DWORD PTR DS : [EDX + 0x3F] ;  EAX = 大写字母的ASCII码 + 0x3F的地址
                013BD1D1 | .  25 FF000000 | AND EAX, 0xFF;  EAX = EAX & 0xFF
                013BD1D6 | .  330C85 4841E6 > | XOR ECX, DWORD PTR DS : [EAX * 4 + 0x2E64148] ;  ECX = ECX ^ (EAX * 4 + byCode)
                013BD1DD | .  8D42 17 | LEA EAX, DWORD PTR DS : [EDX + 0x17] ;  EAX = 大写字母ASCII码 + 0x17的地址
                013BD1E0 | .  25 FF000000 | AND EAX, 0xFF;  EAX = EAX & 0xFF
                013BD1E5 | .  0FAF0C85 4841 > | IMUL ECX, DWORD PTR DS : [EAX * 4 + 0x2E64148] ;  ECX = ECX * (EAX * 4 + byCode)
                013BD1ED | .  8B45 F8 | MOV EAX, [LOCAL_AL.2];  EAX = 0
                013BD1F0 | .  0FB6C0 | MOVZX EAX, AL;  EAX = 0
                013BD1F3 | .  030C85 4841E6 > | ADD ECX, DWORD PTR DS : [EAX * 4 + 0x2E64148] ;  ECX = ECX + (EAX * 4 + byCode)
                013BD1FA | .  0FB6C3 | MOVZX EAX, BL;  EAX = 0x0000 0098
                013BD1FD | .  030C85 4841E6 > | ADD ECX, DWORD PTR DS : [EAX * 4 + 0x2E64148] ;  ECX = ECX + (EAX * 4 + byCode)
                013BD204 | .  8B45 F0 | MOV EAX, [LOCAL_AL.4];  EAX = 0
                013BD207 | .  0FB6C0 | MOVZX EAX, AL;  EAX = 0
                013BD20A | .  030C85 4841E6 > | ADD ECX, DWORD PTR DS : [EAX * 4 + 0x2E64148] ;  ECX = ECX + (EAX * 4 + byCode)
                013BD211 | .  8BC1 | MOV EAX, ECX;  EAX = ECX
                013BD213 | .  894D FC | MOV[LOCAL_AL.1], ECX;  LOCAL_al.1 = ECX
                013BD216 | > 8345 F4 13 | ADD[LOCAL_AL.3], 0x13;  LOCAL_al.3 = LOCAL_al.3 + 0x13 = 0x13*/
            }
            LOCAL_3 += 0x13;
            LOCAL_2 += 0x9;
            EBX += 0xD;
            LOCAL_4 += 0x7;
        }
    }
    return EAX;
}
 
int main() {
    // 首先获取一下用户名
    srand(time(NULL));
    BYTE byCode[10] = { 0x12,0x34,0x56,0x9C,0x12,0x34,0x56,0x78,0x12,0x34 };
    // 用户名计算
    int dwKey = 0x3E8;
    char chUsername[50] = { 0 };
    printf("请输入用户名: ");
    scanf_s("%s", chUsername, 50);
    DWORD dwUNKey = UNEncode(chUsername, 1, 0, dwKey);
    // 序列号的4567位分别对应计算结果的4个字节
    byCode[4] = dwUNKey & 0xFF;
    byCode[5] = dwUNKey >> 8 & 0xFF;
    byCode[6] = dwUNKey >> 16 & 0xFF;
    byCode[7] = dwUNKey >> 24 & 0xFF;
 
    // 还原一下我们输进去的序列号 这里我们进入的分支是k[3] == 0x9C
    while (true) {
        // 随机生成两个1字节的数
        BYTE tempCode0 = rand() % 0xFF;
        BYTE tempCode6 = byCode[6];
        // 最先开始处理的数据 AL = (k[0] ^ k[6] ^ 0x18 + 0x3D) ^ 0xA7
        // 我们也构造一个变量出来让它对应上数组下标为06的元素关系
        BYTE AL = (tempCode0 ^ tempCode0 ^ 0x18 + 0x3D) ^ 0xA7;
        if (AL >= 9) {
            byCode[0] = tempCode0;
            byCode[6] = tempCode6;
            break;
        }
    }
    // ESI = (0x100 * (k[1] ^ k[7] & 0xFF) + ((k[5] ^ k[2]) & 0xFF)) & 0xFFFF
    // EAX = ((ESI) ^ 0x7892 + 0x4D30) ^ 0x3421 & 0xFFFF / 0xB
    // 判断余数是否为0 ,为0返回商,否则返回0
    // 这里余数要为0
    // 商还要小于0x3E8
    while (true) {
        BYTE tempCode1 = rand() % 0xFF;
        BYTE tempCode2 = rand() % 0xFF;
        BYTE tempCode5 = byCode[5];
        BYTE tempCode7 = byCode[7];
 
        DWORD ESI = (0x100 * (tempCode1 ^ tempCode7 & 0xFF) + tempCode5 ^ tempCode2 & 0xFF) & 0xFFFF;
        DWORD EAX = (((ESI ^ 0x7892) + 0x4D30) ^ 0x3421) & 0xFFFF;
        if (EAX % 0xB == 0 && EAX / 0xB == dwKey) {
            byCode[1] = tempCode1;
            byCode[2] = tempCode2;
            byCode[5] = tempCode5;
            byCode[7] = tempCode7;
            break;
        }
    }
    printf(("序列号为: %2X%2X-%2X%2X-%2X%2X-%2X%2X-%2X%2X"), byCode[0], byCode[1], byCode[2], byCode[3], byCode[4], byCode[5], byCode[6], byCode[7], byCode[8], byCode[9]);
}
分支二:当[ARG.2]是0的时候

image-20211205130322040

 

可见无论[ARG.2]是1是0,都会执行跳转下来后的这些语句,并且执行循环,最后ret出去

 

image-20211205123905512

 

至此,它的分析算是完成了~

分支:从验证成功的地方入手

今天在找字符串的时候留了个心眼,突然感觉没必要在验证失败这里找东西,真正的玄机不应该是在验证成功之前吗?于是我就顺着验证失败的字符串往上下看了一下,果不其然找到了一个订阅成功的字符串提示:

 

image-20211203092410307

 

然后我们双击跟进看一下,老样子还是看一下交叉引用,也是只有一个地方引用了,离真相不远了!

 

image-20211203092512189

 

来到了代码段,我们点击F5将其转化成源码(伪码)来看看这里究竟是怎么验证的。

 

image-20211203092655458

 

image-20211203092940993

 

image-20211203093005948

 

还行,看来懂点英文还是好的


[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2021-12-5 17:02 被JokerMss编辑 ,原因: 内容更新
上传的附件:
收藏
点赞4
打赏
分享
最新回复 (6)
雪    币: 2739
活跃值: (2417)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xiwushgya 2021-12-2 18:53
2
0
牛掰,明天继续期待楼主的大作
雪    币: 560
活跃值: (751)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
wx_Van_Zovy 2021-12-2 21:12
3
0
向大佬学习
雪    币: 48
活跃值: (75)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
bmyqzs 2021-12-3 08:57
4
0
打卡
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_lptgynwu 2021-12-3 10:44
5
0
最后那个0xdb是必须等吧
雪    币: 727
活跃值: (1678)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
JokerMss 2021-12-3 20:24
6
0
mb_lptgynwu 最后那个0xdb是必须等吧
哈哈哈,是的,昨天没有继续往下看了,今天刚刚总结了一下
雪    币: 661
活跃值: (1138)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Anowhere 2022-1-21 11:29
7
0
我直接用的暴力破解,直接把0xDB赋值给EAX了,哈哈,毕竟我是个菜鸟
游客
登录 | 注册 方可回帖
返回