手脱TMD壳

admin 2024年4月22日02:11:51评论7 views字数 8454阅读28分10秒阅读模式

郑重声明:

首先,本人对脱壳是没有一点思路的,因此,非常感谢晓师傅的帮助和教导,帮我梳理了一遍脱壳的思路和知识点。让我对一些壳有了认识,也才有了这篇文章。在此,再次感谢晓师傅的帮助!!!

前置知识点

在学习本篇文章的时候,需要掌握以下知识点:

  1. PE结构相关知识------>掌握程度:可以自己随便移动相关节表结构而使PE文件不受影响。

  2. python代码相关基础------>掌握程度:可以根据一些文档自己编写简单的python脚本。

  3. C和C++语言------>掌握程度:对指针有基本的了解,对一些Windows API有一些基本的使用能力。

  4. 对IDA Pro、X64 dbg、windbg三款调试软件有一些基本的使用能力。

  5. 对汇编语言有简单的编写能力。

  6. 对代码有基本的调试、排错能力。

  7. 一些其他的简单能力

废话不多说了,直接开整。

背景介绍

最近在学习病毒分析知识,因此朋友发了一款病毒过来,让我练练手。如图:

手脱TMD壳

拿到样本后,根据病毒分析的步骤,首先通过PE工具查看样本病毒信息:

手脱TMD壳

通过DIE工具,我们可以得到以下的几个关键信息:

  1. 该病毒样本是x64位程序

  2. 该病毒样本是通过VS2022编译器编译的

  3. 该病毒样本以被加壳,壳为:Themida/Winlicense(3.XX)[Winlicense],也就是TMD壳,一种强壳。

因为本篇不涉及病毒分析,只涉及如何手动脱壳,因此只介绍脱壳。

因为我们的目的是学习,所以也不用在网上找相关的脱壳机去一键脱壳,因此我们的思路就仅仅局限于,如何手动的去脱壳。

关于手脱壳的思路及步骤

什么是壳?

首先我们需要了解什么是壳,通过百度,我们找到了以下这篇文章(只放截图):

手脱TMD壳

根据文章介绍,并加之自己的理解,也就是壳是一种加密手段,把原本的程序给加密,并且篡改了程序执行流程。以下是我根据自己的理解画得图:手脱TMD壳

加壳程序:

手脱TMD壳

也就是,正常的壳不管强度如何,最后肯定会对源程序的代码进行解密,解密之后,就正常执行源程序的代码。

那么,我们只需要定位到加壳程序在内存中被解密并调用的入口点即可,因为此时的内存中,源程序是并没有被加密的。

思路有了,但是问题来了,该如何定位并找到源程序的入口点呢?

掌握VS2022编译的特征

想要找到被加壳的程序的入口点,那么就必须要掌握被加壳程序所用编译器编译生成文件的特征。

此病毒程序是通过VS2022编译的x64程序,那么我们在本地编译一个简单的x64程序,来分析一下。

#include <iostream>

int main()
{
printf("Hellon");
}

很简单的一个程序,选择x64 release版本进行编译,编译好之后,放在IDA Pro中,进行分析:

手脱TMD壳

这里就是我们的main函数,但是是谁调用的main函数呢?我们继续往上跟,

手脱TMD壳

发现是__scrt_common_main_seh函数里调用了main,同时在调用mian之前,获得了命令行参数和数量,也就是类似于GetCommandLine函数。

继续往上跟:

手脱TMD壳

start函数里面,首先调用了_security_init_cookie()函数,接着又调用了__scrt_common_main_seh()函数,我们再看看_security_init_cookie()函数:

手脱TMD壳

可以看到,有一个常数:0x2B992DDFA232

我们在看看start()函数的特征:

手脱TMD壳

手脱TMD壳

发现是一个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

手脱TMD壳

来到第一个断点:

手脱TMD壳

根据堆栈入栈的规律,找到第一个调用GetCommand函数的返回地址的下一个地址:0x00007FF611889402

在该地址处搜索上述总结的特征,如常数:0x2B992DDFA232

手脱TMD壳

没有找到,继续往下跟:

来到第二个断点处,

手脱TMD壳

重复上述操作,搜索相关特征:

手脱TMD壳

找到,跟进去看看:

手脱TMD壳

_security_init_cookie()函数作对比看看是否一致:

手脱TMD壳

发现基本一直,也就是我们找到了_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:手脱TMD壳

继续执行:

第二个ret:

手脱TMD壳

继续执行:

第三个ret:

手脱TMD壳

继续执行,到该处:

手脱TMD壳

通过对比__scrt_common_main_seh()的反汇编伪代码与该处,发现基本一致。

手脱TMD壳

因此,这个地方就是我们要找的__scrt_common_main_seh()函数的入口点,地址为:0x00007FF6113185F0

因此,我们已经定位到__scrt_common_main_seh()函数的入口点和_security_init_cookie()函数的入口点。

根据我们的分析入口点被吞,因此后续我们需要构造一个call和jmp:

完整指令如下:

call 0x00007FF611318D4
jmp 0x00007FF6113185F0

dump内存

根据我们找到的信息,给_security_init_cookie()函数的入口点下硬件断点:

手脱TMD壳

重新运行至硬件断点处:

手脱TMD壳

在这里,说明目前内存中所有的壳都已经被解密了,因此,我们可以执行dump了。

手脱TMD壳

修复IAT

dump下来之后,我们需要考虑的是该dump的内存和exe,能否运行?

懂PE知识的都知道,肯定不可以运行!

因此我们dump的虽然都是解密后的内存和exe,但是此时在内存中IAT表都已经被拉伸和填充了,这里的过程我就不在解释了,因为前面的前置知识已经说了,要懂PE知识。

因此,我们需要修复IAT表。

但是这里有一个难点,难点就是该程序的IAT被混淆过了:

找到IAT表所在的地方:

选中一个间接call的地址,按空格键:

手脱TMD壳

复制该地址,在内存窗口中转到改地址:

手脱TMD壳

根据导入表的知识,根据该处地址,找到IAT表的起始位置和结束位置:

起始位置:

手脱TMD壳

结束位置:

手脱TMD壳

而我们程序和系统领空分别为:

手脱TMD壳

0x00007FF6113100000x00007FF96D9A0000,也就是说:0x00007FF6是程序领空,而0x00007FF9及其以后,是系统领空,而dll的函数地址都存在于系统领空。

但是,根据我们的分析,可以看到:

手脱TMD壳

有的地址值是系统领空,在没有0结束符的时候,竟然还出现了程序领空的值,这就说明了,IAT表是被混淆过的。

那么该如何修复呢?

编写python脚本

在这里,介绍一篇文章:https://bbs.kanxue.com/thread-253868.htm,根据该文章,我们可以通过unicorn库和Capstone库,编写虚拟机和编写hook。通过直接修改执行地址的方式,通过判断RSP是否在系统领空来判断该IAT表中的值是否被混淆过的。

为节约篇幅,具体代码就不放了,只放关键的:

手脱TMD壳

只需要判断该地址是否超过了程序地址空间最大的地址即可(当然,如果混淆的特别厉害,可以将地址细化一下)。

跑出的值:

手脱TMD壳

编写DLL

思路

我们拿到了相关的dll和相关的dll中函数地址,那么我们接下来,要考虑的是如何修复。

修复思路有以下几种:

  1. 静态修复

  2. 动态修复

经过思考,静态修复并不可行,因为程序中可能使用的一些dll,系统并不会加载。

如:可能程序中使用的是LoadLibrary的方式加载的。

因此,为了更好的修复,只能通过动态修复。

那么动态该如何修复呢?这里我们推荐dll注入的方式。

而dll注入的方式修复,我们也可以选择是将内存中已经拉伸的PE文件解拉伸保存还是直接dump一个exe,根据内存中的数据对照着修复呢?

选择权在大家。

这里我选择的是dump一个exe,对照着修复。

如何对照着修复呢?

这里的思路是:

  1. 新建一个节表

  2. 新的节表用来存储新的导入表

  3. 导入表指向的THUNK_DATA不能变,病毒程序中是0x23000,那么导入表的FirstThunkOriginalFirstThunk指向的位置还得是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:MyProjectVS2019TestConsoleApplication1x64ReleaseConsoleApplication1.exe"
+0x058 BaseDllName : _UNICODE_STRING "ConsoleApplication1.exe"

因此在代码中,需要定义以上一些结构体,这里推荐一个网站:http://s.ntoskr.com/

dll关键代码为:

找到_LDR_DATA_TABLE_ENTRY结构位置:

手脱TMD壳

遍历所有以加载的模块:

手脱TMD壳

编写FOA转RVA和RVA转FOA函数

手脱TMD壳

定义NT_HEADER和SECTION_HEADER宏

手脱TMD壳

新建节表

手脱TMD壳

遍历修复:

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; // 更新上一个元素的迭代器
}
}

}

以上代码可能有问题,因为我测试好了的代码被我用虚拟机快照复原删除了!!!

上面这些都是我凭借记忆重新写的,没有经过测试。。。

这里我也建议大家仅供参考,自己动手编写,这样才能提高!!!

原文始发于微信公众号(loochSec):手脱TMD壳

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月22日02:11:51
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   手脱TMD壳https://cn-sec.com/archives/2675839.html

发表评论

匿名网友 填写信息