我是逆向练习生,羽墨
目前最为流行的md文件编辑器,当属Typora,免费,简洁,让人爱不释手
接上篇 Typora破解之逆向分析 , 实现二进制层面的内存破解(证明内存破解的可行性,了却心愿)
经过几天断断续续的研究,我去逆向了Node API ,逆向了V8引擎,逆向了Electron框架,走入了无数的深坑。。。终于完美实现了内存破解。
在各路大神破解Typora的文章中,我看到的都是千篇一律的破解办法,所有我也本着论证我在上篇文章中提出的破解思路,而实现了我所希望的目标。
本文中会给出两种办法,一种是基于病毒木马技术,一种是基于PE注入技术(适合新手复现)
注:文章重在讲述逆向过程中的思路与方法 不然发文章除了炫耀毫无意义
1.根据上篇文章分析结果,继续进行深入分析,终于在我努力下,成功找到了关键函数,这个函数调用是napi_create_string_utf8,它会把ascii形式的JS代码当作参数传递,rbp - 50 的位置为这个字符对象的指针,里面存储的数据,经过我观察如下(只说重点的)
0x0偏移为JS代码的缓冲区,0x10偏移为JS代码的大小,0x18偏移为JS代码的关键验证点,必须比0x10位置的数据大1,但是据我观察他正常的时候不是这样的,可是在引擎中还是框架中(记不清了,只知道是海一样的汇编代码)会这样进行判断,好家伙太坑了,我差点晕倒
顺便提一下逆向引擎过程中发现的东西,V8引擎读取JS代码后,经过一些函数,之后申请一个缓冲区,代码流程跳到缓冲区执行,所以说引擎类似于一个动态的编译器,最后也是在内存中执行二进制代码
具体的逆向引擎与框架的过程就不说了。。。
2.根据上篇总结的思路,笔者准备采用病毒木马常用的技术 , 进程替换(傀儡进程)来实现hook关键函数,达到破解的目的
为什么使用这个技术,主要是因为,main.node是动态加载的模块,我必须在它加载之前进行hook,不然它加载起来会直接进入JS代码执行的步骤,所以需要这个技术
那么关键函数怎么hook,hook什么位置呢,上篇中有提到,这是一个导入函数,导入了electron的导出函数,所以目标就明确了,我们可以通过遍历导出表,来找到这个函数的RVA,之后获得目标进程BaseAddr,加上RVA,就可以定位到这个函数了
那么还有一个问题,既然main.node使用的是导入函数,熟悉PE格式的同学可能就会知道了,加载导入表是通过GetProcAddress来加载的,那么是不是只需要修改electron框架的导出表的导出地址就可以了?对,就是这样,当然,直接hook函数也是可以的,但是毕竟要修改人家的原始函数,我感觉多多少少有点不靠谱。。。
3.要实现这个技术,要点是在CreateProcess上, 查看微软文档可知, 创建进程时, 以CREATE_SUSPENDED标志创建,即可在进程创建后,把主线程挂起,挂起以后,程序会处在PE映射完毕,进入Startup入口处的状态
4.挂起以后,需要定位导出函数在导出函数地址表中的RVA
5.进程肯定是会开随机基址的,所以我们不能使用固定的地址
6.定位导出函数地址以后,通过代码注入技术,把hook代码写入到 electron中,hook代码应为shellcode形式
7.修改导出函数的导出函数地址表的地址,指向注入的代码,在代码中根据我们的逆向分析结果,判断是否为关键调用
8.如果判断为JS代码,则向内存中写入Patch,如不是,则调用原函数
9.还有很多细节,在下面解释,大体方向搞清楚了,可以开工了,VS2019 启动!!!
好的,说起来简单做起来难
因为我们使用的是x64版本的Typora,所以也需要编写同样x64的程序来实现破解,这64位程序就有很多坑等着你
这部分比较简单,一行代码搞定
接下来按照前面的分析,应该准备好导出表相关的数据了
虽然我曾写过遍历的函数,我想大家肯定也有不少人写过这种东西,但是32位的不能直接拿来用
这里给出的实现代码,简单说一下坑的地方
第一个坑 遍历目标进程 与 遍历文件做选择,我最后选择了遍历文件
遍历目标进程映射后的PE格式是比较简单,但是我得写多少ReadProcessMem。。。。
那遍历文件也有坑啊,我全踩完了,首先读文件,读进来必定是64位地址,所以相关地方全换成64位值
第二个坑 遍历到的数据是FOA ,还得把FOA转RVA , RVA转FOA ,xxx转VA ,一顿乱七八糟的转换(好吧,还不如多写点ReadProcessMem)
第三个坑 遍历时一般用到循环,循环通过下标遍历就OK ,但PE格式很多数据都为32位值,程序又用到了64位的地址,且64位指针步进都为8 ,然后开始调开始改吧。。。
通过修改,调试,最后得到了需要的数据 :
目标函数的导出地址表的内容 RVA
目标函数的导出地址表的地址 FOA转RVA
目标函数的真实地址 是个FOA转RVA 之后再转VA
代码注释写起来比代码都复杂,就没写了,理清了关键内容就可以自己写出来
这个也是个巨坑啊,首先我选择使用遍历模块的函数,但都失败,最后经过研究得知,当你创建挂起进程的时候,由于程序没有运行,所以系统不会更新此程序在三环的快照信息(遍历的几个函数都是使用快照获取)
所以我这里给出两个办法
第一个, 使用Nt半公开函数 NtQueryInformationProcess ,这个函数也通常用来做反调试
第二个办法
获取线程环境,根据病毒分析经验,在创建进程挂起时,进程的ebx会指向PEB ,在64位程序中 rdx会指向PEB,而64位PEB结构中,PEB+0x10的偏移为进程基址,好的,这样就得到了Base
这里也有坑
根据前面的分析流程,现在应该hook导出函数地址表了,需要得到shellcode的地址,地址需要去对方进程申请
而导出表函数地址表中存放的函数的RVA ,且只有4字节,所以不得不考虑一个问题,如果申请到的地址是64位地址,大于FFFFFFFF则我们没法填入导出函数地址表(所以如果你有导出函数则进程不能大于4个G?微软这PE格式是不是该升级了)
这里我选择在基址附近申请一个内存,不超过4G即可,那么接下来该考虑hook的问题了
hook需要得到对方进程的VA ,前面说过了
最后要注意修改时,要调整内存保护属性
这个是难点,为了写出精致的shellcode,我甚至还得用汇编实现算法
另一个难点主要是Shellcode的编写需要依赖于逆向分析的结果,Shellcode以及注释在下边给出
这里64位程序不支持内联汇编,那么怎么写呢,两个办法 ,联合编译 与 使用机器码 , 这里我选择64位的联合编译 , 怎么实现联合编译呢 ,第一种办法如下, 第二种办法 写好汇编使用ml64汇编编译器生成obj文件,在C程序编译中链接此文件
新建一个asm后缀的文件,修改它的属性 , 从生成中排除 否 ,自定义生成工具
按照图中的配置即可
最后需要在C文件中,extern 汇编函数,即可愉快的使用联合编译
这里给出shellcode的汇编代码 注意不能使用绝对地址
好吧,经过一段时间的代码编写,我们的破解程序已经出炉了,调试的话,有兴趣的网友可以自行编写调试,Shellcode 中有两个简单实现的算法,strstr 与 挪动数据
然后我还为程序添加了图标与 隐藏控制台,算是比较不错的一个程序了,并且我写的代码具有一定的通用性,应该可以实现全版本通吃,但是我没有测试,可能还有一些细节需要完善,例如特征码的查找与关键调用的判断
我测试了最新的 1.25版本,没有问题,下面给出适合新手的PE注入实现
众所周知,PE中的text段就是代码段,我们可以在text段中找到一个空闲的位置,来把shellcode放在其中
那么需要解决几个问题
1.空闲位置的RVA填入导出函数地址表
2.shellcode代码的十六进制形式
3.空闲位置需要有足够的位置存放shellcode
4.随机基址,如果PE注入,那么在跳回原函数的时候,需要它的位置是固定的(当然,如果有足够的大小,可以让Shellcode自定位)
1.根据分析的4点,首先解决第4点,关闭PE的随机基址属性位 , 010editor打开 typora.exe (把文件属性的只读去掉)
010editor有PE格式解析脚本,找到选项头中的属性,把这一项改为0即可
2.找到空闲的text段位置,这里我找到了71B4A50的位置,大小刚好够存放我的shellcode
3.注入代码的位置的FOA转RVA
可以使用PE工具来计算,也可以自己编写程序计算 ,我使用看雪资源下载中的PE编辑器,可以直接下载,这里得到了RVA ,71B5650
4.在导出函数地址中修改为这个RVA
我在上面的程序中已经找到了这个位置,FOA为0x8602fc5 ,来到文件中的这个位置,写入shellcode 的RVA,小端序写入 071B5650
5.然后需要修改一下shellcode,并转换为机器码
把原函数地址写入shellcode,通过计算得出 1426FB7A0 为原函数地址(原函数RVA转VA)
调试查看一下,RVA修改成功,并且来到该地址,接下来把shellcode的十六进制复制到PE文件中即可
把Shellcode机器码从调试器中拷贝出来,放到自己破解的010editor中(新版有反汇编功能)
有一说一 ,还挺好用的,然后把原函数地址 填写到跳转的位置 , 这是我修改后的shellcode
好的,保存即可
1.内存破解程序为 Crack_Typora.exe ,把它放在Typora的目录,双击打开即可
2.PE注入破解程序为Typora_Patch.exe ,把它放在Typora的目录,双击打开即可
注:你还可以把它两发送到桌面快捷方式 更为方便快捷
每个人都有很多想法,但是真正去用行动实现的却只有少数人,而我,就是那少数人。
注:经过大哥的提醒,防止我的程序被恶意传播,所以已经取消了附件下载链接,望谅解
/
/
创建进程并挂起
if
(!CreateProcess(NULL, cmdline, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
{
printf(
"CreateProcess failed (%d)\n"
, GetLastError());
return
0
;
}
/
/
创建进程并挂起
if
(!CreateProcess(NULL, cmdline, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
{
printf(
"CreateProcess failed (%d)\n"
, GetLastError());
return
0
;
}
DWORD GetExportApiAddr(char
*
szPath, char
*
szApiName, DWORD64
*
ExportTableAddrRva)
{
/
/
映射文件到内存 找出需要的数据
HANDLE hFile
=
CreateFile(szPath, GENERIC_READ,
0
, NULL, OPEN_EXISTING,
0
, NULL);
HANDLE hMap
=
CreateFileMapping(hFile, NULL, PAGE_READONLY,
0
,
0
, NULL);
DWORD64 lpBuff
=
MapViewOfFile(hMap, FILE_MAP_READ,
0
,
0
, NULL);
PIMAGE_DOS_HEADER pDosHeader
=
(PIMAGE_DOS_HEADER)lpBuff;
PIMAGE_NT_HEADERS pNtHeader
=
(PIMAGE_NT_HEADERS)((DWORD64)lpBuff
+
pDosHeader
-
>e_lfanew);
PIMAGE_EXPORT_DIRECTORY pExportTable
=
lpBuff
+
RVA_TO_FOA(lpBuff, pNtHeader
-
>OptionalHeader.DataDirectory[
0
].VirtualAddress);
/
/
所有地址转为FOA
DWORD64
*
pAddressOfFunctions
=
(DWORD64
*
)RVA_TO_FOA(lpBuff, pExportTable
-
>AddressOfFunctions);
pAddressOfFunctions
=
lpBuff
+
(DWORD64)pAddressOfFunctions;
DWORD64
*
pAddressOfNames
=
(DWORD64
*
)RVA_TO_FOA(lpBuff, pExportTable
-
>AddressOfNames);
pAddressOfNames
=
lpBuff
+
(DWORD64)pAddressOfNames;
DWORD64
*
pAddressOfNameOrdinals
=
(DWORD64
*
)RVA_TO_FOA(lpBuff, pExportTable
-
>AddressOfNameOrdinals);
pAddressOfNameOrdinals
=
lpBuff
+
(DWORD64)pAddressOfNameOrdinals;
DWORD dwNumberOfNames
=
pExportTable
-
>NumberOfNames;
/
/
判断导出方式(序号
or
名称)
if
((DWORD)szApiName >>
16
=
=
0
)
{
DWORD Index
=
(DWORD64)szApiName
-
pExportTable
-
>Base;
if
(Index >
=
pExportTable
-
>NumberOfFunctions)
return
NULL;
return
(DWORD64)(lpBuff
+
pAddressOfFunctions[Index]);
}
else
{
DWORD64 pNameAddress;
DWORD64 FuncRva;
pNameAddress
=
(DWORD64)(lpBuff
+
RVA_TO_FOA(lpBuff, (DWORD)
*
pAddressOfNames));
for
(size_t i
=
0
; i < dwNumberOfNames; i
+
+
)
{
if
(strcmp(szApiName, (char
*
)pNameAddress)
=
=
0
)
{
FuncRva
=
(WORD)
*
pAddressOfNameOrdinals;
pAddressOfFunctions
=
(DWORD64)pAddressOfFunctions
+
FuncRva
*
4
;
FuncRva
=
(DWORD)(
*
pAddressOfFunctions);
*
ExportTableAddrRva
=
(DWORD64)pAddressOfFunctions
-
lpBuff;
*
ExportTableAddrRva
=
FOA_TO_RVA(lpBuff,
*
ExportTableAddrRva);
UnmapViewOfFile(lpBuff);
CloseHandle(hFile);
return
FuncRva;
}
pAddressOfNameOrdinals
=
(DWORD64)pAddressOfNameOrdinals
+
2
;
pAddressOfNames
=
(DWORD64)pAddressOfNames
+
4
;
pNameAddress
=
(DWORD64)(lpBuff
+
RVA_TO_FOA(lpBuff, (DWORD)
*
pAddressOfNames));
}
}
UnmapViewOfFile(lpBuff);
CloseHandle(hFile);
return
NULL;
}
DWORD GetExportApiAddr(char
*
szPath, char
*
szApiName, DWORD64
*
ExportTableAddrRva)
{
/
/
映射文件到内存 找出需要的数据
HANDLE hFile
=
CreateFile(szPath, GENERIC_READ,
0
, NULL, OPEN_EXISTING,
0
, NULL);
HANDLE hMap
=
CreateFileMapping(hFile, NULL, PAGE_READONLY,
0
,
0
, NULL);
DWORD64 lpBuff
=
MapViewOfFile(hMap, FILE_MAP_READ,
0
,
0
, NULL);
PIMAGE_DOS_HEADER pDosHeader
=
(PIMAGE_DOS_HEADER)lpBuff;
PIMAGE_NT_HEADERS pNtHeader
=
(PIMAGE_NT_HEADERS)((DWORD64)lpBuff
+
pDosHeader
-
>e_lfanew);
PIMAGE_EXPORT_DIRECTORY pExportTable
=
lpBuff
+
RVA_TO_FOA(lpBuff, pNtHeader
-
>OptionalHeader.DataDirectory[
0
].VirtualAddress);
/
/
所有地址转为FOA
DWORD64
*
pAddressOfFunctions
=
(DWORD64
*
)RVA_TO_FOA(lpBuff, pExportTable
-
>AddressOfFunctions);
pAddressOfFunctions
=
lpBuff
+
(DWORD64)pAddressOfFunctions;
DWORD64
*
pAddressOfNames
=
(DWORD64
*
)RVA_TO_FOA(lpBuff, pExportTable
-
>AddressOfNames);
pAddressOfNames
=
lpBuff
+
(DWORD64)pAddressOfNames;
DWORD64
*
pAddressOfNameOrdinals
=
(DWORD64
*
)RVA_TO_FOA(lpBuff, pExportTable
-
>AddressOfNameOrdinals);
pAddressOfNameOrdinals
=
lpBuff
+
(DWORD64)pAddressOfNameOrdinals;
DWORD dwNumberOfNames
=
pExportTable
-
>NumberOfNames;
/
/
判断导出方式(序号
or
名称)
if
((DWORD)szApiName >>
16
=
=
0
)
{
DWORD Index
=
(DWORD64)szApiName
-
pExportTable
-
>Base;
if
(Index >
=
pExportTable
-
>NumberOfFunctions)
return
NULL;
return
(DWORD64)(lpBuff
+
pAddressOfFunctions[Index]);
}
else
{
DWORD64 pNameAddress;
DWORD64 FuncRva;
pNameAddress
=
(DWORD64)(lpBuff
+
RVA_TO_FOA(lpBuff, (DWORD)
*
pAddressOfNames));
for
(size_t i
=
0
; i < dwNumberOfNames; i
+
+
)
{
if
(strcmp(szApiName, (char
*
)pNameAddress)
=
=
0
)
{
FuncRva
=
(WORD)
*
pAddressOfNameOrdinals;
pAddressOfFunctions
=
(DWORD64)pAddressOfFunctions
+
FuncRva
*
4
;
FuncRva
=
(DWORD)(
*
pAddressOfFunctions);
*
ExportTableAddrRva
=
(DWORD64)pAddressOfFunctions
-
lpBuff;
*
ExportTableAddrRva
=
FOA_TO_RVA(lpBuff,
*
ExportTableAddrRva);
UnmapViewOfFile(lpBuff);
CloseHandle(hFile);
return
FuncRva;
}
pAddressOfNameOrdinals
=
(DWORD64)pAddressOfNameOrdinals
+
2
;
pAddressOfNames
=
(DWORD64)pAddressOfNames
+
4
;
pNameAddress
=
(DWORD64)(lpBuff
+
RVA_TO_FOA(lpBuff, (DWORD)
*
pAddressOfNames));
}
}
UnmapViewOfFile(lpBuff);
CloseHandle(hFile);
return
NULL;
}
/
/
第二个参数是ProcessBasicInformation的话,则第三个参数必须为一个指针指向结构PROCESS_BASIC_INFORMATION
/
/
可以获取PEB , 通过PEB获取基址
/
/
loadlibrary getprocaddress 获取函数地址
__kernel_entry NTSTATUS NtQueryInformationProcess(
[
in
] HANDLE ProcessHandle,
[
in
] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[
in
] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
typedef enum _PROCESSINFOCLASS {
ProcessBasicInformation
=
0
,
ProcessDebugPort
=
7
,
/
/
调式端口
ProcessWow64Information
=
26
,
ProcessImageFileName
=
27
,
ProcessBreakOnTermination
=
29
ProcessDebugObjectHandle
=
30
,
/
/
获取调试对象句柄,句柄为空表示未调试
ProcessDebugFlags
=
31
/
/
检测调试标志位为
0
表示处于调试状态
} PROCESSINFOCLASS;
typedef struct
{
DWORD ExitStatus;
/
/
接收进程终止状态
DWORD PebBaseAddress;
/
/
接收进程环境块地址 PEB
DWORD AffinityMask;
/
/
接收进程关联掩码
DWORD BasePriority;
/
/
接收进程的优先级类
ULONG UniqueProcessId;
/
/
接收进程
ID
ULONG InheritedFromUniqueProcessId;
/
/
接收父进程
ID
} PROCESS_BASIC_INFORMATION;
/
/
第二个参数是ProcessBasicInformation的话,则第三个参数必须为一个指针指向结构PROCESS_BASIC_INFORMATION
/
/
可以获取PEB , 通过PEB获取基址
/
/
loadlibrary getprocaddress 获取函数地址
__kernel_entry NTSTATUS NtQueryInformationProcess(
[
in
] HANDLE ProcessHandle,
[
in
] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[
in
] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
typedef enum _PROCESSINFOCLASS {
ProcessBasicInformation
=
0
,
ProcessDebugPort
=
7
,
/
/
调式端口
ProcessWow64Information
=
26
,
ProcessImageFileName
=
27
,
ProcessBreakOnTermination
=
29
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-5-10 14:51
被yumoqaq编辑
,原因: