Typora解密之跳动的二进制
时间:2022-05-19
磨刀霍霍 :准备工作
开发环境识别
用IDA打开 Typora.exe (不建议,可能是我电脑问题,IDA分析了三四个小时),打开后发现程序中有 electron ,V8字样,由于前几天刚好分析了一个IE漏洞,由V8联想到 JavaScript引擎,此时猜测与JS语言有关。
打开程序目录,查看一下有没有JS代码,一番查找,看到了asar文件以及node字样,感觉有些眼熟哪里见过。问一下度娘,百度搜索asar,electron,nodemodules
不搜不知道,一搜吓一跳,原来这是使用 NodeJs的electron框架开发的桌面应用,JS也能写桌面程序了。
继续搜索相关资料,得到以下信息:
1.electron 使用了谷歌的 V8引擎以及渲染引擎(意思是这种程序跟个浏览器差不多呗)
2.electron 有主进程与渲染进程 ,通过IPC交换信息,渲染进程只负责渲染(难怪我附加调试有好几个进程。。)
3.Typora.exe是electron框架,基本与开发者的代码无关(修改框架复杂度太高,一般人不会去动,所以别去逆向它了)
4.查看目录发现,在app.asar.unpacked中发现了一个 main.node ,看名字很奇怪,.node文件是啥东西
5.app.asar是打包的JS代码,并且只是简单的打包,没有任何加密措施,且electron也没有代码保护措施(圈起来要考)
根据第五条信息,笔者尝试使用工具对asar进行解包,解包失败不知道什么原因(或许必须使用NodeJS自带的解包工具?但是解包也是加密文件,我又懒得下载nodejs,需要的时候再解吧),放进010editor查看,得到文件名与一些密文,此时陷入了僵局。
尝试调试
打开typora进程,x64dbg附加,查看有没有有用的信息(刚开始我不知道这个是NodeJS开发,想着通过注册窗口跟踪程序流程,好家伙差点就逆到引擎去了),发现了有好几个进程,后面命令行参数可以看到渲染gpu等关键字,并且目录参数指向了app.asar。
根据前面得到的信息,这些进程都是由主进程创建的,程序逻辑是由主进程处理,那么附加主进程看一下(没参数的那个)
看到了主进程加载了main.node模块,什么.node也能当dll加载 ? 或许它本来就是一个dll呢(也不排除加壳后,手动加载的情况)
那么使用PE工具查一下,VS2017编译的64位DLL。
二
梅开二度 :逻辑分析
到这一步,已知信息:
1.JS文件被加密
2.框架为electron
3.框架会加载main.node模块
4.解析JS脚本的是V8引擎
5.c++支持node api进行开发
根据我们的已知信息,来对程序的整体逻辑进行一个简单的分析。
1.框架对代码无保护且修改框架难度过高,V8引擎不支持解析加密的JS代码,那么JS代码如何运行?
站在一个开发者的角度,我可能会想到由框架加载我的解密代码,把js代码解密后送到js引擎去执行就可以了
2.那么解密代码放在哪里合适呢?
既然框架被编译为二进制了,且根据之前的分析,妥妥的c++开发,想要最简单的方法实现解密,加载同样为c/c++编译的二进制代码即可。在Windows平台上,想要加载代码执行,也就剩动态链接库了。由此可以推测,之前找到的main.node,可能就是解密模块。
逻辑总结
- 框架加载解密模块
- 解密模块对app.asar进行解密(解密后会不会把文件写出来呢,如果写出来可以直接拷走。。)
- 解密后的代码送入JS引擎执行
- 另外的逻辑 :由解密模块解密app.asar的xxx.js代码,xxx.js代码执行后,由它来负责解密剩下的js代码并执行,可以提高破解难度三
精益求精 :main.node分析
1.根据之前的推测,main.node负责解密,按照程序员开发习惯,Ctrl CV实现,也就是使用公开的一些算法
2.IDA加载main.node,按照逆向惯例,先搜索一波字符串
此时看到了buffer,base64,app.asar等关键字。猜测一下,app.asar加载到buffer然后进行base64解密。
3.继续开展搜索工作,base64未免太过简单了吧,使用FindCrypt3插件 ,搜索一下算法常量吧。
此时找到了AES的算法常量,前两个是重复的,可能是插件问题。
4.好的,现在面临一个问题,我不懂算法,怎么解密。。。只能去问度娘了,搜索一下AES加密解密原理与 C 实现代码。
5.根据搜索得知:
- AES使用最后那个常量数组进行解密
- AES有五种加密模式,常用的为ECB CBC模式
- 根据AES密钥长度,有不同的加密轮数(不是很懂哈。。)
6.对解密常量进行交叉引用跟踪,找到这个函数以后,继续对函数进行交叉引用跟踪。
大概经过三四次的跟踪,发现了这个函数,在这个函数里F5查看反编译代码,发现了app.asar字符串的引用。
7.此时进行推测,这个函数加载了app.asar的内容,并且调用 SUB_180003E40进行解密。
8.跟进SUB_180003E40 进行查看 ,此时发现了base64字符串的引用,推测对buffer进行了base64解密。
9.看到很多不认识的API,百度搜索得知,这是Node API,简单去看一下函数功能http://nodejs.cn/api/n-api.html#napi_call_function
NAPI_EXTERN napi_status napi_call_function(napi_env env, //环境 napi_value recv, //名为global的值 napi_value func, //要调用的javascript函数 size_t argc, //JavaScript函数的参数个数 类似argc const napi_value* argv, //JavaScript函数的参数数组 类似argv napi_value* result); //返回的JavaScript对象
10.根据文档得到的信息,参照这一部分Node API,得到了如下信息(猜测调用了此函数)笔者对这语法难以理解,程序的目的是把对象进行base64编码?但是看这代码,base64也没有被当作参数传递进去
Buffer.from( object, encoding )object:此参数可以包含字符串,缓冲区,数组或arrayBuffer。encoding:如果对象是字符串,则用于指定其编码。它是可选参数。其默认值为utf8。 Buffer.from(string[, encoding]):返回一个被 string 的值初始化的新的 Buffer 实例
11.暂时先不管node api的语法与功能,继续往下看。
看到了这部分的函数调用,进入查看,发现与 C实现AES算法结构相似,推测这部分为AES解密。
分析总结
根据目前的分析,得到如下信息
- main.node模块使用node api 进行js函数调用
- main.node模块使用了AES解密算法(模式未知)
到了这一步,想要继续破解,首先要得到js代码,有两个办法以及面临的问题。
1.分析算法,找到密钥,如果是CBC模式,还需要找到IV ,之后使用解密算法,解密app.asar的js代码
2.分析程序执行流程,找到解密后的缓冲区,直接拷走,得到彻底解密后的js代码。
面临的问题:
- nodejs语法不懂,这些api调用的具体js函数不清晰
- AES解密流程不熟悉,找到密钥或者iv的难度较高(通过加密轮数判断密钥长度,通过算法部分判断加密模式,并找到相关数据)
- 就算你找到解密AES的办法,它会不会还有别的加密措施与防护措施,例如密钥需经过hash摘要,文件完整性校验等(如果还有加密保护措施,那还得继续分析别的算法,一步一步还原,对于算法不熟悉的人时间成本太大了)四
大海捞针 :寻找JS代码
1.根据之前的分析,我选择第二种获得js代码的办法,分析程序执行流程得到解密后的 JS代码。
2.分析解密前的 js函数调用,由之前的分析得知,参数有两个,在V27的位置,V27是由参数 a3 +8 得来的,动态调式一波。
3.x64dbg打开typora.exe,下一个dll断点,根据之前分析,他是动态加载的(多打了个a 懒得换图了)
4.断点设置完成后,断在loadlibrary,进入模块入口后,去IDA计算偏移,定位到前面分析的函数位置。
对了,记得使用x64dbg自带的 PEB隐藏功能,并忽略所有异常,这个模块有简单的反调试手段(查看导入表可知,太过简单不分析反调试手段了)
定位函数位置,直接看后4位即可,同样为模块偏移 674A的位置。
5.下断后查看decrypt(命名一下方便)函数的参数,x64架构下,函数参数为 rcx rdx r8 r9 rsp+0x20
前四个从左到右,超过四个则入栈,rsp+0x20为起始地址,详情可参考微软x64调用约定。
6.r8 = a3 查看 *(a3 + 8)的值, ,这个值为之前分析的 v27 地址,也就是argv 继续查看指针指向内容。
现在得到了buffer.from js函数的两个参数 ,第一个像是密文,第二个没看出来,不是预料中的base64,所以之前的推断貌似是错的?
7.关注一下这两个地址 0000079908482119 00000799083CFEA5 在调用完js函数会有什么改变,直接来到调用的位置,调试器同步来到这个位置。
来到这个位置再次确认参数,第五个参数为 rsp+20,也就是rax的值,rax为argv,进入内存查看得到前面同样的地址,也就是 (a3 + 8),步过这个函数,查看对参数的改变。
8.执行完后查看刚才记录的位置,好的好的,耍我呢,啥都没变,并且后续也没调用相关数据(或许是最后一个参数返回了一个对象,忘记看了,如果返回的话应该是密文相关的东西,并且放到了某个数据结构中,所以在IDA中没看到直接使用的行为)
继续分析,到了AES解密代码部分,既然是解密,那肯定得把密文的缓冲区拿过来吧。
首先看到一串16进制的赋值,v46开头的数组 刚好32个字节,也就是256bit有点像是AES-256的样子了。然后看到申请了 32字节的内存,v32
之后调用了 sub_18000B060函数,对v46 与 v32进行操作, 目测参数为 (目标地址,源地址,大小)
进入 sub_18000B060函数查看,一大堆运算,根本不想看,根据之前的推测,可能是作者感觉直接把密钥放在程序中有些不妥,所以对密钥进行一个类似于解密或hash运算的工作(不展开分析了)
9.继续分析
可以看到sub_180007000 函数 ,参数v45 IDA提示我是一个 char[256]的数组,v32为32字节的地址,v10为一串神秘数据。
可以得到一个结论,在经过 v46的一系列运算,得到了一个同样大小 32字节的数据,再把数据 与 v10神秘数据进行操作,放入v45的256字节的数组中(好家伙 这是密钥吗 搞这么复杂)
跟进sub_180007000 查看, 把v10放到了 v45数组的0xF0的位置
之后调用了sub_180007800函数对自己的PE文件有些操作,简单看了下前面的汇编,主要内容为,把v32 放到 v45中,大小为32字节
10.继续分析
接下来看一下sub_180005c00 函数,使用了v27,之前分析出来的密文地址就在v27中,v30没看出来,应该是传出参数后面用到了。
猜测这个函数对密文进行一波操作,看一下返回值用来做什么,这个伪代码看的头疼,汇编看一下。
.text:0000000180004021 call sub_180005C00.text:0000000180004026 mov rbx, rax.text:0000000180004029 mov r14, [rax+8] ;返回值+8的内容给 r14.text:000000018000402D sub r14, [rax] ; r14 - 返回值的内容.text:0000000180004030 mov rcx, r14 ; 得到一个大小 Size
人工反编译一下 首先确定rax为一个指针 *(rax+8) - *rax 就是这个地址里面存储了两个值,拿第二个值减第一个值得到一个size。
此时猜测,这两个值或许是 密文的开始地址与结束地址?
然后用这个size 申请了一块内存 , IDA 命名为 Block , sub_18000B060 之前分析过, 对*v12进行操作, 结果给到Block
把V12代入rax中 , *(v12+8) - *v12 , 结束地址减去开始地址得到 size
由此可以验证猜测, *v12 为密文开始地址 ,V13为密文大小
11.继续分析第三个方框的内容
以v13 + 1 的大小 申请了一块内存 v14 , sub_18000B060 对 Block 再次进行操作, 结果给到v14。
v14的最后一个字节置为0 ,推测已经把密文转换为字符串了 , 需要一个 NULL 结尾。
;v15 = r8d rcx = v13 rbx = v14 .text:0000000180004094 movsxd rcx, r14d.text:0000000180004097 movzx r8d, byte ptr [rcx+rbx-1]
由上可得 v15 = v14[v13-1] , 也就是从v14中取了一个字节的值 ,位置在null字符的前一byte。
12.继续分析sub_180006AC0
可以看到,sub_180006AC0 的参数 , v45(256字节数组),block ,v13。
结合之前对 sub_180007000的分析,可以得知, 目前v45的状态 ,v45[0-31]为32字节的类似密钥的东西 v45[0xF0] 为 v10的神秘数据 ,v13为Block大小。
跟进简单查看 ,查看后感觉可读性不好,笔者对照汇编代码,重新修改了一下反编译代码。
__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; }
根据目前的分析,可以推测 ,sub_180006AC0函数为 主要的解密算法函数,看着像是 AES CBC模式,因为对算法不熟悉,大胆猜测一下。
key存放在v45中, 前32字节 ,也就是256位 , iv存放在 block+v5中 (不清楚对不对)
13.继续分析剩下的内容
好的好的,看着有点头疼,后面的代码大概意思就是,又对解密后的数据进行了一系列操作,最后返回了一个缓冲区。
读者感兴趣可以自行分析,实在是写不动了。
釜底抽薪 :得到JS代码
1.根据前面的分析,我们已经大致了解了程序流程,来到调用解密函数的函数,只需要在彻底解密后,送到JS引擎执行的时候,拿到解密的JS代码即可。
2.根据上层调用代码,可以得到,解密后返回了一个值,作为调用JS函数的参数 ,定位到678F偏移处,x64dbg同步定位。
3.断下后查看v28的内容 , RSP+20 的位置,然后继续查看这个指针的指针的内容,最后得到了解密后unicode形式的JS代码。
4.把内容拷走,拿到010editor,把00去掉,变成ascii形式,检查一下得到的数据。
看起来跟密钥有关的一串字符编码数据:
搜索一下license相关的数据, 找到不少,看起来也像是代码,应该没问题
指鹿为马 :破解可行性分析
修改文件破解
如果懂算法与NodeJS,可以通过分析,找到关键的key等数据,对app.asar进行解包解密操作得到JS代码进行修改后,打包回去即可
可能遇到的问题:对app.asar进行完整性校验。
内存破解
简单说几种思路,由于main.node是后加载的模块,所以内存破解有些难度。
- 调试器加载 :参照上述手段,在模块加载通知中断下,定位到解密函数下断,修改内存中的JS代码
- 导出表HOOK:参考病毒木马使用的进程替换(傀儡进程)技术,创建进程后挂起,由于main.node中的node api是使用框架中的导出api,所以可以替换导出函数为自己的函数,在调用时进行参数判断,如果为JS代码,则修改
- DLL劫持:替换main.node,由自己加载真正的main.node并调用,调用时,定位到解密函数并hook,等待JS代码并修改
- PE代码注入 :修改框架的PE文件,并加载自己的DLL,加载后进行导出表hook
可能遇到的问题:对main.node或者框架进行完整性校验,更加强大的反调试手段。
方法还有很多,不再一一列举,这里只能提出思路
点到为止 :总结
- 通过这次逆向分析,踩了不少坑,学到了不少东西,并且加深了逆向技术的基础。
- 作为一个逆向练习生,遇到不懂的,不会的,应该迎难而上,扬长避短,不可轻言放弃。
- 遇到一个纠结的地方,不要过度停留,逆向分析应该是分析大方向,站在开发者角度,根据分析出来的功能猜测作者的意图,以找到关键突破点。
结语
- 由于笔者对算法与Node Js开发并不熟悉,所以没办法得到密钥与其它解密数据(文章中关于算法的一些操作皆为推测,相信熟悉算法的大佬可以看出来密钥所在)
- 对于最后得到的JS代码也没办法判断到底完不完整,是否还有未解密的部分,所以只能到此为止了(看起来是完整了)
- 经过笔者一段时间的努力,已经成功实现内存破解,详情见下篇。