逆向-手拖TMD壳

前置知识点

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

  1. PE结构相关知识——>掌握程度:可以自己随便移动相关节表结构而使PE文件不受影响。
  2. python代码相关基础——>掌握程度:可以根据一些文档自己编写简单的python脚本。
  3. C和C++语言——>掌握程度:对指针有基本的了解,对一些Windows API有一些基本的使用能力。
  4. 对IDA Pro、X64 dbg、windbg三款调试软件有一些基本的使用能力。
  5. 对汇编语言有简单的编写能力。
  6. 对代码有基本的调试、排错能力。
  7. 一些其他的简单能力

 

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

背景介绍

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

f3ccdd27d220240421145400

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

d2b5ca33bd20240421145423

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

  1. 该病毒样本是x64位程序
  2. 该病毒样本是通过VS2022编译器编译的
  3. 该病毒样本以被加壳,壳为:Themida/Winlicense(3.XX)[Winlicense],也就是TMD壳,一种强壳。

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

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

关于手脱壳的思路及步骤

什么是壳?

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

d2b5ca33bd20240421145500

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

d2b5ca33bd20240421145521

加壳程序:

d2b5ca33bd20240421145534

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

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

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

掌握VS2022编译的特征

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

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

#include <iostream>

int main()
{
	printf("Hello\n");
}

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

d2b5ca33bd20240421145612

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

d2b5ca33bd20240421145638

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

继续往上跟:

d2b5ca33bd20240421145654

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

d2b5ca33bd20240421145712

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

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

d2b5ca33bd20240421145727

d2b5ca33bd20240421145742

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

d2b5ca33bd20240421145828

来到第一个断点:

d2b5ca33bd20240421145846

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

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

d2b5ca33bd20240421145904

没有找到,继续往下跟:

来到第二个断点处,

d2b5ca33bd20240421145923

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

d2b5ca33bd20240421145936

找到,跟进去看看:

d2b5ca33bd20240421145949

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

d2b5ca33bd20240421150005

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

d2b5ca33bd20240421150026

继续执行:

第二个ret:

d2b5ca33bd20240421150043

继续执行:

第三个ret:

d2b5ca33bd20240421150055

继续执行,到该处:

d2b5ca33bd20240421150113

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

d2b5ca33bd20240421150133

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

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

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

完整指令如下:

call 0x00007FF611318D4
jmp 0x00007FF6113185F0

dump内存

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

d2b5ca33bd20240421150200

重新运行至硬件断点处:

d2b5ca33bd20240421150215

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

d2b5ca33bd20240421150231

修复IAT

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

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

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

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

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

找到IAT表所在的地方:

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

d2b5ca33bd20240421150244

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

d2b5ca33bd20240421150257

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

起始位置:

d2b5ca33bd20240421150311

结束位置:

d2b5ca33bd20240421150322

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

d2b5ca33bd20240421150401

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

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

d2b5ca33bd20240421150423

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

那么该如何修复呢?

编写python脚本

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

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

d2b5ca33bd20240421150501

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

跑出的值:

d2b5ca33bd20240421150525

编写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:\MyProject\VS2019\Test\ConsoleApplication1\x64\Release\ConsoleApplication1.exe"
   +0x058 BaseDllName      : _UNICODE_STRING "ConsoleApplication1.exe"

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

dll关键代码为:

找到_LDR_DATA_TABLE_ENTRY结构位置:

d2b5ca33bd20240421150611

遍历所有以加载的模块:

d2b5ca33bd20240421150625

编写FOA转RVA和RVA转FOA函数

d2b5ca33bd20240421150641

定义NT_HEADER和SECTION_HEADER宏

d2b5ca33bd20240421150657

新建节表

d2b5ca33bd20240421150711

遍历修复:

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

	}

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

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

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

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 共3条

请登录后发表评论

    请登录后查看评论内容