写壳笔记

写壳笔记

完整的项目代码:
https://wws.lanzous.com/iiLa7nlxqle
密码:9ygk

前置知识:

  1. PE结构
  2. C++编程
  3. TEB结构

写壳的几个步骤

  1. 编写加壳器,实现在目标程序添加一个区段

  2. 在DLL项目中,编写壳代码,导出壳代码start函数,该函数执行我们对exe的加载及修复重定位,IAT等操作

  3. 将DLL区段合并到text区段,拷贝到目标程序新建的区段

  4. 设置OEP到壳代码起始位置

1.创建两个项目

加壳器程序(exe)

用于加载原程序后进行加壳工作

  • 编译环境 Debug x86 (方便调试)
  • 设置项目属性
    • C/C++ -代码生成-运行库 : 多线程调试DLL(MDd)
    • C/C++ -常规-支持仅我的代码调式 :
    • 右键项目 -生成依赖项-项目依赖项 : 勾选stub (重新编译的时候会同时重新编译stub)

stub程序 (DLL)

用于执行壳代码

  • 编译环境 Release x86 (优化一些系统API)
  • 设置项目属性
    • C/C++ -常规-支持仅我的代码调式 :
    • C/C++ -代码生成-安全检查(GS):禁用
    • C/C++ -代码生成-运行库 : 多线程L(MT)
  • 前置代码
    • 合并区段 --因为会用到全局变量,常量,而这些都在.data段,所以为了避免代码复杂度过高,合并到.text段,在stub.cpp文件加以下代码
#pragma comment(linker, "/merge:.data=.text")	//合并区段
#pragma comment(linker, "/merge:.rdata=.text")	//合并区段 			
#pragma comment(linker, "/section:.text,RWE")   //设置区段属性 			

2.编写加壳器

加壳器需要做的事情:

  1. 将宿主程序以文件形式读取到内存
  2. 将stub以LoadLibrary方式动态加载到内存,不执行DLLmian (设置标志DONT_RESOLVE_DLL_REFERENCES)
  3. 在宿主程序新建区段.pack区段, 拷贝壳代码.text区段到宿主程序.pack区段
  4. 修复壳代码全局变量基址
  5. 处理TLS,加密区段,修复重定位,压缩等
  6. 设置新的OEP
  7. 另存为新的EXE

3.编写stub程序( 壳代码)

stub程序需要做的事情:

  1. 合并区段.rdata 、.data 到.text区段
  2. 导出一个起始函数,这里取名为start
  3. 动态获取API
  4. 解密IAT,修复宿主程序重定位,解压代码段等
  5. 可进行反调试,反虚拟机等操作
  6. 跳转到宿主程序OEP

拷贝区段到宿主程序如何运行

  • 在这里会遇到一个问题,如果壳代码中有定义全局变量或staic变量,直接运行后会出现崩溃

    原因是所有涉及全局变量的代码,都是以 dll 和.text区段 为基址进行计算的

    例如: 模块的地址为 0x60000000,那么所有的地址都是以它开头的

    但是现在区段被拷贝到了 exe 文件中,exe 文件的默认基址是 0x400000
    如果使用前面 0x60000000 为基址的代码或数据继续运行,几乎都会出现错误
    所以需要进行重定位的修复,修复的公式为 :

    需要重定位的地址 - dll基址 - dll.text基址 + exe基址 + exe.pack的基址

  • 注意: 以LoadLibrary方式进行动态加载DLL ,并设置标志DONT_RESOLVE_DLL_REFERENCES ,使其不会执行DLLmain

/* 加壳器程序代码 */
VOID CPack::FixReloc()
{
	/*
		1. 找到 dll 的重定位表,遍历其中的重定位块,以全 0 结构结尾
		2. 解析重定位块,找到所有 Type 为 3 的项,使用上面的公式重定位
		3. 是否支持随机基址,不支持需要关闭随机基址
	*/
	typedef struct _TYPEOFFSET
	{
		WORD offset : 12;
		WORD type : 4;
	} TYPEOFFSET, * PTYPEOFFSET;

	//1. 找到 dll 的重定位表,遍历其中的重定位块,以全 0 结构结尾
	auto reloc_table = (PIMAGE_BASE_RELOCATION)(OptionalHeader(m_StubBase)->DataDirectory[5].VirtualAddress + m_StubBase);

	ULONG dllbase = m_StubBase;
	ULONG dlltext = GetSection(m_StubBase, ".text")->VirtualAddress;
	ULONG exebase = OptionalHeader(m_FileBase)->ImageBase;
	ULONG exepack = GetSection(m_FileBase,newSectionName)->VirtualAddress;

	while (reloc_table->SizeOfBlock)
	{
		auto item = (PTYPEOFFSET)(reloc_table + 1); //获得重定位项
		ULONG count = (reloc_table->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2; //重定位数量
        
		for (ULONG i = 0; i < count; i++)
		{
			//2. 解析重定位块,找到所有 Type 为 3 的项,使用上面的公式重定位
			if (item[i].type == 3) 
			{
				//需要重定位的数据地址,因为需要修改存储此数据所在区域的内存属性,所以要设置一个指针指向该地址
				ULONG* address = (ULONG*)(item[i].offset + reloc_table->VirtualAddress + m_StubBase);  
				DWORD old_protect = 0; //重定位所在区域不可写,需要修改内存分页属性
				VirtualProtect(address, 4, PAGE_READWRITE, &old_protect);
				*address = *address - dllbase - dlltext + exebase + exepack;
				VirtualProtect(address, 4, old_protect,&old_protect);
			}
		}
		reloc_table = (PIMAGE_BASE_RELOCATION) ((ULONG)reloc_table + reloc_table->SizeOfBlock);
	}
    
    //3. 目前的壳代码不支持重定位,需要关闭
	//OptionalHeader(m_FileBase)->DllCharacteristics &= ~IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE;
    
	//判断是否有重定位,没有不进行修复
	if (OptionalHeader(m_FileBase)->DllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)
	{		
		CopySutbReloc(); //函数实现代码可查看 关于重定位的修复
	}
	
	return;
}

关于加密代码段

有加密就要有解密,所以加密信息一定要告知壳代码,让壳代码解密,那么如何做到加壳器与壳代码的通信呢?

在两个项目中都添加一个共享结构体,并在壳代码中导出一个该结构体变量,加壳器通过GetProcAddress动态获取该结构体变量的地址,向里面写入数据即可实现通信

/*加壳器代码*/
VOID CPack::EncryptCode(LPCSTR SectionName)  //加密代码段
{
	//1.找到.text区段	
	auto section = GetSection(m_FileBase, SectionName);
	auto text = (BYTE*)(section->PointerToRawData + m_FileBase);
	//2.保存加密信息到共享结构体
	m_share->xor_key = rand() % 0x100; //密钥
	m_share->xor_SecVirAddr = section->VirtualAddress;
	m_share->xor_Size = section->SizeOfRawData;
	//3.加密
	for (ULONG i = 0; i < m_share->xor_Size; i++)
		text[i] ^= m_share->xor_key;
	return VOID();
}
/*stub程序代码*/
void DecryptCode() //解密代码段
{
		auto SectionVA = (BYTE*)(share.xor_SecVirAddr + g_imageBase); //1. 计算出被加密区段的 va
		ULONG oldProtect = 0;
		pVirtualProtect(SectionVA, share.xor_Size, PAGE_READWRITE, &oldProtect);
		for (ULONG i = 0; i < share.xor_Size; i++)
			SectionVA[i] ^= share.xor_key; //2. 使用加壳器提供的大小和 key 解密区段
		pVirtualProtect(SectionVA, share.xor_Size, oldProtect, &oldProtect); //	3. 由于代码段不可读写,所以需要修改属性
}

关于加密IAT

IAT: 集合了导入API函数地址的一张表

如果我们的dll地址发生改变,要对每一处调用修改其调用地址是很麻烦的,如果有这样的表,我们将所有的函数调用的入口地址集中在一个集中的地址中,然后通过解析这个表方便我们进行修改,这样我们只要修改这个表所指的函数入口地址。

为什么要加密IAT?

目的: 增加分析难度,IAT错乱后,在OD或x64Dbg调试器中,无法直观的看到API名称

如何进行加密IAT?

  1. 在加壳器中其实不算加密,只是简单的清空掉宿主程序导入表IAT的VirtualAddress及Size字段为0,当然,数据目录表下标12的IAT表首地址和size也是需要清空的
  2. 在壳代码中,用之前保存在共享结构体中的导入表RVA,解析宿主程序导入表,动态加载所有导入模块,解析INT或者IAT(因为这时这两个表存放的内容还是一样的,别忘了,现在我们是PE加载器的身份),通过序号或名称获得函数地址.
  3. 精心构造一段shellcode,将shellcode首地址填入到IAT表中,只需要注意,最终在shellcode中能够跳转到正确的函数地址即可
/*加壳器代码*/
VOID CPack::EncryptIAT()
{
    //	1.保存IAT的RVA到共享结构体
	m_share->import_RVA = OptionalHeader(m_FileBase)->DataDirectory[1].VirtualAddress;
    //	2.将IAT表清空 (注意有两个地方需要清空)	
	OptionalHeader(m_FileBase)->DataDirectory[1].VirtualAddress = 0;
	OptionalHeader(m_FileBase)->DataDirectory[1].Size = 0;
	OptionalHeader(m_FileBase)->DataDirectory[12].VirtualAddress = 0;
	OptionalHeader(m_FileBase)->DataDirectory[12].Size = 0;
}
/*stub程序代码*/
void DecryptIAT()
{
	//	1.通过共享找到原始程序的IAT
	auto import_table = (PIMAGE_IMPORT_DESCRIPTOR)(share.import_RVA + g_imageBase);
	BYTE shellcode[] = "\xB8\x00\x00\x00\x00\x35\x15\x15\x15\x15\xFF\xE0"; //中间4个0x00替换成加密后的函数地址
	//2.遍历导入表
	while (import_table->Name)
	{
		auto dllName =(char*)(import_table->Name + g_imageBase); //DLL名称
		HMODULE hModule = pLoadLibraryA(dllName);

		auto iat_table =(ULONG*)(import_table->FirstThunk+ g_imageBase);
		auto int_table =(ULONG*)(import_table->OriginalFirstThunk + g_imageBase);

		for (int i = 0; int_table[i]; i++)
		{
			ULONG address = 0;
			ULONG old_protect = 0;

			pVirtualProtect(&iat_table[i], 4, PAGE_READWRITE, &old_protect);
			if (IMAGE_SNAP_BY_ORDINAL(int_table[i])) //如果最高位是1,那就是序号导入
			{
				address = (ULONG) pGetProcAddress(hModule, (LPCSTR)(LOWORD(int_table[i])));  //通过序号找到函数地址
			}
			else
			{
				auto funcName = (PIMAGE_IMPORT_BY_NAME)(int_table[i]+ g_imageBase);
				address = (ULONG)pGetProcAddress(hModule, funcName->Name);//通过名称找到函数地址
			}

			BYTE* buffer = (BYTE*)pVirtualAlloc(NULL,0x1000,MEM_COMMIT, PAGE_EXECUTE_READWRITE);//开辟空间			
			memcpy(buffer, shellcode, 13);
			address ^= 0x15151515;
			*((ULONG*)&buffer[1]) = address; //替换成加密后的函数地址
			iat_table[i] = (ULONG)buffer;	//缓冲区首地址填充到IAT表
			pVirtualProtect(&iat_table[i], 4, old_protect, &old_protect);
			}
		import_table++;
		}
}

关于重定位的修复

  • 什么是重定位?

    • 当可执行程序不是以默认加载基址加载的时候,PE加载器会进行修复重定位操作
  • 如何模拟PE加载器修复重定位项?

    • 修复公式非常简单: 修复后的地址 = 修复前的地址 - 默认加载基址 + 当前加载基址
  • 但仅仅如此操作是不够的,虽然目标程序重定位是由壳代码修复的,但是壳代码运行前也是需要PE加载器进行修复重定位的.

    所以,我们还需要将壳代码中的重定位信息拷贝到目标程序,并改变原来重定位表指向壳代码的重定位表,让PE加载器可以修复壳代码的重定位元素

  • 改变指向后,还需要将每一个重定位项的RVA进行修正

    • 原因是: 之前所有重定位项的RVA是根据DLL基址为准的,现在将区段拷贝到了exe程序的.pack区段中,还用原来的RVA寻址就会出现错误
    • 修复公式: 修复后的RVA = 修复前的RVA - 旧区段RVA + 新区段RVA
/*加壳器代码*/ /*这里修复的是壳代码的重定位*/
VOID CPack::CopySutbReloc()
{
	/*
		1.拷贝sutb重定位表到exe
			1.1 由于重定位元素的位置之前是在sutb的.text区段,我们将其拷贝到了exe的.pack区段
				所以需要把所有重定位项的RVA加上exe.pack区段RVA -sutb.text区段RVA才行
		2 自定义添加一个区段 .reloc2 
			2.1	区段大小和stub重定位大小一致或更多
			2.2	因为需要以全0结构结尾,所以注意初始化内存
		3.改变exe重定位表的指向
	*/
	typedef struct _TYPEOFFSET
	{
		WORD offset : 12;
		WORD type : 4;
	} TYPEOFFSET, * PTYPEOFFSET;

	//备份exe重定位表RVA地址 和 默认加载基址
	m_share->reloc_RVA = OptionalHeader(m_FileBase)->DataDirectory[5].VirtualAddress;
	m_share->old_imageBase = OptionalHeader(m_FileBase)->ImageBase;

	//DLL重定位表
	auto reloc_table = (PIMAGE_BASE_RELOCATION)(OptionalHeader(m_StubBase)->DataDirectory[5].VirtualAddress + m_StubBase);
	auto reloc_size = OptionalHeader(m_StubBase)->DataDirectory[5].Size;
	auto reloc_base = (ULONG*)reloc_table;//保存重定位表起始位置,用于拷贝

	auto exe_pack_RVA = GetSection(m_FileBase, newSectionName)->VirtualAddress; //获得exe .pack区段RVA
	auto sutb_text_RVA = GetSection(m_StubBase, ".text")->VirtualAddress; //获得sutb .text区段RVA

	//修复重定位项的RVA
	while (reloc_table->SizeOfBlock)
	{
		DWORD old_protect = 0;
		ULONG* address = &reloc_table->VirtualAddress;
		VirtualProtect(address, 4, PAGE_READWRITE, &old_protect);
		*address = *address + exe_pack_RVA - sutb_text_RVA;
		VirtualProtect(address, 4, old_protect, &old_protect);
		reloc_table = (PIMAGE_BASE_RELOCATION)((ULONG)reloc_table + reloc_table->SizeOfBlock);
	}

	 
	//自定义添加一个区段 .reloc2 区段大小和stub重定位大小一致
	AddSection(".reloc2", reloc_size);

	//拷贝sutb重定位表到宿主程序.reloc2区段
	auto destSection = GetSection(m_FileBase, ".reloc2");
	auto destAddr = destSection->PointerToRawData + m_FileBase;
	memcpy((LPVOID)destAddr, reloc_base, reloc_size);

	//修改重定位表指向 和大小  : 注意destAddr在这里是FOA 需要转换成RVA
	OptionalHeader(m_FileBase)->DataDirectory[5].VirtualAddress = FOAtoRVA(destAddr - m_FileBase , m_FileBase);
	OptionalHeader(m_FileBase)->DataDirectory[5].Size = reloc_size;
}


/*stub代码*/ /*这里修复的是宿主程序的重定位*/
void FixReloc()
{
	typedef struct _TYPEOFFSET
	{
		WORD offset : 12;
		WORD type : 4;
	} TYPEOFFSET, * PTYPEOFFSET;
    
	if (!share.reloc_RVA)  //没有重定位元素不进行修复
		return;
    
	//1.找到exe重定位表
	auto reloc_table = (PIMAGE_BASE_RELOCATION)(share.reloc_RVA + g_imageBase);

	while (reloc_table->SizeOfBlock)
	{
		auto item = (PTYPEOFFSET)(reloc_table + 1); //获得重定位项
		ULONG count = (reloc_table->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / 2;

		for (ULONG i = 0; i < count; i++)
		{
			//2. 解析重定位块,找到所有 Type 为 3 的项,使用上面的公式重定位
			if (item[i].type == 3)
			{
				//需要重定位的数据地址,因为需要修改存储此数据所在区域的内存属性,所以要设置一个指针指向该地址
				ULONG* address = (ULONG*)(item[i].offset + reloc_table->VirtualAddress + g_imageBase);
				DWORD old_protect = 0; //重定位所在区域不可写,需要修改内存分页属性
				pVirtualProtect(address, 4, PAGE_READWRITE, &old_protect);
				*address = *address - share.old_imageBase + g_imageBase;
				pVirtualProtect(address, 4, old_protect, &old_protect);
			}
		}
		reloc_table = (PIMAGE_BASE_RELOCATION)((ULONG)reloc_table + reloc_table->SizeOfBlock);
	}

}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值