0%

Typora逆向

文件格式分析

Typora采用的是electron框架构建跨平台桌面应用。从实现方式上来说,其本质还是基于chrome内核的html、js、css构成的应用。

electron打包的项目,最常见的就是 asar 格式的私有编码文件,里面包含文件名、大小、内容偏移量等数据,按文件头部的 json内容 解析即可提取出所有文件。

image-20220720142337810

image-20220720143042743

工具查看

image-20220720143450494

在github上有一个项目讲到关于electron(该方案可以把启动文件编译为node二进制文件,作为启动入口,来保护薄弱的js代码。在项目启动时,将加密后的代码进行解密,交回electron流程进行执行,从而避免上面说的直接解包拿到源代码的可能。

image-20220720145218070

image-20220720160414013

main属性就是 electron 项目启动的主入口。 把 main.node 拖到ida中, 分析执行流程。

IDA分析主入口

啊这拖进去就是个dll啊

image-20220721142800220

根据前面分析的,main.node(.node 文件是 c/c++ 写的扩展文件)是负责解密的,先直接用插件看一下用了什么加密方法

image-20220720164134883

采用的是AES对称加密算法,解密采用的是第三个常量数组。交叉引用看一下

image-20220721142839196

注意到这里,推测这里就是先加载app.asar的内容,然后调用sub_180003E40进行解密

image-20220721143250799

主要看一下sub_180003E40函数,查阅一下资料可知,napi_create_string_utf8函数目的是生成一个base64编码的字符串,结果存储在最后一个参数。napi_get_named_property函数是从第二个参数中检索第三个参数的属性名称,获得的该属性的值存放在第四个参数。

image-20220721144404863

主要看下面这部分AES解密,要么找到解密后的缓冲区,要么找到密钥,然后判断教秘方式进行解密。第一种方式要简单一点(因为加密算法不太会)。

分析流程获取解密后的缓冲区

napi_call_function函数第三个参数就是要调用的函数,第五个参数是参数数组。

image-20220721153437504

image-20220721145500979

动态调试一下,下一个dll断点(note:注意选择调试-高级-隐藏PEB以及忽略所有异常,因为有反调试)

image-20220721150921124

image-20220721145735259

image-20220721150804855

再通过IDA查看函数偏移

image-20220721150231522

定位函数位置

image-20220721151136852

我们知道64位程序调用约定是 rcx rdx r8 r9,超过四个通过栈传递,前面分析v27 = *(a3 + 8),a3也就是r8存储的内容

image-20220721152336879

选择在转储中跟随右下角圈出来的地方,可以看到buffer.from的两个参数。最后没懂这里的操作,查看博客发现这个参数没有发生改变,后续也没有调用相关数据结构,继续往下看AES解密部分,解密首先肯定得获得密文缓冲区。

可以看到申请了32个字节的内存给v32,然后sub_18000B060对v32和v46今次了操作,v32应该是目标地址,第三个参数是大小,v46就是源地址了。这个函数进去发现是一大堆运算,估计是对v46的一些解密或者hash之类的操作。

image-20220721154913867

可以看到sub_180007000函数对上一步通过v46得到的v32数据与v10数据进行操作,然后放到v45数组中(感觉应该是密钥,这么复杂应该是为了保护密钥吧),在这个函数中调用了sub_180007800,这个函数对自己的PE文件进行了一些操作,感觉不是很重要,往下看看。

image-20220721155431157

image-20220721155356220

接下来分析一下sub_180005c00函数,前面分析的密文地址应该就是在v27中,这个函数主要看它的返回值有什么用。

下面这段汇编的意思是,返回值存储在rax中,将*(rax+8)的内容给r14,然后减去*rax,得到一个size。或许说这两个值是密文开始与结束地址?

image-20220721161853372

可以看到用前面得到的size申请了一块Block,函数sub_18000B060对*v12进行操作,存储在Block中。v12就是前面说的rax。所以分析到这里知道了,v12为密文开始地址,v13为密文大小。

image-20220721162241819

v13 + 1 的大小 申请了一块内存 v14 , sub_18000B060 对 Block 再次进行操作, 结果放到v14,v14的最后一个字节置为0 ,推测已经把密文转换为字符串了 , 需要一个 NULL 结尾。v15 = v14[v13-1] , 也就是从v14中取了一个字节的值 ,位置在null字符的前一字节。

image-20220721163753782

关注一下上面的sub_180006AC0函数,这个函数是我们从解密数组一步步交叉引用找到的函数,v45是sub_180007000函数对上一步通过v46得到的v32数据与v10数据进行操作得到的。(下面是这位大佬分析的)

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
_int64 __fastcall sub_180006AC0(v45,block,block_size)
{

if ( block_size )
{
v3 = block;
v5 = v45 + 0xF0 - (_QWORD)block; //v45+0xF0的地址 减去 block的地址得到v5
v6 = ((block_size - 1) >> 4) + 1; //做为外圈循环的次数
do
{
v7 = *v3;
//v7为 xmmword 16字节浮点寄存器 ,把block的内容取16字节给v7 16字节符合AES块大小
//由此推测block是真正的密文,将在这个函数中进行解密操作

sub_180007320(v3, v45); //用到了AES解密常量 应该是解密相关 并且对推测的key 也就是前32字节有一些操作
v8 = 16i64; //内圈循环16次
do
{
result = *((char*)(v3 + v5)); //block地址 + v5偏移 取一个字节内容
*(char*)v3 ^= result; //取block的1字节数据,与block地址 + v5偏移 进行异或
v3 = (__int128 *)((char *)v3 + 1); //block += 1
--v8; //总共16次 也就是16个字节异或
}
while ( v8 );
v5 -= 16i64; //外圈循环 v5 每次-16 也就是每次异或 异或的值都会变化 范围为-16字节
v45 + 0xF0 = v7; //block的16字节内容 给到v45+0xF0
--v6; //外圈循环次数
}
while ( v6 );
}
return result;
}

得到解密后数据

来到调用解密函数的函数,只需要在彻底解密后,送到JS引擎执行的时候,拿到解密的JS代码即可。解密后返回了一个值,这个值作为了调用JS函数的参数

image-20220721165140509

在x64dbg中调试,查看第五个参数也就是rsp+20的位置。

image-20220721165400986

一步步在转储中跟随,最后得到了解密后unicode形式的JS代码

image-20220721165843391

破解思路

main.node模块是运行起来后加载的模块。其实要破解的思路就是修改JS代码,修改判断逻辑。下面介绍的几种方法都是为了修改JS代码服务的。

1.调试器加载:在模块加载通知中断下,定位到解密函数下断,修改内存中的JS代码

2.导出表hook:参考进程替换(傀儡进程)技术,创建进程后挂起,由于main.node中的node api是使用框架中的导出api,所以可以替换导出函数为自己的函数,在调用时进行参数判断,如果为JS代码,则修改。

3.Dll劫持:替换main.node,由自己加载真正的main.node并调用,调用时,定位到解密函数并hook,等待JS代码并修改。

4.PE代码注入: 修改框架的PE文件,并加载自己的DLL,加载后进行导出表hook。

总结

总结一下分析思路:框架会加载解密模块main.node,解密模块对app.asar进行解密,然后将解密的代码送入JS引擎执行。