• Linux-03


    PE文件的组成

    • 头部

      • DOS头(兼容老版本系统而保留的)

      • NT头

        • 文件头

        • 扩展头

          • 数据目录表

            • 导出表 : 用于保存dll中的导出符号

            • 导入表 : 用于保存本模块所使用的其它模块的符号

              • 在程序中调用导出函数的时候,形成汇编句式:

                call [IAT]

            • 重定位表 : 支持随机加载基址

        • 区段头

          • .text : 用于保存可执行的代码

          • .data : 用于全局变量/对象的初始数据

          • .rdata : 用于保存常量数据

    • 区段数据

    ELF文件组成

    • ELF头

      • 文件头 : 用于记录一个ELF文件的信息(多少位? 能够运行的CPU平台是什么?,程序入口点在哪里?等等)

      • 程序头表 : 程序运行起来的时候, 需要用到哪些区段的数据? 例如,程序的可执行代码存在哪个区段? 常量数据存在哪个区段? ELF文件运行起来的时候, 必须存在程序头,否则ELF文件无法加载并运行.

      • 区段头表 : 用于记录ELF文件的主要的数据.

        • .text 记录代码

        • .data 记录数据

        • .rdata 记录常量数据

        • .symtab 记录符号表(相当于PE文件的导出表)的数据

        • .rel.plt 记录某个区段的重定位内容(相当于PE文件的导入表)

    命令readelf -h 文件名可以文件头信息

    命令readelf -l 文件名可以查看程序头信息

    命令readelf -s 文件名可以段信息

    1567598446876

    区段头

    1. 区段名

      1. 区段名是一个基于区段头字符串表数段首地址的偏移

      2. 区段头字符串表在区段表中的下标记录在elf文件头中. 字段:e_shstrtabndx

      3. 找名字的过程是:

        1. 通过elf头的e_shoff找到区段头表的首地址.

        2. 通过elf头的区段头字符串表下标

        3. 通过下标索引区段表, 就得到区段头字符串表的区段头

        4. 通过字符串表的区段头的:sh_offset就可以得到字符串表在文件中的首地址.

        5. 最后通过区段名偏移 + 字符串表的偏移 ,就能定为到字符串.

          1567598507725

        1567598522141

    2. sh_link字段

      1. 如本区段的数据是一张表, 表中的数据要引用其它区段的数据时, link字段就会保存被引用的区段的区段头下标.

    3. sh_info字段

      1. 每个不同类型的区段, info的意义是不一样的

      2. 例如,区段是符号表时, info的值是一个符号在符号表中的下标. 这个符号符号表中第一个非LOCAL类型的符号.

    ELF的原理

    1. 怎么支持随机加载基址

      1. 什么样的汇编代码会产生需要重定位的代码

        printf("hello world
        ");
        ; 对应的汇编:
        push 0x43000 ; "hello world "字符串首地址
        call printf
        add esp,4

        MessageBox(0,0,0,0);
        ; 对应的汇编:
        push 0
        push 0
        push 0
        push 0
        call [0x403008]; MessageBox所在的IAT的地址
      2. 在PE文件中, 使用重定位表来保存需要重定位的代码的位置, 在加载的时候, 如果发生随机基址, 那么就遍历重定位表来修改每个地址.

      3. 在ELF文件中, 没有这样的重定位表. 但是ELF文件是支持随机加载基址的. 在ELF文件中,使用的是getpc + 偏移的技术来支持随机加载基址.

        #include <stdio.h>
        int g_nNum = 0x12345678;
        int main()
        {
        printf("hello %d",g_nNum);
        }

        汇编:

        lea     ecx, [esp+4]
        and     esp, 0FFFFFFF0h
        push    dword ptr [ecx-4]
        push    ebp
        mov     ebp, esp
        push    ebx
        push    ecx
        call    __x86_get_pc_thunk_ax
        add     eax, 1AA7h

        ; 根据全局变量的偏移来访问全局变量的值.
        ; 这样一来是不会产生重定位的.
        mov     edx, (g_nNum - 1FD8h)[eax] ; mov edx,[eax + g_nNum - 0x1FD8]                       ; MOV EDX,DWORD PTR DS:[EAX+0x30]
        sub     esp, 8
        push    edx
        lea     edx, (aHelloD - 1FD8h)[eax] ; "hello %d"
        push    edx             ; format
        mov     ebx, eax
        call    _printf
        add     esp, 10h
        mov     eax, 0
        lea     esp, [ebp-8]
        pop     ecx
        pop     ebx
        pop     ebp
        lea     esp, [ecx-4]
        retn
      4. getpc之后再加上一个偏移值, 得出的值是一个.got段的首地址

        一般,只读数据段.rodata.got的上面, 数据段.data.got的下面.

    2. 怎么实现导出表的功能

      1. 在PE文件中,导出一个函数需要使用关键字声明或使用def文件定义.才能导出.

      2. PE文件使用导出表来记录导出函数的信息:

        1. 导出函数的名字 (导出名称表)

        2. 导出函数的序号 (导出表的序号基数+导出地址表的下标)

        3. 导出函数在文件中的rva (导出地址表)

      3. 在ELF文件中, 导出一个函数不需要任何的额外手续, 在文件中定义的函数都被默认导出. 只有被加了static的函数或全局变量才不会被导出.

      4. 使用符号表来记录导出函数的信息

        1. 符号表由符号结构体来组成

        2. 符号结构体记录的是:

          1. 符号的名字

          2. 符号的类型

          3. 符号的绑定类型(决定了符号是否被导出)

          4. 符号数据在文件中的位置(如果符号是一个函数,那么这个位置就是导出函数的地址.)

          5. 符号数据在文件中的大小(如果符号是一个函数, 就是函数机器码占的字节数)

        3. ELF文件的符号表保存在区段表中. 只要区段类型是SHT_SYMTAB的时候, 说明该区段保存的就是符号表.

        4. 符号表使用的结构体:

          typedef struct
          {
            Elf64_Word st_name;//符号名
            unsigned char st_info;//符号类型和绑定类型
            unsigned char st_other;// 符号的可见性
            Elf64_Section st_shndx;//符号所在的区段的下标
            Elf64_Addr st_value;//符号数据在文件的偏移
            Elf64_Xword st_size;//符号数据所占的字节数
          } Elf64_Sym;
    3. 怎么实现导入表的功能

      1. 在PE文件,使用导入表的时候, 是通过call [IAT]来调用导入函数

      2. 在PE文件中, 使用了导入结构体数组来记录导出的dll和函数

        1. DllName的字段记录要导入的dll的名字

        2. 有导入名称表记录了从dll中导入进来的函数名或函数的序号

      3. 在加载器加载PE文件的时候, 会根据导入名称表修复导入地址表(IAT表)

      4. 在ELF文件中, 使用导入表的时候, 所有的导入函数的地址都保存在.got段中, 保存的时候,是以一个4字节的数组来保存的, 这些导入函数的地址在运行前是没有的, 在运行的时候,加载器会找到所有导入函数的地址,并依次填充进去.

      5. 在调用导入函数的时候, 步骤有两步

        ; 1. 先调用.plt段中.
        call .plt + 偏移  ; 通过偏移调用到.plt段

        ; 2. 在plt段中有一个句代码.直接通过.got的首地址
        ;   索引到一个导入函数的地址, 最终完成调用.
        .plt jmp [ ebx + 偏移 ] ;ebx保存的是.got的首地址

        .got 导入函数地址1
        .got 导入函数地址2
        .got 导入函数地址3
      6. 导入函数的名字以及导入函数应该填充到.got表的哪个位置?

        1. 通过动态符号表(SHT_DYNSYM)(可称为导入名称表)和重定位表来完成的.

        2. 重定位表只用于保存符号名和.got的对应关系

          typedef struct
          {
            Elf64_Addr r_offset;// 重定位的位置
            Elf64_Xword r_info;//符号的类型和符号的下标
          } Elf64_Rel;
        3. 总结

          1. 重定位表记录的内容是: 要重定位的位置, 以及要重定位的符号(符号实际就是导入的符号)

        4. 导入的符号在哪个SO文件中?

          1. 区段头中, 当区段类型是SHT_DYNAMIC时, 这个段就记录着动态链接的信息, 其中就有记录需要加载哪些so文件.

            1567598533021

    ELF文件的畸形化

  • 相关阅读:
    Live2d Test Env
    Live2d Test Env
    Live2d Test Env
    图神经网络入门
    CommandLineRunner 可能会导致你的应用宕机停止,我劝你耗子尾汁
    不使用 MQ 如何实现 pub/sub 场景?
    为什么 @Value 可以获取配置中心的值?
    vite + ts 快速搭建 vue3 项目 以及介绍相关特性
    给 Mac 添加右键菜单「使用 VSCode 打开」
    【Python】连接常用数据库
  • 原文地址:https://www.cnblogs.com/ltyandy/p/11462940.html
Copyright © 2020-2023  润新知