前言
在开始之前还是要先纠正一下之前第一部分中末尾的COFF文件结构图,那个是我在解析时的顺序而非COFF本身的结构,我在查看一篇很早之前对于COFF介绍的文章中突然意识到了自己的错误,下图是文章中给的图示。
有意思的是当我分别使用msvc和gcc将如下代码编译为obj时发现了差别
#include <stdio.h>
void my_printf()
{
printf("hello world");
}
int main()
{
my_printf();
return 0;
}
他们生成的obj文件的文件头,节头,符号表,字符串表在COFF文件中的布局是相同的,但是节的原始数据和节的重定位表却不同。
下图是msvc编译的obj,紧跟着节原始数据后的就是重定位表
而下图是gcc编译的obj,紧随节原始数据后的是另外一个节的原始数据,而不是重定位表
所以总的来说,gcc和msvc编译的OBJ文件文件格式并不完全相同,而gcc编译的OBJ文件更符合上述文章中提到的COFF结构。
友情提示:所有源码可[1]在此获得
在上一节中我们最终能够解析OBJ文件,这一节将继续使用解析的内容实现OBJ文件的加载和运行,当然这里主要针对的是Cobal Strike中所用的BOF文件,毕竟我们实现的仅仅是小小加载器而非针对中间文件的链接器。
至于什么是BOF文件以及如何编写BOF,Cobalt Strike的官方文档已经做出了详细的说明,另外在早些时候还发布了编写BOF文件的Visual Stdio的[2]官方模板,更便于大家的编写和调试。
正文
一:文件头
万事从头起,Coff文件头包含了定位所有节/表的重要信息,遍历所有节进行重定位需要他,定位符号表也需要他
coff_file_header_t* coffFileHdr = procFileHeader(content);
二:内存映射
如果了解过dll反射加载或者傀儡进程技术的应该知道,我们需要一块内存空间对PE进行内存映射,并按照一定的字节对齐,一般是0x1000,同样我们对Coff文件进行内存映射也需要这么一块内存,但是并没有文档告诉我们应该按照多少字节对齐,CoffLoader的做法是使用VirtualAlloc独立的为每个节申请内存,理由则是这样比较方便。我在实现的时候采取了一次性分配所需内存,并按照4K大小对齐。
所以我们需要遍历所有的节并获得节原始数据大小,并进行4K对齐,这样下来我们能够得到所有节按照4K对齐后所需大小,然后使用VirtualAlloc进行一次性申请,注意还要申请一段内存用来保存每个节的地址,方便后续重定位使用
// 事先申请一段内存专门存放每个节映射到内存后的地址
char** secMapPtr = (char**)calloc(sizeof(char*) * (coffFileHdr->NumberOfSections + 1), 1);
for (uint32_t i = 0; i < coffFileHdr->NumberOfSections; i++)
{
...
allSection += ALIGN_TO_4K(secHdr->SizeOfRawData); //计算所有节大小
...
}
//申请内存
void* ptrSection = VirtualAlloc(NULL, ALIGN_TO_4K(allSection + 0x1), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
有了内存后就是将节的原始数据拷贝到内存中去,节的地址此时也被保存到刚才申请的内存中
for (i = 0; i < coffFileHdr->NumberOfSections; i++)
{
coff_sec_header_t* secHdr = procSecHeader(content, i);
if (secHdr->PointerToRawData != 0)
memcpy((char*)ptrSection + i * ALIGNPAGE, content + secHdr->PointerToRawData, secHdr->SizeOfRawData);
secMapPtr[i] = (char*)ptrSection + secMapOffset[i]; //获取每个节的内存地址
}
三:重定位
作为COFF加载最为重要也是最为复杂的一部分,COFF的重定位类型有很多,且和处理器有关系,当然我们只处理x64 Processors和Intel 386 Processors。虽然类型很多,但是总的来说可以分为两种,分别是绝对地址定位和相对地址定位。
如下所示,位于节A的0x1001地址需要重定位到节D的位置,如果是绝对地址定位,则此处直接填写目标跳转地址即可,如果是相对地址定位,则是0x4000 - 0x1005 = 0x2ffb。
接着我们再看CoffLoader重定位的一般步骤
-
1. 取得待重定位地址处的值,如上就是0x1000处的值,这里为0
-
2. 找到重定位的目标所在节的地址,如上就是0x4000
-
3. 找到重定位符号对应符号表中的Value字段(这个下面会说)
-
4. 将上述三个值相加得到跳转的目标地址 ,如上就是0x4000+0x0+Value
-
5. 根据重定位的类型回写到待重定位地址
关于第一点我没有在微软官方的[3]PE Format文档中看到相关说明,表示此值为目标地址所在节中的偏移,但是我在名为[4]djgpp的网站找到了相关信息,但也是说明了你在重定位时候应当这么做。
关于第三点,我们在重定位的时候,都要先在符号表中找到重定位的符号信息,再由符号信息来判断符号所在节,以及在节中的偏移。
其中最为头疼的就是符号表中有个字段名为Value,这个值随着StorageClass和SectionNumber有诸多含义,对于我们来说就是其是否指代在符号在节中的偏移,CoffLoader的做法很决绝,他几乎在每个重定位的处理上都加上了这个Value字段,起初我不是很理解为什么这样做,而且在Havoc中并没有加上这个值,直到我在测试ScreenshotBOF的时候老是出错,在加上此值之后能顺利运行,我才知道这个值的重要性。
但是在我的实现中,并没有像CoffLoader那样在每个处理块中都加上这个值,而是先根据StorageClass和SectionNumber来判断Value表示的是否是节内偏移。
BOOL needAddSymValue(coff_sym_t* symPtr)
{
if (symPtr->StorageClass == IMAGE_SYM_CLASS_EXTERNAL && symPtr->SectionNumber != IMAGE_SYM_UNDEFINED)
return TRUE;
if (symPtr->StorageClass == IMAGE_SYM_CLASS_STATIC)
return TRUE;
if (symPtr->StorageClass == IMAGE_SYM_CLASS_LABEL)
return TRUE;
return FALSE;
}
COFF的”IAT“表
我们都知道PE文件在调用导入函数时候,是通过IAT表调用的,在之前的文章中也有提到过,编译器通过这种方式进行代码优化,防止产生多余的跳转指令,调用的格式如下:
Call [xxxxxxxx]
但在Coff 中并没有IAT一说,那我们该怎么办呢?
首先在BOF中通过DECLSPEC_IMPORT DWORD WINAPI LIBRARY$Function来声明使用外部函数,比如
DECLSPEC_IMPORT DWORD WINAPI KERNEL32$GetCurrentProcessId();
如此一来,在obj文件中产生了类似 _imp_KERNEL32$GetCurrentProcessId的符号,我们便可以按照固定格式解析此符号并找到对应的库和函数地址,随后我们可以自己申请一段内存作为IAT表,专门存放这类地址,之后将所有导入函数重定位到此即可。
如果细心的朋友可能已经发现,我在申请节映射内存的时候多申请了一块内存,其实这块内存就是给IAT表准备的
void* ptrSection = VirtualAlloc(NULL, ALIGN_TO_4K(allSection + 0x1), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
ALIGN_TO_4K(allSection + 0x1) 这里就是为了多申请一块内存
四:入口函数
查找入口依然是先查找符号表,找到后获取符号所在节以及其在节内偏移,之后调用。
for (uint32_t i = 0; i < coffFileHdr->NumberOfSymbols; i++)
{
if (strcmp(symHdr[i].sn.Name, entryFuncname) == 0)
{
printf("[+] Found Entry,Type = %d,StorageClass = %dnn", symHdr[i].Type, symHdr[i].StorageClass);
#ifdef _WIN32
foo = (void(*)(char*, unsigned long))(secMapPtr[symHdr[i].SectionNumber - 1] + symHdr[i].Value);
foo(argData, argSize);
#endif // _WIN32
}
}
其中需要注意的是,在x86上,原本符号名为go的函数在obj中却变成了_go,前置下划线实际上是C函数或者C++ extern "C"函数的修饰形式之一,单独下划线表示函数的调用约定是__cdcel,具体的表格如下:
引用
[1]:https://github.com/jseclab/wechat_public/tree/main/bof/p2
[2]:https://github.com/Cobalt-Strike/bof-vs
[3]:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#storage-class
[4]:https://www.delorie.com/djgpp/doc/coff/reloc.html
原文始发于微信公众号(无名之):Coff Loader 第二部分:一步步实现COFF加载器
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论