记:ELF输出小程序的编写——初始化定义

    科技2026-03-05  5

    0x00 概论

    最近十一好忙,需要干的事情越来越多……所以原本准备写完的软件编写文章就一拖再拖了。

    这篇文章只是抛砖引玉,希望大家可以通过我的拙见,实现更好的文章,以及更好的工具。

    这里的软件还是雏形,依赖的环境十分苛刻,不保证在任何系统上的可靠使用,仅在Windows上使用通用的库函数和API进行编译通过。对代码只有逻辑性的保证。具体的编译问题需要读者自行修复。

    本文实现了简单的将读取elf文件转换为可以被objcopy处理的bat程序运行的小程序的代码。

    阅读本文之前,您需要掌握的技能有:

    技能名称技能熟练度技能教程链接C语言熟悉暂无

    0x10 软件编写——初始化定义

    由[1] 记:ELF文件解析初定义——文件头解析的相关说明可以得到,系统中需要一个基础的兼容数据结构的映射。

    typedef unsigned int* Elf32_Addr ; //无符号程序指令 typedef unsigned short Elf32_Half ; //无符号中等定义的整形数据 typedef unsigned int* Elf32_Offest ; //无符号文件位置指针 typedef int Elf32_SWord ; //有符号标准整形数据 typedef unsigned int Elf32_Word ; //无符号标准整形数据 typedef unsigned char Elf32_Char ; //无符号短整型

    这个东西其实并不是必须的,也可以使用#define的形式进行声明。但是我个人认为在养成良好的编程习惯的情况下,这个是必须要注意到的。这种typedef的声明方式可以让编译器更好的识别我们的自定义数据结构并且对于使用此数据结构的具体参数进行相关性约束,以避免一些低级错误。

    而之所以说不是必须的就在于这个参数也可以直接使用。但是在一个可能需要兼容多个平台的软件结构下就必须要这么做了。这样可以最大程度上将代码与平台具体的参数解耦(这里仅限于应用层)。如果需要更换的平台没有相关的定义,就可以直接在这里替换成平台支持的定义即可。

    但在这里,笔者做的还是有些不足,就是没有再次虚拟一层作为转换接口,使用代码的方式描述需要的数据结构长度。后面也会对此进行优化。

    上面的定义其实没啥好说的,就是相当于简单的重命名了相关的数据结构接口。

    0x20 当前的文件头抽象

    由[1] 记:ELF文件解析初定义——文件头解析的相关说明可以得到,系统中需要的一个基础的文件头的结构映射表。

    typedef struct { Elf32_Char identification[IDENTIFICATION_BUFFER_LEN]; Elf32_Half type; Elf32_Half Machine; Elf32_Word Version; Elf32_Addr Entry; Elf32_Offest Program_Header_Offest; Elf32_Offest Section_Header_Offest; Elf32_Word Flags; Elf32_Half ELF_Header_Size; Elf32_Half Program_Header_Size; Elf32_Half Program_Header_Number; Elf32_Half Section_Header_Size; Elf32_Half Section_Header_Number; Elf32_Half String_And_Section_Index; }S_FILE_TABLE;

    文档中简单的说明了,有了这个文件的头,就可以轻松的得到了需要的数据表对于文件开始位置的偏移。

    这个位置并不能表示任何确认的参数,只能得到一堆关于Section或者是Program格式的数据集。下面还需要对这个数据集做相关的解析,才可以得到各个需要的数据。

    托ELF那久远的年代,那时对于文件的抽象还没有现在操作系统这么复杂,所以大部分的文件结构都可以使用C很简单的读取。而现在较新的操作系统的应用层文件则较为复杂(apk等为代表)。当然,其底层的相关库文件和底层执行文件很多还是满足ELF格式的。

    可以直接使用标准读写API读取指定位置的数据并装进文件头。

    char* buff; printf("Init input file\n"); //得到文件 FILE *fp = NULL; FILE * fpout = NULL; FILE * ftemp; int flen = 0; /*open fp*/ fp = fopen(".\\GreenDreamer.out", "rb"); //获取到文件指针 /*open fp*/ ftemp = fp; //这里的temp其实不需要,但是需要逻辑严谨,所以安排了这个变量 fseek(ftemp,0,SEEK_END); flen = ftell(ftemp); //得到需要读取的长度, //这里笔者选择全部读取的原因在于用于演示的文档不足10k //如果文件较大建议只读取需要的部分, //这也是后面笔者将会逐渐优化的部分,这里仅为demo fseek(ftemp,0,SEEK_SET); buff = (char*)malloc(flen+1); //这里没有释放只是因为这个只是demo的一部分。后面会用到 memset(buff,0,flen+1); //这个是安全性代码,防止printf可能的意外 fread(buff,flen,1,fp); //拷贝到临时的缓冲区, /*close fp*/ fclose(fp); //开关的代码最好对称编程 /*close ftemp*/ fclose(ftemp);

    这里笔者没有关心大小端,事实上ELF文件专门有一位是判断代码大小端的。这个文件的大小端格式并不受到当前代码生成的方式的约束,始终是同一种排列,无需关心大小端问题。

    0x30 数据段的读取

    0x31 数据结构的定义

    得到了这个文件的文件组成的信息就可以根据自己的需求拿到一个表。

    但是在此之前需要一个和这个表数据结构相对应的结构体定义。

    typedef struct { Elf32_Word Section_Header_Name; Elf32_Word Section_Header_Type; Elf32_Word Section_Header_Flag; Elf32_Addr Section_Header_Address; Elf32_Offest Section_Header_Offest; Elf32_Word Section_Header_Size; Elf32_Word Section_Header_Link; Elf32_Word Section_Header_Info; Elf32_Word Section_Header_Address_Align; Elf32_Word Section_Header_Entry_Size; }S_SECTION_HEADER;

    这个定义详情也可以看笔者文章[1] 记:ELF文件解析初定义——文件头解析 ,这个定义看起来似乎可以直接使用,事实上也的确可以。但是如果想要多一些操作的空间,就还需要一些额外的定义,以满足需求。

    由于Section的真实数据段就是一坨数据组成的数据表。这个数据表没有任何实际的意义。所以需要抽象一个表头对数据表做一个映射。

    typedef struct NODE { struct NODE* next;/* 这种定义严格上来说并不严谨 */ struct NODE* prev; char* Name; unsigned int Start_Address; unsigned int End_Address; unsigned int Number; unsigned int File_Entry; }S_NEED_SELECT;

    上面就是这个表头的定义了,它是由一个简单的双向链表和一些基本的定义组成,因为本软件暂时的需求仅仅是得到一个可以被Objcopy识别的一个脚本文件,所以只需要按照需要取出需要的数据即可。于是剩下的基本定义就是这个section的名称、起始和截止地址、section表中的序号、在文件头的偏移。有了这些就可以满足本程序暂时的需求。

    特别说明一下,

    struct NODE* next; struct NODE* prev;

    这种定义其实并不严谨,因为一些编译器很可能做出不同的解释,从而影响到当前数据结构的合理长度,所以如果在某些平台下是需要指定好数据的长度的,否则可能会出现意想不到的偏移。

    比如在对于数据长度与数据偏移较为严谨的数据结构中,32位为编译基本定义的编译器解析的这个指针就是32位长度的,而64位的编译器解析则为64位,这样就造成了就算其他的定义都已经强制的32位兼容了,但是这个指针则实在没办法兼容。所以这种定义一般只建议放在对于数据长度和偏移不怎么需要严格要求的数据结构内。

    而且这种定义唯一的好处就是编写程序和调试的时候较为方便,这种方便是在牺牲抽象的等级存在的,如果程序中出现了极多的双向链表,则最好使用如下的基础定义:

    typedef struct { void* next;/* 这种定义严格上来说并不严谨 */ void* prev; }NORMAL_LIST;

    然后需要进行使用的时候可以重新引用一番以去除赋予其多余的参数

    typedef NORMAL_LIST NODE_LIST; //仅是举例说明,这句话没有经过编译,具体情况需要读者自行分析

    0x32 section表的整理与显示

    S_SECTION_HEADER* section_header; S_NEED_SELECT* need_select_list; //根据头检索到段 section_header = (S_SECTION_HEADER*)malloc(file_table.Section_Header_Size*file_table.Section_Header_Number); //上文得到的文件头中可以得到当前Section的列数与长度,就可以确认需要的长度了。 memcpy(section_header,&buff[(unsigned int)file_table.Section_Header_Offest],file_table.Section_Header_Size*file_table.Section_Header_Number); //将数据丢进去,这个表会自动贴合数据结构。否则说明数据结构的生成出现了问题。 //这里使用copy而不是直接给指针就是为了后面对于只读取局部文件做铺垫。 need_select_list = (S_NEED_SELECT*)malloc(sizeof(S_NEED_SELECT)*file_table.Section_Header_Number);//对表头的初始化 memset(need_select_list,0x00,sizeof(S_NEED_SELECT)*file_table.Section_Header_Number); //安全性的初始化 //遍历段列表提取需要的数据 for (int i = 0;i<file_table.Section_Header_Number;i++) { //较简单的链表映射 need_select_list[i-1].next = &need_select_list[i]; need_select_list[i].next = need_select_list; need_select_list[i].prev = &need_select_list[i-1]; //实际的数据存放 need_select_list[i].Number = i; need_select_list[i].Start_Address = section_header[i].Section_Header_Address; need_select_list[i].File_Entry = section_header[i].Section_Header_Offest; need_select_list[i].End_Address = section_header[i].Section_Header_Address + section_header[i].Section_Header_Size; }

    这里也可以看到,笔者对于当前的数据使用都较为随便,而且这样不对称的malloc会影响到当前的内存使用。这个原因在与当前的生命周期下还没有使用完成,在使用完后,自然需要回收内存段。

    free(need_select_list); free(section_header);

    上面的代码是在没有什么值得说的,但是需要注意的是,现在较多的elf的第一个Section一般是debug段或者是字符定义段,这个段类似于抽象的root段,一般是不存在象征意义上的名称的。也就是说需要考虑到这点否则表头将出现错位。

    0x33 显示得到的数据表

    //检索字符串对应段名。 unsigned int String_Base = need_select_list[0].File_Entry; //一般第一个的数据段都是当前段的名称字符表。 unsigned int String_Number_Offest = 1; for (int i = 1;i < file_table.Section_Header_Number;i++) { need_select_list[i].Name = &buff[String_Base + String_Number_Offest]; do { String_Number_Offest++; } while (buff[String_Base + String_Number_Offest] != '\0'); String_Number_Offest++; printf("Section %02d is %s: 0x%08x-0x%08x \n", i, need_select_list[i].Name, need_select_list[i].Start_Address, need_select_list[i].End_Address); }

    上面说过,section的名称字段的第一个一般都是空白的,但是它是实施意义上的存在的,只是没有象征意义上的名称。不能直接忽略,所以在这里就从第二个进行遍历。

    输出的结果类似于Section 01 is .GreenDream: 0x00000000-0x12345678

    这样就可以得到一个很简单的Section结构表。

    0x34获取需要的数据

    //下面从界面中获取需要的参数 //这个定义正好得到了需要最大的输入的数据长度。 input_keyboard = (char * )malloc(file_table.Section_Header_Number); char temp; int i = 0; for ( i = 1;i<file_table.Section_Header_Number;i++) { printf("please select compare section number:"); //遍历等待输入 scanf_s("%d",&temp,1); if (temp>file_table.Section_Header_Number || temp<0) { //这里就是简单的错误判断,也就是输入和表内的数字不同或者是小于0,这里需要注意,当前的数128也会认为是负数,这个需要读者根据自身情况去修改。 printf("please input true section number!!!!!!"); i--; } else { input_keyboard[i-1] = temp; printf("Input Next? No Next Please Input 0:"); //询问是否继续下去,不想就输入0 scanf_s("%d",&temp,1); if (temp == 0) { break; } else { continue; } } }

    这就是一个简单的读取按键进行选择的代码段,当输入0时就直接退出。

    再次强调一下,这个代码仅为Demo。并没有判断一些错误的输入与未输入的异常状态,这个需要读者自行行动或者等待笔者之后的完善。

    0x35处理需要的数据

    //正式处理当前的参数 printf("Analzy Data"); //核心的文件处理区,这个是暂时还是较为简单的适配objcopy。后面会慢慢脱离 output_data(input_keyboard,i,need_select_list,file_table.Section_Header_Number); //生成文档 //获取当前工作目录 memset(work_path,0x00,1000);//这里的数据只是暂时的,日后可能根据具体的参数进行最小化的申请。 getcwd(work_path,1000); //可以获得当前的目录,这个根据平台会有不同的API。这个需要注意。 //新建工作指定的输出文档 strcat(work_path,"\\Output.bat"); //写入输出文档 fpout = fopen(work_path, "w+"); //保存输出文档流 fputs(output_buffer,fpout); fclose(fpout); //全部释放,这个是良好的编程习惯。 _fcloseall(); free(work_path); free(input_keyboard); free(need_select_list); free(section_header); free(buff);

    0x36处理的核心(暂时)

    char* output_data( char* Input_Number, int Input_Size, S_NEED_SELECT* Need_Delect_List, int List_Number) { char* String_List; char* Output_List; //这个也是暂时的,日后可能不需要这样进行工作 String_List = malloc(100); Output_List = malloc(100); //这个是备份的文件戳,用于后面作为释放。 char* String_List_bak = String_List; char* Output_List_bak = Output_List; //安全性初始化 memset(output_buffer,0x00,sizeof(output_buffer)); memcpy(output_buffer,START_CMD,sizeof(START_CMD)); //放进去最前面的命令 strcat(output_buffer,OUTPUT_CMD); strcat(output_buffer,CMD_PARAM); //根据输入的数字生成具体的段 for (int i = 0;i<Input_Size;i++) { //子指令开始 memcpy(Output_List,SELECT_CMD,sizeof (SELECT_CMD)); //开始 Output_List[strlen(Output_List) - 1] = '\0'; //指令加入 memcpy(String_List,SELECT_CMD,sizeof (SELECT_CMD)); //寻找到需要的section名称 String_List = strchr(String_List,'\"'); //合并section名称在文件中 strcat(String_List,Need_Delect_List[Input_Number[i]].Name); strcat(String_List,"\" ^\n"); //取出数据准备传出 strcat(Output_List,String_List); strcat(output_buffer,Output_List); //清空缓存的数据 memset(String_List_bak,0x00,100); memset(Output_List_bak,0x00,100); //将备份的数据指针还原 Output_List = Output_List_bak; String_List = String_List_bak; } //放进去后面的指令 strcat(output_buffer,GET_ELF_PATH); strcat(output_buffer,OUTPUT_CMD); //仅作为验证 printf("%s",output_buffer); //显得蛋疼做的。 memset(String_List_bak,0x33,100); memset(Output_List_bak,0x33,100); //释放 free(String_List_bak); free(Output_List_bak); }

    这样就可以生成一个和objcopy兼容的指令,这个指令可以取出需要的section。

    这里的一些大写的标志表示需要的宏定义。因为此程序仅为过度的版本,所以暂时只需要这种方式即可以完成对于elf的指定bin的二进制纯文件输出脚本的生成。

    下面就是这些宏定义的具体参数。

    #define START_CMD \ "set basefile=\%1\n\ echo \%basefile\%\n\ echo del bin file\n\ del /Q " #define CMD_PARAM \ "objcopy.exe ^\n\ -I elf32-little ^\n\ -O binary ^\n\ --gap-fill 0xff ^\n\ -S -v ^\n" #define SELECT_CMD \ "-j \"" #define GET_ELF_PATH "\%basefile\%\\ext.out ^\n" #define OUTPUT_CMD \ "\%basefile\%\\ext.bin\n"

    这里就可以看出,当前生成的文件是具有强烈指向性的脚本文件,比如必须要求输入elf32-little格式文件,或者指定的二进制输出空白部分为0xFF。这些在后面实现了自身输出的情况下都可以自行完成,当然,也可以直接修改这里的宏定义以满足一些objcopy可以支持的要求。

    0x37最后的生成

    生成的脚本文件大概长得这副样子。

    set basefile=%1 echo %basefile% echo del bin file del /Q %basefile%\Debug\Exe\ext.bin objcopy.exe ^ -I elf32-little ^ -O binary ^ --gap-fill 0xff ^ -S -v ^ # 打广告专用区块,真实显示一般是需要输出的区域 -j "Greendream rw" ^ -j "Greendream ro" ^ -j "Greendream s0" ^ # 这个区间就是需要转换的位置了 %basefile%\ext.out ^ %basefile%\ext.bin

    这里可以看出,这个脚本需要一个输入的目录为基础目录,在这个基础目录下就会生成需要的脚本文件。但是要求待读取的elf文件必须是ext.out。当然,这个可以直接修改宏定义解决。

    0x40 后记

    根据当前的文件格式,这个文件不一定是满足我们读取的文件,elf也有很多字符都是做这个控制的。

    在这里笔者只使用了ELF 平台的ID。这个ID是约定俗成的,比如ARM就是40.

    #define EM_ARM 40 /* ARM *///暂时先用这个

    在对于File Header中就可以直接校验这个编码来确认是否满足当前的CPU架构。

    这个校验暂时不需要做很多,因为并不在需求范围内。所以暂时不做太多的要求。

    0x50 本文关键词

    bat objcopy elf

    更多

    本文首发自 记:ELF输出小程序的编写,更多文章可进入我的博客详查。

    Processed: 0.012, SQL: 10