前置知识点
在学习本篇文章的时候,需要掌握以下知识点:
- PE结构相关知识——>掌握程度:可以自己随便移动相关节表结构而使PE文件不受影响。
- python代码相关基础——>掌握程度:可以根据一些文档自己编写简单的python脚本。
- C和C++语言——>掌握程度:对指针有基本的了解,对一些Windows API有一些基本的使用能力。
- 对IDA Pro、X64 dbg、windbg三款调试软件有一些基本的使用能力。
- 对汇编语言有简单的编写能力。
- 对代码有基本的调试、排错能力。
- 一些其他的简单能力
废话不多说了,直接开整。
背景介绍
最近在学习病毒分析知识,因此朋友发了一款病毒过来,让我练练手。如图:
拿到样本后,根据病毒分析的步骤,首先通过PE工具查看样本病毒信息:
通过DIE工具,我们可以得到以下的几个关键信息:
- 该病毒样本是x64位程序
- 该病毒样本是通过VS2022编译器编译的
- 该病毒样本以被加壳,壳为:Themida/Winlicense(3.XX)[Winlicense],也就是TMD壳,一种强壳。
因为本篇不涉及病毒分析,只涉及如何手动脱壳,因此只介绍脱壳。
因为我们的目的是学习,所以也不用在网上找相关的脱壳机去一键脱壳,因此我们的思路就仅仅局限于,如何手动的去脱壳。
关于手脱壳的思路及步骤
什么是壳?
首先我们需要了解什么是壳,通过百度,我们找到了以下这篇文章(只放截图):
加壳程序:
也就是,正常的壳不管强度如何,最后肯定会对源程序的代码进行解密,解密之后,就正常执行源程序的代码。
那么,我们只需要定位到加壳程序在内存中被解密并调用的入口点即可,因为此时的内存中,源程序是并没有被加密的。
思路有了,但是问题来了,该如何定位并找到源程序的入口点呢?
掌握VS2022编译的特征
想要找到被加壳的程序的入口点,那么就必须要掌握被加壳程序所用编译器编译生成文件的特征。
此病毒程序是通过VS2022编译的x64程序,那么我们在本地编译一个简单的x64程序,来分析一下。
#include <iostream>
int main()
{
printf("Hello\n");
}
很简单的一个程序,选择x64 release版本进行编译,编译好之后,放在IDA Pro中,进行分析:
这里就是我们的main函数,但是是谁调用的main函数呢?我们继续往上跟,
发现是__scrt_common_main_seh
函数里调用了main,同时在调用mian之前,获得了命令行参数和数量,也就是类似于GetCommandLine
函数。
继续往上跟:
start
函数里面,首先调用了_security_init_cookie()
函数,接着又调用了__scrt_common_main_seh()
函数,我们再看看_security_init_cookie()
函数:
可以看到,有一个常数:0x2B992DDFA232
。
我们在看看start()
函数的特征:
发现是一个call和一个jmp指令。
因此,通过上述简单分析一个VS编译的程序,我们得到了以下这些特征信息:
1.入口点为一个: sub rsp,28
call add rsp,28
jmp
2.call的是_security_init_cookie()
函数
3._security_init_cookie()
函数中,有一个常量:0x2B992DDFA232
4.jmp的是一个__scrt_common_main_seh()
函数
5.疑似用到了GetCommandLine函数。
脱壳
定位OEP
根据上述拿到的特征码,我们可以通过不同的方法进行测试,在这里,为了节省时间,就不一一去验证了。
直接给GetCommandLineA函数进行下断点。
命令:bp GetCommandLineA
来到第一个断点:
根据堆栈入栈的规律,找到第一个调用GetCommand函数的返回地址的下一个地址:0x00007FF611889402
在该地址处搜索上述总结的特征,如常数:0x2B992DDFA232
没有找到,继续往下跟:
来到第二个断点处,
重复上述操作,搜索相关特征:
找到,跟进去看看:
与_security_init_cookie()
函数作对比看看是否一致:
发现基本一直,也就是我们找到了_security_init_cookie()
函数的入口点。地址为0x00007FF611318D44
我们从该地址处,在上下汇编指令中查找是否有start()
函数特征,发现并没有,这说明什么?
这说明,入口点被吞了,那么在后面我们需要自己构造一个call _security_init_cookie()和jmp __scrt_common_main_seh()
的入口点。
现在我们已经掌握了_security_init_cookie()的入口点,即call 0x00007FF611318D44
,那么现在我们需要找到__scrt_common_main_seh()
的入口点,
回到GetCommandLineA断点处:
往下跟:
第一个ret:
继续执行:
第二个ret:
继续执行:
第三个ret:
继续执行,到该处:
通过对比__scrt_common_main_seh()
的反汇编伪代码与该处,发现基本一致。
因此,这个地方就是我们要找的__scrt_common_main_seh()
函数的入口点,地址为:0x00007FF6113185F0
因此,我们已经定位到__scrt_common_main_seh()
函数的入口点和_security_init_cookie()
函数的入口点。
根据我们的分析入口点被吞,因此后续我们需要构造一个call和jmp:
完整指令如下:
call 0x00007FF611318D4
jmp 0x00007FF6113185F0
dump内存
根据我们找到的信息,给_security_init_cookie()
函数的入口点下硬件断点:
重新运行至硬件断点处:
在这里,说明目前内存中所有的壳都已经被解密了,因此,我们可以执行dump了。
修复IAT
dump下来之后,我们需要考虑的是该dump的内存和exe,能否运行?
懂PE知识的都知道,肯定不可以运行!
因此我们dump的虽然都是解密后的内存和exe,但是此时在内存中IAT表都已经被拉伸和填充了,这里的过程我就不在解释了,因为前面的前置知识已经说了,要懂PE知识。
因此,我们需要修复IAT表。
但是这里有一个难点,难点就是该程序的IAT被混淆过了:
找到IAT表所在的地方:
选中一个间接call的地址,按空格键:
复制该地址,在内存窗口中转到改地址:
根据导入表的知识,根据该处地址,找到IAT表的起始位置和结束位置:
起始位置:
结束位置:
而我们程序和系统领空分别为:
0x00007FF611310000
、0x00007FF96D9A0000
,也就是说:0x00007FF6是程序领空,而0x00007FF9及其以后,是系统领空,而dll的函数地址都存在于系统领空。
但是,根据我们的分析,可以看到:
有的地址值是系统领空,在没有0结束符的时候,竟然还出现了程序领空的值,这就说明了,IAT表是被混淆过的。
那么该如何修复呢?
编写python脚本
在这里,介绍一篇文章:https://bbs.kanxue.com/thread-253868.htm,根据该文章,我们可以通过unicorn库和Capstone库,编写虚拟机和编写hook。通过直接修改执行地址的方式,通过判断RSP是否在系统领空来判断该IAT表中的值是否被混淆过的。
为节约篇幅,具体代码就不放了,只放关键的:
只需要判断该地址是否超过了程序地址空间最大的地址即可(当然,如果混淆的特别厉害,可以将地址细化一下)。
跑出的值:
编写DLL
思路
我们拿到了相关的dll和相关的dll中函数地址,那么我们接下来,要考虑的是如何修复。
修复思路有以下几种:
- 静态修复
- 动态修复
经过思考,静态修复并不可行,因为程序中可能使用的一些dll,系统并不会加载。
如:可能程序中使用的是LoadLibrary的方式加载的。
因此,为了更好的修复,只能通过动态修复。
那么动态该如何修复呢?这里我们推荐dll注入的方式。
而dll注入的方式修复,我们也可以选择是将内存中已经拉伸的PE文件解拉伸保存还是直接dump一个exe,根据内存中的数据对照着修复呢?
选择权在大家。
这里我选择的是dump一个exe,对照着修复。
如何对照着修复呢?
这里的思路是:
- 新建一个节表
- 新的节表用来存储新的导入表
- 导入表指向的THUNK_DATA不能变,病毒程序中是0x23000,那么导入表的
FirstThunk
和OriginalFirstThunk
指向的位置还得是0x23000。
为什么呢?
因为我们不论用哪种方法,程序内部的间接call都是使用的是,如:call [0x23000]。
如何遍历一个程序的所有dll?
在这里,我们需要了解PEB的知识。这里也要求我们会windbg调试器的使用。
这里就不带领大家一个个手动去定位查找了,但是在编写代码的时候,需要跟进windbg对照着去定位的。
在x64程序中gs:[60]执行PEB地址。而PEB结构便宜0x18的地址是_PEB_LDR_DATA
结构,该地址中的三个链表指向的结构为:_LDR_DATA_TABLE_ENTRY
结构,如下:
0:000> !peb
PEB at 000007fffffd3000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: Yes
ImageBaseAddress: 000000013f170000
Ldr 0000000077092e40
Ldr.Initialized: Yes
Ldr.InInitializationOrderModuleList: 0000000000443430 . 0000000000446ae0
Ldr.InLoadOrderModuleList: 0000000000443300 . 0000000000446ac0
Ldr.InMemoryOrderModuleList: 0000000000443310 . 0000000000446ad0
0:000> dt _PEB_LDR_DATA 0x00000000`77092e40
ntdll!_PEB_LDR_DATA
+0x000 Length : 0x58
+0x004 Initialized : 0x1 ''
+0x008 SsHandle : (null)
+0x010 InLoadOrderModuleList : _LIST_ENTRY [ 0x00000000`00443300 - 0x00000000`00446ac0 ]
+0x020 InMemoryOrderModuleList : _LIST_ENTRY [ 0x00000000`00443310 - 0x00000000`00446ad0 ]
+0x030 InInitializationOrderModuleList : _LIST_ENTRY [ 0x00000000`00443430 - 0x00000000`00446ae0 ]
+0x040 EntryInProgress : (null)
+0x048 ShutdownInProgress : 0 ''
+0x050 ShutdownThreadId : (null)
0:000> dt _LDR_DATA_TABLE_ENTRY 0x00000000`00443300
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000000`00443410 - 0x00000000`77092e50 ]
+0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000000`00443420 - 0x00000000`77092e60 ]
+0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
+0x030 DllBase : 0x00000001`3f170000 Void
+0x038 EntryPoint : 0x00000001`3f1713d0 Void
+0x040 SizeOfImage : 0x7000
+0x048 FullDllName : _UNICODE_STRING "C:\MyProject\VS2019\Test\ConsoleApplication1\x64\Release\ConsoleApplication1.exe"
+0x058 BaseDllName : _UNICODE_STRING "ConsoleApplication1.exe"
遍历所有以加载的模块:
编写FOA转RVA和RVA转FOA函数
定义NT_HEADER和SECTION_HEADER宏
新建节表
遍历修复:
for (const auto& pair : addressMap) {
pImpDesc->Name = (DWORD)Raw2Rav(newFileBuf, pNewLastSectionHeader->PointerToRawData + 0x700 + i);
memcpy((PVOID)(newFileBuf + pNewLastSectionHeader->PointerToRawData + 0x700 + i), pair.first.c_str(), pair.first.length() + 1);
i = i + pair.first.length() + 1;
//拿到相关dll的地址:
PVOID hSysDllBaseImage = GetModuleHandleA(pair.first.c_str());
if (NULL != hSysDllBaseImage)
{
//定位到该dll的导出表
DWORD64 pExpRva = (getNtHeader(newFileBuf)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY pExpDataDir = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)hSysDllBaseImage + pExpRva);
PDWORD pNameTable = (PDWORD)((DWORD_PTR)hSysDllBaseImage + pExpDataDir->AddressOfNames);
PWORD pOrdinalTable = (PWORD)((DWORD_PTR)hSysDllBaseImage + pExpDataDir->AddressOfNameOrdinals);
PDWORD pFunctionTable = (PDWORD)((DWORD_PTR)hSysDllBaseImage + pExpDataDir->AddressOfFunctions);
for (DWORD i = 0; i < pExpDataDir->NumberOfFunctions; i++)
{
char* pszFunctionName = (char*)((DWORD64)hSysDllBaseImage + pNameTable[i]);
std::string funcName_str(pszFunctionName);
DWORD64 addr = ((DWORD64)hSysDllBaseImage + pFunctionTable[pOrdinalTable[i]]);
FuncMap[addr] = new FunctionInfo(pair.first, funcName_str, pOrdinalTable[i]);
}
for (const auto& info : pair.second) {
FunctionInfo* dist = FuncMap[info.RVAaddr];
iatFuncMap[info.FOAaddr] = dist;
}
// 使用迭代器遍历 map
auto prev_it = iatFuncMap.begin(); // 初始设定为开始,但不用于第一次比较
PDWORD64 dwTempAddr;
for (auto it = iatFuncMap.begin(); it != iatFuncMap.end(); ++it) {
if (it != iatFuncMap.begin()) { // 确保这不是第一次迭代
if ((it->first - prev_it->first) > 8)
{
pImpDesc++;
pImpDesc->FirstThunk = it->first;
pImpDesc->Name = (DWORD)Raw2Rav(newFileBuf, pNewLastSectionHeader->PointerToRawData + 0x700 + i);
memcpy((PVOID)(newFileBuf + pNewLastSectionHeader->PointerToRawData + 0x700 + i), pair.first.c_str(), pair.first.length() + 1);
i = i + pair.first.length() + 1;
dwTempAddr = (PDWORD64)(Rav2Raw(newFileBuf, it->first) + newFileBuf);
*dwTempAddr = Raw2Rav(newFileBuf, ((DWORD64)pImportByName - (DWORD64)newFileBuf));
memcpy(pImportByName->Name, it->second->funcname.c_str(), it->second->funcname.length() + 1);
pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD64)pImportByName + 2 + it->second->funcname.length() + 1);
}
else
{
pImpDesc->FirstThunk = it->first;
dwTempAddr = (PDWORD64)(Rav2Raw(newFileBuf, it->first) + newFileBuf);
*dwTempAddr = Raw2Rav(newFileBuf, ((DWORD64)pImportByName - (DWORD64)newFileBuf));
memcpy(pImportByName->Name, it->second->funcname.c_str(), it->second->funcname.length() + 1);
pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD64)pImportByName + 2 + it->second->funcname.length() + 1);
pImpDesc++;
}
}
else
{
pImpDesc->FirstThunk = it->first;
dwTempAddr = (PDWORD64)(Rav2Raw(newFileBuf, it->first) + newFileBuf);
*dwTempAddr = Raw2Rav(newFileBuf, ((DWORD64)pImportByName - (DWORD64)newFileBuf));
memcpy(pImportByName->Name, it->second->funcname.c_str(), it->second->funcname.length() + 1);
pImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD64)pImportByName + 2 + it->second->funcname.length() + 1);
pImpDesc++;
}
prev_it = it; // 更新上一个元素的迭代器
}
}
}
以上代码可能有问题,因为我测试好了的代码被我用虚拟机快照复原删除了!!!
上面这些都是我凭借记忆重新写的,没有经过测试。。。
这里我也建议大家仅供参考,自己动手编写,这样才能提高!!!
请登录后查看评论内容