Coff Loader 第二部分:一步步实现COFF加载器

admin 2024年4月22日03:56:13评论3 views字数 4528阅读15分5秒阅读模式

前言

在开始之前还是要先纠正一下之前第一部分中末尾的COFF文件结构图,那个是我在解析时的顺序而非COFF本身的结构,我在查看一篇很早之前对于COFF介绍的文章中突然意识到了自己的错误,下图是文章中给的图示。

Coff Loader 第二部分:一步步实现COFF加载器

有意思的是当我分别使用msvcgcc将如下代码编译为obj时发现了差别

#include <stdio.h>

void my_printf()
{
    printf("hello world");
}

int main()
{
    my_printf();
    return 0;
}

他们生成的obj文件的文件头节头符号表字符串表在COFF文件中的布局是相同的,但是节的原始数据节的重定位表却不同。

下图是msvc编译的obj,紧跟着节原始数据后的就是重定位表

Coff Loader 第二部分:一步步实现COFF加载器

而下图是gcc编译的obj,紧随节原始数据后的是另外一个节的原始数据,而不是重定位表Coff Loader 第二部分:一步步实现COFF加载器

所以总的来说,gccmsvc编译的OBJ文件文件格式并不完全相同,而gcc编译的OBJ文件更符合上述文章中提到的COFF结构。

友情提示:所有源码可[1]在此获得

在上一节中我们最终能够解析OBJ文件,这一节将继续使用解析的内容实现OBJ文件的加载和运行,当然这里主要针对的是Cobal Strike中所用的BOF文件,毕竟我们实现的仅仅是小小加载器而非针对中间文件的链接器。

至于什么是BOF文件以及如何编写BOFCobalt 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 ProcessorsIntel 386 Processors。虽然类型很多,但是总的来说可以分为两种,分别是绝对地址定位相对地址定位

如下所示,位于节A的0x1001地址需要重定位到节D的位置,如果是绝对地址定位,则此处直接填写目标跳转地址即可,如果是相对地址定位,则是0x4000 - 0x1005 = 0x2ffb

Coff Loader 第二部分:一步步实现COFF加载器

接着我们再看CoffLoader重定位的一般步骤

  1. 1. 取得待重定位地址处的值,如上就是0x1000处的值,这里为0

  2. 2. 找到重定位的目标所在节的地址,如上就是0x4000

  3. 3. 找到重定位符号对应符号表中的Value字段(这个下面会说)

  4. 4. 将上述三个值相加得到跳转的目标地址 ,如上就是0x4000+0x0+Value

  5. 5. 根据重定位的类型回写到待重定位地址

关于第一点我没有在微软官方的[3]PE Format文档中看到相关说明,表示此值为目标地址所在节中的偏移,但是我在名为[4]djgpp的网站找到了相关信息,但也是说明了你在重定位时候应当这么做。

Coff Loader 第二部分:一步步实现COFF加载器

关于第三点,我们在重定位的时候,都要先在符号表中找到重定位的符号信息,再由符号信息来判断符号所在节,以及在节中的偏移。   

Coff Loader 第二部分:一步步实现COFF加载器

其中最为头疼的就是符号表中有个字段名为Value,这个值随着StorageClassSectionNumber有诸多含义,对于我们来说就是其是否指代在符号在节中的偏移,CoffLoader的做法很决绝,他几乎在每个重定位的处理上都加上了这个Value字段,起初我不是很理解为什么这样做,而且在Havoc中并没有加上这个值,直到我在测试ScreenshotBOF的时候老是出错,在加上此值之后能顺利运行,我才知道这个值的重要性。

Coff Loader 第二部分:一步步实现COFF加载器

但是在我的实现中,并没有像CoffLoader那样在每个处理块中都加上这个值,而是先根据StorageClassSectionNumber来判断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,具体的表格如下:

Coff Loader 第二部分:一步步实现COFF加载器

引用

[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加载器

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年4月22日03:56:13
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Coff Loader 第二部分:一步步实现COFF加载器http://cn-sec.com/archives/2628346.html

发表评论

匿名网友 填写信息