首页
社区
课程
招聘
[原创]逆向快速入门
发表于: 2023-2-5 23:31 19661

[原创]逆向快速入门

2023-2-5 23:31
19661

本公众号分享的所有技术仅用于学习交流,请勿用于其他非法活动,如有错漏,欢迎留言指正

逆向快速入门

系统引导,磁盘分区,文件系统

BiOS+MBR

  • MBR:主引导记录,446字节
    • 如果修改这里,加载木马驱动,即使重装系统也没有清除木马,因为重装系统只会修改主分区的活动分区。MRB的数据没有发生改变
  • 主分区(只有四个,16byte*4)
    • 活动分区,用来启动操作系统
    • 扩展分区,可以建立逻辑分区
  • 魔数(2Byte)
    • 0x55AA,类似PE的'MZ' ,如果这里被破坏,系统就废了。

      BiOS+MBR启动过程:

  • 在打开电源时,计算机开始自检过程,从BIOS中载入必要的指令,然后进行一系列的自检操作(进行硬件的初始化检查,包括内存、硬盘、键盘等)同时在屏幕上显示信息自检完成之后,根据CMOS的设置,BIOS加载启动盘,将主引导记录(MBR,Master Boot Record,物理硬盘的第1个扇区(LBA为0)0柱面,0磁头,1扇区)中的引导代码载入内存,接着,启动过程由MBR来执行。启动代码搜索MBR中的分区表(DPT),找出活动分区,将活动分区的第1个扇区(VBR,volume boot record,卷引导记录,活动分区的VBR也叫DBR)中的引导代码载入内存0x07C00处,引导代码检测当前使用的文件系统,查找ntldr文件,找到之后将启动它。BOS将控制转交给ntldr,由ntldr完成操作系统的启动(注意:Windows7与此不同,用的是Bootmgr,Bootmgr就会读取系统文件winload.exe,加载Windows7内核、硬件、服务等,然后加载桌面等信息,从而启动整个Windows7系统)。
  • MBR:计算机启动后从可启动介质上首先装入内存并且执行的代码,整个物理硬盘的第1扇区。
  • DBR:相当于主分区中的第一个扇区,(Dos(OS)Boot Record)为操作系统进入文件系统以后可以访问的第一个扇区,通常用来解释文件系统,操作作系统可以直接访问的第一个扇区,它通常把包括一个引导程序和一个被称为BPB(Bios Parameter Block)的本分区参数记录表。引导程序的主要任务是当MBR将系统控制权交给它时,判断本分区根目录前两个文件是不是操作系统的引导文件(DOS为例:即是Io.sys和Msdoc.sys。如果确定存在,就把它读入内存并把控制权交给该文件。BPB参数块记录着本分区的起始扇区结束扇区文件存储格式硬盘介质描述符根目录大小FAT个数分配单元的大小等重要参数。DBR是由高级格式化程序(即Format.exe等程序)所产生的。由BPB参数表、分区引导程序出错信息以及分区引导有效标志55AA等内容构成
  • VBR:每个非扩展分区以及逻辑分区的第一个扇区(VBR包括DBR(非扩展分区)和EBR)。可存放小段程序,用于启动该分区上某程序或者操作系统(DBR)。比如ntldrd的VBR会加载分区上的ntldr来启动xp。
  • 扇区参数2种:
    • CHS(Cyliner柱面,head磁头,sector扇区,比如80个柱面2个磁头,每个磁道18个扇区)
    • LBA(逻辑块地址Logic Block Address)
  • 扩展分区中的每个逻辑驱动器都存在一个类似于MBR的扩展引导
    记录(Extended Boot Record,EBR)

    BIOS+MBR局限性

  • 固定4个主分区,只能装3个操作系统
  • 主分区中的活动分区(操作系统启动盘,即c盘),最大只支持2TB

    • LBA(logical block address,32位),一个扇区512B,分区2^322^9=241=2TB
    • 可以用大扇区,突破2TB的限制,512->4K,但产生的碎片也多,扇区并不是越大越好。

      UEFI+GPT

  • “统一的可扩展固件接口"(Unified Extensible Firmware lnterface)替代BIOS

  • LBA(64位),分区数量无限制,分区大小也支持到2^64(4G*4G*512),MS人为限制128个分区
  • UEFI本身己经相当于一个微型操作系统。UEFI已具备文件系统的支持,它能够直接读取FAT分区中的文件。可开发出直接在UEF下运行的应用程序,这类程序文件通常以efi结尾。可以将Windows安装程序做成efi类型应用程序,然后把它放到任意分区中直接运行即可。安装Windows操作系统变得简单
  • 而在UEFIF,这些统统都不需要,不再需要主引导记录,不再需要活动分区,不需要任何工具,只要复制安装文件到一个FAT32(主)分区/U盘中,然后从这个分区/U盘启动,安装Windows就是这么简单。在BIOS下,启动操作系统之前必须从硬盘上指定扇区读取系统启动代码(包含在MBR中)然后从活动分区中新导启动操作系统。对扇区的操作远比不上对分区中文件的操作更直观更简单
  • PMBR的作用是,当使用不支持GPT的分区工具时,整个硬盘将显示为个受保护的分区,以防止分区表及硬盘薮据遭到破坏
  • Secure Boot: UEFI子规定,。防止恶意软件侵入。UEFI规定:主板出的时候,可以内置一些可靠的公钥。然后,任何想要在这块主板上加载的操作系统或者硬件驱动程序,都必须通过这些公钥的认证。即必须用对应的私钥签署过,否则主板拒绝加载。由于恶意软件不可能通过认证,因此就没有办法感染Boot。没规定谁负责颁发这些公钥。微软要求主板厂商内置windows的公钥,就没办法安装其他操作系统了,比如Linux。

MFT表与文件记录

  • 文件恢复
  • ntfsdoc.pdf
  • FileSys.cpp(解析FAT32和ntfs)
    • 找到MFT表位置,找到文件的原始数据

      地址映射

      两种模式*两种模型

      实模式保护模式

  • 实模式保护模式相对,实模式运行于20位地址总线,保护模式则启用了32位地址总线,地址使用的是虚拟地址,引入了描述符表;
  • 实模式程序和程序之间基本上没有隔离和保护,系统容易死机蓝屏。
  • 保护模式主要保护的就是我们内存资源,实现应用层和内核层,应用层与应用之间的数据访问隔离,防止被非法访问,非法破坏,非法修改。而做到这一点的就是通过分段分页机制实现的。所以 保护模式下要访问内存,需要一个地址转换(虚拟地址转化成物理地址)的过程,
  • 虽然二者都引入了段这个概念
    • 但是实模式的段是64KB固定大小只有16不同的段(64KB*16=1MB),cs,ds存放的是段的序号(模16的地址)。
    • 保护模式则引入GDT和LDT段描述符表的数据结构来定义每个段。cs,ds存放的是段选择子
  • 简而言之,保护模式是为了保护的就是我们内存资源,所以为了增加一些安全属性,才引入了分段分页机制。实模式下没有虚拟地址逻辑地址的概念,直接这样算出地址cs<<4 + ip,所有的地址都是物理地址。

    平坦模型分段模型

  • 平坦模型分段模型相对,区别在于程序的线性地址是共享一个地址空间(所以说平坦模型指的就是一个段)还是需要分成多个段,即多个程序同时运行在同一个CS,DS的范围内,还是说每个程序都拥有自己的CS,DS。

    • 实模式下
      • 平坦模型指令的逻辑地址要形成线性地址,不需要切换CS,DS;
      • 分段模型的逻辑地址,必须切换CS,DS,才能形成线性地址。
      • 这种多段模型为了访问到1MB地址空间,还需要额外打开A20地址线呢,这种访存方式本身就是种补救措施,相当于给硬件打了个补丁,既然是补丁,访问内存的过程必然是不顺畅的。
    • 保护模式下

      • 平坦模型的逻辑地址,必须要经过段选择子去查找段描述符,切换CS,DS,才能形成线性地址。
      • 分段的概念发生变化,引入GDT和LDT段描述行表的数据结构来定义每个段。cs,ds存放的不再是段地址,而是存放的是段选择子,但不叫分段模型,而是称为平坦模型
    • 简而言之,在实模式下,分段模型的出现只是为了解决寄存器位数和地址总线位数不匹配,才需要使用段寄存器左移4位+基址寄存器来间接寻址,如果寄存器位数和地址总线位数匹配,就可以直接寻址了,不再需要使用段寄存器了,此时就是平坦模型。在保护模式下,分段的概念发生变化,引入GDT和LDT段描述行表的数据结构来定义每个段。cs,ds存放的不再是段地址,而是存放的是段选择子,但不叫分段模型,而是称为平坦模型

      全部寻址模型

  • x86体系结构下,使用的较多的内存寻址模型主要是实模式分段模型(只是为了向后兼容,模拟8086/8088的doc环境才会用到)和保护模式平坦模型(重点掌握这个即可)
  • 1.实模式分段模型 real mode segment model(之前16bit系统)
    • 在早期8086时代的寻址方式,那时候寄存器才16位(只能表示64KB的内存空间),但地址总线是20位(能表示1MB的内存空间),问题来了,用16bit的寄存器来表示段内偏移,但段的起始地址是20bit的,而寄存器只有16bit,所以可以通过让段的起始地址对齐到16(也就是说要求段的起始地址能被16整除,也就是低4bit都为0),这样20bit的内存空间,存放的时候只需要存放段的起始地址中高16bit数据即可,从而16bit的寄存器也可以存放段的起始地址了。故段的起始地址右移4位存放在CS,DS等寄存器中,用IP寄存器存放段内偏移。addr = cs<<4 + ip实现了用16bit寄存器寻址20bit的内存空间。
    • 对于8086/8088运行在实模式的程序,其实就是运行在实模式分段模型中。对于不同的程序,有不同的CS,DS值,每个程序的段起始址都不同。对于这样的程序而言,偏移地址16位的特性决定了每个段只有64KB大小。
    • 16位寄存器(64K),20位地址(1M)
    • 每个段64KB,共16个段
    • cs/ds寄存器存放的是段的起始(16字节对齐,末尾4位为0,可不存,相当于seg>>4),ip等寄存中放段内偏移
  • 2.实模式平坦模型(real mode flat model)
    • 该模式只有在80386及更高的处理器中才能出现。
    • 80386的实模式就是指CPU可用的地址线只有20位,能寻址0~1MB的地址空间。注意:80386的实模式并不等同于8086/8088的实模式,后者的实模式其实就是实模式分段模型
    • 平坦模型,意味着我们这里不使用任何的分段寄存器。(其实还是使用了CS,DS,只是不用程序员去显式地为该寄存器赋值,jmp指令时就已经将CS,DS设置好了)
  • 3.保护模式平坦模型 protected mode flat model(现在的32bit/64bit系统)
    • Linux,Vindow XP/7采用的内存寻址模型
      • Linux中,段主要分为4种,即为内核代码段,内核数据段,用户代码段,用户数据段。对于内核代码段和数据段而言CS,DS的值是0xC000 0000,而用户代码和数据段的CS,DS的值是0x0000 0000
      • 当CPU运行32位模式,不管怎样,寄存器和指令都可以寻址整个线性地址空间,所以根本就不需要再去使用基地址。基址可以设为一个统一的值
      • 32/64位寄存器,32/64位地址
      • 1个寄存器就可以寻址整个线性地址空间cs/ds寄存器值固定为0或者定值,无需再参与地址计算
      • 逻辑地址到物理地址:页表,CR3,应用层和内核层公用CR3(会产生漏洞)
  • 4.保护模式分段模型不存在,怎么理解呢
    • 首先在保护模式下,分段的概念发生了变化,引入GDT和LDT段描述符表的数据结构来定义每个段。跟实模式分段模型不一样.
    • 如果从以前的角度来理解,在x86和x64系统中寄存器位数和地址总线的位数是匹配的,不需要使用分段模型

      保护模式平坦模型下地址转换的过程

  • 宏观来看:内存空间是先分段(代码段,数据段,栈等),段内再页(page,4k)
  • 从逻辑地址到物理地址的翻译过程叫寻址
  • 总而言之,从逻辑地址到物理地址的翻译过程就是在逐层查表的过程
    • 逻辑地址= 选择子:段内偏移
    • 根据选择子去段描述表得到段基址后(逻辑辑地址->线性地址)
    • 线性地址 = 段基址+段内偏移
    • 根据线性地址中的高10bit(页目录项),中间10bit(页表项)去查页目录表和页表得到最终的物理内存页的基地址(线性地址->物理地址)
    • 物理地址 = 物理内存页的基地址+页内偏移,即(CR3[页目录项])[页表项]+页内偏移

分段机制

段寄存器
  • 段寄存器就是为了段机制而存在的,但在不同的模式下,他们的作用,存放的数据各不一样。
  • 在8086实模式分段模型
    • 段寄存器存放的是实实在在的段的起始地址,而且地址必须能被16整除的(就是说低4位都是0)
    • 在实模式下没有逻辑地址(即虚拟地址)的概念,直接这样算出物理地址cs*16+ip(是为了克服在实模式下,16bit的寄存器无法寻址20bit地址总线的内存空间的问题)
      -代码段寄存器CS表示程序的段地址
    • 数据段寄存器DS表示操作数的段地址
    • 堆栈段寄存器SS表示堆栈的段地址
    • 附加段寄存器ES表示辅助据段的段地址
  • 在现在x86/x64保护模式平坦模型
    • 6个段寄存器称为cs、ss、ds、es、fs和gs
    • cs,ss,ds代码段,栈段,数据段用;es,fs,gs任意数据段通用
    • 在保护模式下,段寄存器不再存放段的起始地址,而是是存放段选择子
      段的属性
  • 段基地址( Base Address)
    段基地址规定线地址空间中段的开始地址
  • 段界限(Limit)
    段的大小(20位表示,段属性中的G位指定粒度为1B或4KB,就是单位不同)
  • 基地址和界限定义了段所映射的线性地址的范围,超出范围外的内存就是不合法的了。
  • 段有两种增长方向(由段其他属性指定 ):低地址往高地址(代码段);从高地址往低地址(堆栈段)
  • 段的分类
    代码段(CS),数据段(DS,ES),栈(SS),状态段(TR,在进程切换的时候用来保存进程的上下文寄存器中的值)
  • 任务状态段
    • TSS( Task State Segment),是操作系统在进行进程切换时保存进程现场信息的段,即保存CPU中各寄存器(如CS,EIP,ESP, eflags等等)的值,实现任务的挂起和恢复。A,B进程切换。
    • TSS由104字节组成。
    • 任务状态段寄存器TR:当前任务的在务状态段描述符选择子(类似于CS,DS)
      段描述符
  • 段描述符:表示段的基地址界限属性的数据结构,占8个字节(64bit,低32位和高32位)
  • 基地址(32位)分开储存,段界限(20位)分开存储
  • G:段界限粒度( Granularity位:0表示界限粒度为1字节;1表示界限粒度为4K字节)
  • DB:代码段(D):1表示32位地址和操作数,0表示16位地址和操作数;
    1
    2
    向下展数据段(B):1表示上部界限为4G0表示64K
    栈段(B):1表示32位栈顶指针ESP,0表示16位SP
  • AVL位是软件可利用位:忽略
  • P:存在 Present位,1存在内存,0表示不存在访问异常
  • DPL:描述符特权级( Descriptor Privilege level共2位,(0,3)规定了所描述段的特权级,用于特权检查。
  • TYPE:存储段的具体属性(4bit)

  • 代码段描述符:代表代码,它可以放在GDT或LDT中。置S标志为1。
    • 数据段描述符:代表数据段,代表代码它可以放在GDT或LDT中。置S标志为1。
  • 任务状态段描述符:代表任务状段,用于保存处理器奇存器的内容。它只能出现在GDT中,根据据相应的进程是否正CPU上运行,其Type字段段的值分别为11或9。置S标志置为0.
  • 一致代码段低级别(度用层,3级)能访问的高级别(内核级,0级)共享出来的代码段。非一致伏码段就是只能本级别访问的代码段,低级不能访问高级的,高级不能访问低级。比如rpl是3,dpl是0,属于低级别访问高级别代码,在C=1时,即一致性代码段情况下,合法,否则不合法。
  • S:代码段和数据段为1,任务状态段(TR)为0
    段描述符表(GDT,LDT)
    段描述符是存放在段描述符表里的。有三种类型的描述符:
    • 全局描述符表GDT( Global Descriptor Table),表起始地址存放GDTR寄存器中
    • 局部描述符表LDT( Local Descriptor Table),表起始地址存放LDTR寄存器中
    • 中断描述符表IDT( Interrupt Descriptor Table),表起始地址存放IDT寄存器
  • 全局描述符表GDT和中断描述符表IDT只有一张,局部描述符表可以有若干张(每个任务可以有一张)
  • 全局描述符表GDT含有每个任务都可能或可以访问的段的描述符,通常包含描述操作系统所使用的代码段、数据段和堆栈段的描述符,也包含一些特殊的数据段描述符。就是说,全局描述符表GDT是可以被共享的
  • 通过LDT可以使各个任务私有的段与其它任务相隔离,从而达到受保护的目的。(比如某个进程的指针跑飞了,至少不会破坏别的进程的数据)而通过GDT可以使各任务都需要使用的段能够被共享。
  • 一个任务可使用的整个虚拟地址空间分为相等的两半,一半空间的描述符在GDT中,另一半空间的描述符在LDT中。(比如32bit系统中,地址空间为4G,高地址2G(内核的)存放GDT中,低地址2G(应用进程的)存放LDT中)
  • 在任务切换时,切换LDT,并不切换GDT
    段选择子
  • 保护模式下段寄存器(CS,DS,ES,SS等)里存放的是段选择子,16bit,分为3部分:

    • RPL:最低两位是请求特校级RPL( Requested Privilege Level),用于特权检查,0最高级别,3最低级别。CPU的当前特权级CPL,就是内核层和应用层)每当一个代码段选择子装入CS寄存器中时,处理器自动地CPL存放到CS的RPL字段。
    • TI:段选择子的第2位是引用描述符表指示位,标记为TI( Table Indicator)Tl=0指示从全局描述符表GDT中读取描述符:T=1指示从局部描述符表LDT中读取描述符。
    • Index段选择子的高13位是描述符索引(index)所谓描述符索引是指段描述符在描述符表的序号,总共8192个索引。
  • 段描述符高速缓寄存器
    • 提高访问速度,不用去遍历GDT和LDT表了
    • 对程序员而言它是不可见

      分段机制将逻辑地址转换成线性地址的过程

      逻辑地址
      • 保护模式下,逻辑地址是编译器生成的进程地址空间,类似的逻辑地址,甚至很多可能相同的,使用c语言指针的值就是逻辑地址。逻辑地址由段选择子:段内偏移组成
  • 实模式的“实”体现在程序中用到的地址都是真实的物理地址,没有逻辑地址的概念,段基址:段内偏移地址产生就是物理地址,即程序员可见的地址完全是真实的内存地址。
    线性地址
  • 实模式分段模型下使用是用段寄存器左移4位+基址寄存器(cs<<4 + ip)来间接寻址,实模式下没有逻辑地址(即虚拟地址)的概念,也没有线性地址,直接这样算出地址cs<<4 + ip,所有的地址都是物理地址。
  • 保护模式
    • 分段的概念发生变化,引入GDT和LDT段描述行表的数据结构来定义每个段。cs,ds存放的不再是段地址,而是存放的是段选择子,但不叫分段模型,而是称为平坦模型
    • 根据段选择子去从GDT或者LDT表中读取段描述符,段描述符中的32位基地址,20位段界限段界限粒度定义了段所映射的线性地址的范围,然后用段的基址 + 段内的偏移量,就得到了对应的线性地址。
      物理地址
      物理地址是CPU在存取数据时最终在存取数据时地址总线上发的电平信号,靠该地址来访问对应数据。
      • 保护模式下,要得到物理地址,必须要将逻辑地址经过分段,分页等机制转化而来。物理地址 = 物理内存页的基地址+页内偏移,即(CR3[页目录项])[页表项]+页内偏移
      • 实模式分段模型下使用是用段寄存器左移4位+基址寄存器(cs<<4 + ip)来间接寻址,实模式下没有逻辑地址(即虚拟地址)的概念,也没有线性地址,直接这样算出地址cs<<4 + ip,所有的地址都是物理地址。

        分页机制将线性地址转换成物理地址的过程

        PDE和PTE解析(x86)
  • 在x86系统中,线性地址是32bit,分为3部分
  • X64系统中,线性地址是48bit,分为5部分,除了页内偏移,PDE,PTE还有:
    • PML4 entry在线性地址39~47bit用于索引PML4 entry,指向PDP
    • PDP entry-在线性地址的30~38bit用来索引 PDP entry指向PDE
      OFFSET
      OFFSET (物理内存的页内偏移,低12位,因为物理内存页是以4K为单位)
      PTI
      PTI(页表项索引,表示页表中某一项的下标,10位,因为一个页表的页表项是存放在一个物理内存页中的,每一项在x86系统中是占4个字节,4KB/4B=1024个页表项,需要10bit来表示)
  • PTE(页表项),格式如下:
  • 物理内存页的基地址,高20bit,,下一级物理内存页的起始地址,因为物理内存页是对齐4K的,也就是以4K为单位,即物理内存页的起始地址能被4k整除,所以低12bit都为0。故低12bit就不用存储了,只需要存储高20bit的即可,恢复的时候,物理内存页的基地址<<12 + 12bit页内偏移地址即可恢复完整的物理内存页的地址。
  • 剩下12bit表示物理内存页的属性
    • P:有效位。0表示表项无效
    • R/W:0只读;1可读可写,修改可读可以把内存变成可写
    • U/S:0表示R3程序可访,1表示只能R0级可访问,修改可以改变访问权限
    • PWT(Page Write Through)
      1
      PWT=1时,写数据到缓存( CPU cache)同时将数据写入内存。
    • PCD (Page Cache Disable)
      1
      PCD=1时,禁止的某个页写入缓存,直接写内存。
    • A:0表示该页未被防问,1该已访问。
    • D:脏位,0表示该页未写过,1该页己写过,
      -PAT页表属性
    • G:全局标志,G=1,刷新TLB时将不会刷新PDE/PTE的G位为1的页,G=1时,切换进程该PTE仍有效。
    • AVL(Avail):仅用于多处理器系统;指明可读写或只读
      PDI
      PDI(页目录项索引,表示页目录表中某一项的下标,10位,因为页目录表也是存放在物理内存页中的,每一项在x86系统中是占4个字节,4KB/4B=1024个页目录项,需要10bit来表示)
  • PDE(页目录项),格式如下
  • 页表基地址,高20bit,下一级页表的起始地址,因为页表存放在一个物理内存页中,是对齐4K的,也就是以4KB为单位,即所有页表基地址都能被4k整除,所以低12bit都为0。故低12bit就不用存储了,只需要存储高20bit的即可,恢复的时候,20bit页表基地址<<12+10bit的PTI<<2即可恢复完整的页表地址。
  • 剩下12bit表示页表属性
    • PS( Page Size):只存在于页目录表项中。0表示这是KB页,指向一个下级页表。1表示这是4MB大页,直接指向物理页(此时32位的虚拟地址分为两段,低22位为页内偏移,2^22=4MB)
    • 其他属性与上述PTE一致
      CR3
      CR3寄存器存放的是页目录表的基地址
      TLB
      TLB( Translation Lookaside Buffe),地址翻译缓存表,"快表",存线性地址翻译的物理地 址,线性地址<- ->物理地址。把上一次线性地址转化成物理内存的地址的记录存到TLB中,下一次先查TLB,如果有对应的记录,就不需要重新地址转换了,提高效率。
      CPU cache
      CPU cache:容量比内存小但访间速度快,缓存物理内存中的数据:物理地址<- ->数据
      结合pte修改内存只读实验
      1.编写实验代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdafx.h>
#include <conio.h>
int main(int argc, char* argu[])
{
    /// 设置一个静态区的字符串
    char *str = "hello world";
    /// 打印静态区的内存地址(虚拟地址),打印静态区的字符串
    printf("addr=%p, s\n", str, str);
    printf("Plase press any key\n");
    /// 让程序暂停;
    getched();
    /// 修改静态区内存的值
    *str=A;
    printf("%s\n", str);
    return 0;
}
2.准备调试环境
  • xp系统安装VC6等IDE
  • 安装 windbg,准备好XP的符号
    3.修改pe只读属性
  • !process 0 0 显示所有线程的信息
  • !vtop diraddr vaddr将虚拟地址转换成物理地址,查看到PTE
  • !ed PIEaddr修改PTE的R/W字段为1,从而将静态区内由原来的只读变为了可读可写
  • 按回车让程序继续执行,可以发现修改静态区字符串的值已经被修改了
    PDPT,PDE和PTE解析(x86 PAE,PAE指的是将物理地址从32位扩大到36位,是因为硬件实际上有36根地址总线,默认只是开放了32根。)
  • OFFSET (物理内存的页内偏移,低12位,因为物理内存页是以4K为单位)
  • PTI(页表项索引,表示页表中某一项的下标,9位,因为一个页表的页表项是存放在一个物理内存页中的,每一项在x86系统中是占8个字节,4KB/8B=512个页表项,需要9bit来表示)
  • PDI(页目录项索引,表示页目录表中某一项的下标,9位,因为页目录表也是存放在物理内存页中的,每一项在x86系统中是占8个字节,4KB/8B=512个页目录项,需要9bit可以表示)
  • PDPT页目录指针表索引,2位
  • CR3寄存器存放的是页目录指针表的基地址
  • 在x86 开启PAE的情况下4PDPTE*512PDE*512PTE=2^20Pages*4KB/page=2^32B虚拟内存地址依然是32bit,怎么支持到36位了呢?虚拟地址不是和物理地址一一对应的吗?
    尝试理解:地址总线是36位的,将36位物理地址存放在8字节中,页表基地,页目录基地址,不再是20bit(32-12),而是24bit了,也就是说物理页的基地址是24bit,所以应该这样算4PDPTE*512PDE*512PTE=2^20Pages*(4KB*16)/page=2^36B但在x86 开启PAE的情况下4PDPTE*512PDE*512PTE这个三维数组和在x86 没有启PAE的情况下的二维数组1024PDE*1024PTE的元素个数依然一样,但地址由20bit变成了24bit,那每个元素的基地址2^24/2^20=16,即一个页表项可以可以对应16个4k页,即一个页表项代表的物理页的基地址的跨度是跨越了16个4k页的大小,而偏移量还是12位,不就会导致剩余15个4k页的物理内存都没有对应的虚拟地址吗?那这样还是只支持4G啊
    • //@todo:PAE 暂时先这样理解吧,在x86没有开启PAE,cr3寄存器只作为进程的目录基地址的容器;但如果在在x86开启了PAE,cr3寄存器除了作为进程的目录基地址,为了建立虚拟地址和物理地址一对一的映射关系,cr3寄存器还会被修改,使其指向不同的PDPT,那总的PDPT目录指针表不止1份,不止是2^2项目了,而实际上最大可以存放2^2*16=64项了。
X86 PAE虚拟地址映射物理地址寻址实验
  • PAE(Physical address extend),物理地址扩展,扩展物理内存地址从32位到36位,支持最多64GB的物理内存(页面然小为4KB)。为X86系统开启PAE方法(物理内存小于4G,开启无意义)
    编写测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdafx.h>
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include <conio.h>
int main(int argc,char* argv[])
{
    /// 在堆上分配1024个字节
    char *str (char *)malloc(1024);
    if(str == NULL)
        return -1;
    /// 初始化
    memset(str,0,1024);
    /// strcpy()有安全风险,可能会造成栈溢出。
    /// 因为这个函数没有方法来保证有效的缓冲区尺寸,所以它仅仅能假定缓冲足够大来容纳要拷贝的字符串。
    /// 在程序执行时,这将导致不可预料的行为,容易导致程序崩溃,最好使用strcpy_s()。
    strcpy_s(str,"hello world");
    /// 打印堆的内存地址(虚拟地址),打印堆的字符串
    printf("addr of str:%p,%s\n",&str,str);
    /// 让程序暂停
    getch();
    /// 手动释放堆内存
    free(str);
    str NULL;
 
    return 0;
}
开启PAE
  • WIN7:bcdedit
    • 1.管理员方式打开CMD.
    • 2.在cmd中输入:bcdedit/set pae forceenable这里的bcdedit,是关于命令行的启动配置编辑器。使支持的物理内存大于4GB
    • 3.开启应用程序默认为3GB(3*1024):bcdedit/set increaseuserva 3072因为32位windows默认应用程序只能使用2G内存,剩下的都保留给系统内核了,所以还要开启3GB
  • XP:boot.ini
    multi(0)disk(0)rdisk(0)partition(1)\WINDOWS=Microsoft Windows XP Professional /noexecute=optin /fastdetect后加上/PAE
    multi(0)disk(0)rdisk(0)partition(1)\WINDOWS=Microsoft Windows XP Professional /noexecute=optin /fastdetect/PAE
    打开windbg本地调试功能
  • 以管理员权限运行:bcdedit -debug on打开local debug功能,然后重启系统,这样,系统的本地调试功能就打开了。在本地调试部分,主要是利用windbg来查看一些系统数据信息,常见的数据结构以及内容等。
    设置符号
    开始调试
    • !process 0 0显示所有进程的信息
      • PROCESS 82243020 SessionId:0 Cid:01e8 Peb:7ffd3000 ParentCid:01f4 DirBase:07880320ObjectTable:e11b2470 HandleCount:12. Image:addr.exe
      • 07880320 页目录的基地址,用这个基地址可以算出物理地址
  • .process /p eprocess进入进程上下文,因为应用层各进程的内存之间是隔离的,切换到对应进程才能查看对应内存的数据
  • !dq addr+pdpe*8 L1查看页目录基地址
    • 地址总线是36位的,!dq是以8字节查看的虚拟地址的值,L1只查看一个单位
    • 得到的地址去除0-11(对齐到4K),取12-35位+pde*8
  • !dq addr+pde*8查看页表基地址
    • 得到的地址去除0-11(对齐到4K),取12-35位+pte*8
  • !dq addr+pte*8 L1查看物理内存页基地址
    • 得到的地址去除0-11(对齐到4K),取12-35位+offset即可得到物理地址
  • !dc paddr即可查看到里面的内容验证
  • !vtop DirBase vaddrwindbg提供直接把虚拟地址转换成物理地址
PML4,PDP,PD和PT解析(x64)


  • x64中是使用了低48位,每级页表占9位,共4级,缩写分别为:PML4,PDP,PD,PT。
  • !pte vaddr查看地址翻译过程
  • !vtop dirbase vaddr虚拟地址转换为物理地址
  • CR3(R3和R0公用导致了meltdown&spectre漏洞)
  • 1985年80386刚出来时,直接寻址64K时,有谁想到仅仅过了37年的现在2022年普通用户16GB都不够用了(大概翻了256倍)。未来会不会也不用30年,或许在我挂之前,64位硬件真的普及起来,4G*4G(是现在1024倍),那时候应该是个什么样的数据时代。

    meltdown&spectre漏洞

  • 指令在执行的时候会分支预测&乱序执行&CPU缓存旁路攻击
  • 漏洞原理:在执行异常指令3时,出现异常,后面的指令不会被执行,但由于分支预测和乱序执行,将提前读取指令4的非法内核内存数据(当前用户态程序不可见)到CPU缓存中,但不会放到寄存器中(当前程序可见)经过合法性检测之后,将缓存中的数据放入寄存器,供当前程序访问,
  • CPU缓存:用户态和内核态都无法正常访问,边信道攻击猜测,继而访问主机的完整内存。
  • 边信道攻击:也叫旁路攻击针对加密电没备运每程中的时间消耗、功率消耗或电磁辐射之类的侧信道信息泄露而对加密设备进行攻击的方法被称为边信道攻击这类新型攻击的有效性远高玉密码分析的数学方法,因此给密码设备带来了严重的威胁
  • 旁路攻击的实例
  • 修复方法:KPTl(Kernel Page Table Isolation)或KAISER(Kernel Address Isolation to have Side-channels Efficiently Removed)
  • KPT会将内核内存与用户态进程分开,也就是将内核移到一个完全独立的地址空间中,所以对于普通进程来说是不可见的。KPT技术利用了Intel处理器的地址切换方式、缓存数据转储以及内存数据的重载。打了KPT1补丁之后内核访问用户态内存都要切CR3了,TLB全清
  • 采用shadow页表技术,R3,R0用不同的页表,内核她趾在R3中织有极少数被映射,大部分都无效,R0中的都有效,并且R3地址也都能访问,只通过SMAP和SMEP来进行保护。
  • SMEP(Supervisor Mode Execution Prevention)当设置了CR4存器的控制位时,会保护特权进程(比如在内核态的程序)不能在不含supervisor标老(对宇ARM处理器,"就是PXN标志)的内存区域执行代码,其实就是内核程序不能跳转到用户态执行代码。
  • SMAP(Supervisor Mode Access Prevention),内核态代码不能读写用户态的内存数据,SEMP控制执行,SMAP控制读写,为了相互通信,通过修改标志位,在某位置临时取消SMAP,实现精确位置的读写
  • 新的UserDirectoryTableBase用来保存R3的CR3,而原来的DirectoryTableBase则为R0的CR3。
1
2
3
4
5
6
7
8
9
10
0:kd>dt nt!_EPROCESS @Sproc ImageFileName Pcb.DirectoryTableBase
Pcb.UserDirectoryTableBa
*** ERROR:Module load completed but symbols could not be loaded for LiveKdD.SYS
+0x000 Pcb
+0x028 `DirectoryTableBase`:
0x1ab002
+0x278 `UserDirectoryTableBase`:
:0x2f00001
+0x450 ImageFileName:
[15]"System"

其他与内存相关的概念

  • Swap分区:在系统的物理内存不够用的时候,把硬盘空间中的一音放出来,以供当前运行的程序使用。
  • page in:分页(Page)从磁盘重新回到内存的过程
  • page out:分页(Page)写入磁盘的过程
  • Page Fault:当内核需要一个分页时,但发现此分页不在物理内存中因为已经被Page-Out了),此时就发生了分页错误Page Fault,CPU会产生一个hard page fault中断。
  • Hard page fault也称为major page fault,指需要访问的内存不在虚拟地址空间,也不在物理内存中,需要从慢速设备(也就是磁盘的Swap分区)载入。从swap回到物理内存也是hard page fault。
  • 与之相对的minor page fault也称为soft page faut,指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系(建立一个页表)可。比如多个进程访问同一个共享内存中的数据,某些进程还没有建立起映射关系,会出现soft page fault
  • invalid faultt也称为segment fault,指进程需要访问的内存地址不在它的虚拟地址空间范围内(在保护模式,每个段有段的基地址和段的大小,如果段的偏移超过了段的大小,就不在有效范围内了)属于越界访问,内核会报segment fault错误。

    程序(代码+数据)是如何存储的?

    系统虚拟内存空间布局

  • X86支持32位寻址,因此可以支持最大2^32,即4GB的虚拟内存空间(当然也可以通过PAE将寻址空间扩大到64GB,PAE即Physical address extension,x86的处理器增加了额外的地址线以选择那些增加了的内存,所以内存的大小从32位增加到了36位,最大的内存由4GB增加到了64GB。
  • X64内存理论上支持最大2^64的寻址空间,但实际上这个空间太大了,目前根本用不完。因此实际上x64系统一般都只支持到40多位(比如Windows:支持44位最大寻址空间为16TB,Linux支持48位最大寻址空间256TB等),支持的空间达到了TB级别。但是,无论是在内核空间还是在应用层空间,这些上TB的空间并不都是可用的,存在着所谓的空洞(HOLE)

    程序的内存空间布局

  • 一个程序加载入内存之后,就处在进程空间。

    变量(即数据)

    按变量的类型分类

    C 数据类型 | 菜鸟教程 (runoob.com)

@todo有空补充完整

  • 整数的表示
    • 原码
    • 反码
    • 补码
  • 浮点数
  1. 字符
  • 字符
    • ASSIC
    • UNICODE
    • GBK2312
  • 字符串
    • 宽字节字符串
    • 多字节字符串
  1. 地址
    • 指针
    • 引用
    • 传参问题
    • 内存泄漏问题
  2. 存储方式
    • 低位优先
    • 高位优先

      按变量的定义的位置来分类

  • 全局变量
    • 全局变量的初始化顺序,在同一源文件则按照变量定义的先后顺序初始化,如果分散在不同的源文件,则初始化顺序无法预料。
  • 局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/// 不要返回局部变量的指针或引用,能通过编译,但程序执行会出问题
/// 指针和引用都是对应局部变量的地址,函数退出的时候,局部变量就被销毁了
char *func(void)//err
{
    char c = 'x';
    return &c;
}
char &func(void)//err
{
    char c = 'x';
    return c;
}
 
/// 返回局部变量的值,是可以的,局部变量销毁之前会被拷贝出来
char func(void)//ok
{
    char c = 'x':
    return c;
}
 
/// 返回局部变量指针的值(但局部变量的指针存放的是的堆上的地址),是可以的,局部变量销毁之前会被拷贝出来
/// 返回裸指针,可能会忘记释放,不建议这样使用,一定要使用的话,记得使用引用计数或者智能指针
char *func(void)//ok
{
    char *c = (char *)malloc(100);
    return c;
}
/// 返回局部变量指针的地址,和第一种情况相同
char *func(void)//err
{
    char *c = (char *)malloc(100);
    return &c;
}
 
/// 综上,可以发现只要返回的变量在退出函数的时候不被销毁即可
/// 但不建议这样做,不要为了返回局部量的地址,而将局部变量改成局部静态变量,破坏了局部变量的作用域,在多线程竞争会出问题,要考虑多线程安全,得不偿失
char &func(void)//ok
{
    static char c = 'x';
    return c;
}
  • 外部变量
    • 比如在1.c定义全局变量int a;在2.c中声明extern int a;即可使用变量a
  • 全局静态变量
    • 比如在1.c定义全局静态变量static int a;在2.c中声明extern int a;也无法使用变量a,为了防止命名冲突(避免项目的其他开发成员使用同样的变量名字造成冲突)
  • 局部静态变量
    • 分配的内存也是在静态存储内存上的,其第一次初始化后就一直存在直到程序结束。其作用域只在定义它的函数内可见,出了该函数就不可见了。
1
2
3
4
5
6
7
8
void func(void)
{
    ...
    /// 只在初始化化一次,之后不会再初始化了,带有记忆功能,函数退出之后过了,
    /// 依旧保留上一次的结果(来世投胎还带有前世的记忆),很有意义的特性
    static int c = 0;
    ...
}
  • 常量常变量
    • 常量:没有名字的不变量
    • 常变量:有名字的不变量。常变量具有变量的基本属性:有类型,占存储单元,只是不允许改变其值。有名字就便于在程序中被引用。 C99允许使用常变量
1
2
3
4
5
6
7
8
/// 定义数字常量
#define PI 3.1415926
 
///  定义数字常变量
const float pi = 3.1415926;
 
/// 定义字符串指针变量str 指向字符串常量"hello world!"
char str[] = "hello world!";
  • 寄存器变量
    • 在程序运行时,根据需要到内存中相应的存储单元中调用,如果一个变量在程序中频繁使用,例如循环变量,那么,系统就必须多次访问内存中的该单元,影响程序的执行效率。因此,C语言/C++语言还定义了一种变量,不是保存在内存上,而是直接存储在CPU中的寄存器中,这种变量称为寄存器变量。
    • 寄存器变量的定义形式是:register 类型标识符 变量名
    • 寄存器是与机器硬件密切相关的,不同类型的计算机,寄存器的数目是不一样的,通常为2到3个,对于在一个函数中说明的多于2到3个的寄存器变量,C编译程序会自动地将寄存器变量变为局部变量
    • 由于受硬件寄存器长度的限制,所以寄存器变量只能是charint指针型。寄存器说明符只能用于说明函数中的变量和函数中的形参),因此不允许将外部变量静态变量说明为register
    • register型变量常用于作为循环控制变量,这是使用它的高速特点的最佳场合。比较下面两个程序的运算速度。
    • 由于register变量使用的是硬件CPU中的寄存器,寄存器变量无地址,所以不能使用取地址运算符"&"求寄存器变量的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/// 这两个程序中,前者使用了两个寄存器变量,后者使用了两个局部变量,
/// 程序除此之外完全一样。但运行时感觉的执行速度是不同的,前者使用寄存器变量的程序要比后者使用自动变量的程序要快。
 
/* 程序1 */
#include <stdio.h>
int main ( )
{
    register int temp, i;
 
    for ( i=0; i<=30000; i++ )
 
        for ( temp=0; temp<=100; temp++ ) ;
 
            printf ("ok\n");
    return 0;
}
 
/* 程序2 */
#include <stdio.h>
int main( )
{
    int temp, i;
 
        for ( i=0; i<=30000; i++ )
 
            for ( temp=0; temp<=100; temp++ ) ;
 
                printf ("ok\n");
    return 0;
}

变量的作用域

作用域:变量的可见代码域(块作用域,函数作用域,类作用域,程序全局作用域)。取决于变量定义位置。

  • 整个项目:全局变量、外部变量、常量、常变量(全局的)
  • 当前源文件:全局静态变量。
  • 所属函数内部:局部变量、局部静态变量、寄存器变量、常变量(定义在函数内部的)。

    变量的存储位置

  1. 静态区
    • .data数据段存放初始化的全局变量和静态变量
    • .bss存放未初始化的全局变量和静态变量
1
2
3
4
5
6
7
8
9
10
11
/// 未初始化的全局变量,存放在静态区的.bss,.bss是不占内存的,因为里面存放的都是0,可以压缩存储
int g_array[100*1024*1024];
/// 初始化的全局变量,应该存放在静态区的.data,但编译器可能会将其优化,存放的.bss
int g_array[100*1024*1024] = {0};
/// 初始化的全局变量,存放在静态区的.data,编译出来的.exe文件会膨胀到4*100MB
int g_array[100*1024*1024] = {1};
int main(void)
{
    printf("hello world\n");
    return 0;
}
1
2
3
4
5
- .rdata,.data,.bss都是存放的数据。
- 除了.bss段.rdata,.data段的值都是在编译的时候就确定了,并且将其编译进了可执行文件,经过反汇编都能找得到。.bss段是在代码运行的时候手动编写汇编代码将其初始化为0的(这就是未初始化的全局和静态变量默认值为0的根源)bss不占据实际的磁盘空间,只在段表中记录大小,在符号表中记录符号。当文件加载运行时,才分配空间以及初始化
- `.rdata只读数据段`存放常量和常变量
    - 函数中的 "abcde "这样的字符串存放在常量区
- 在所有函数体外定义的是全局量,加了static修饰符后不管在哪里都存放在静态区,在所有函数体外定义的static变量表示在该文件中有效,不能extern到别的文件用,在函数体内定义的static表示只在该函数体内有效。
  1. .text代码区存放程序执行的指令
  2. (heap),堆区内存的分配和释放需要使用malloc/free,不是自动分配的。一般由程序员分配释放,若程序员不释放,虽然程序结束时所有的数据空间都会被释放回系统,但是精确的申请内存/释放内存匹配是良好程序的基本要。
    • 注意它与数据结构中的堆是两回事。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。虽然堆栈,堆栈的说法是连起来叫,但是他们还是有很大区别的,连着叫只是由于历史的原因。
  3. (stack),由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    • 在函数体中定义的变量通常是在栈上
    • 函数调用时会在栈上有一系列的保留现场及传递参数的操作。栈的空间大小有限定,vc的缺省是2M。栈不够用的情况一般是程序中分配了大量数组和递归函数层次太深。
    • 当一个函数调用完返回后它会释放该函数中所有的栈空间。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>
    int a = 0; ///< 初始化的全局变量,存放在静态区.data上
    char _p1; ///< 未初始化区的全局变量 ,存放在静态区.bss上
    /// 函数的参数,返回值在栈上
    int main(int argc, char* argu[]) 
      int b; ///< 局部变量在栈上
      char s[] = "abc"; ///< 局部变量在栈上
      char *p2; ///< 局部变量在栈上
      char *p3 = "123456"; ///< 常量字符串"123456\0"在存放在静态区.rdata上,但局部变量p3在栈上。 
      static int c = 0///< 初始化的静态变量在静态区.data上,若未初始化则在静态区.bss上
      /// 分配得来得1020字节的内存在堆上。 
      p1 = (char_ )malloc(10); 
      p2 = (char *)malloc(20);
      /// 常量字符串"123456\0"放在静态区的.rdata上,编译器可能会将它与p3所指向的"123456"优化成一个地方。
      strcpy(p1, "123456");
    }
堆和栈的区别
  • 申请方式
    • 栈:由系统自动分配与回收,intb=0,增长由高到低
    • 堆:malloc/free,地址由低到高
  • 大小限制
    • 栈:应用层1M到10M,内核层:12K到24K不等(所以内核层不能使用递归,避免栈溢出)
    • 堆:受限于计算机系统中有效的虚拟内存。
  • 申请后系统的响应
    • 栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存 ,否则将报异常提示栈溢出。
    • 堆:首先应该知道操作系统有一个记录空闲内存 地址的链表,当系统 收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配 给程序,另外,对于大多数系统,会在这块内存 空间中的首地址处记录 本次分配 的大小,这样,代码中的delete语句才能正确的释放本内存 空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表 中。
  • 申请效率比较
    • 栈:由系统自动分配,速度较快。但程序员是无法控制。(指针函数不能够返回栈上的内存的地址,因为当函数执行完后,地址就无效了)
    • 堆:速度比较慢,而且容易产生内存碎片(明明系统存在内存,但想要分配的时候却分配不到),但可以使用一些机制来减少内存碎片。
  • 存放内容
    • 栈:栈是用来记录程序执行时函数调用过程中的活动记录(栈帧),参数,返回地址,ebp,局部变量等,有明确的类型和格式
    • 在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由 右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
    • 当本次函数 调用结束后,局部变量先出栈,然后是参数,最后栈顶指针 指向最开始存的地址,也就是主函 数中的下一条指令,程序由该点继续运行。
    • 堆:一般是在堆的头部用一个字节存放堆的大小,剩余部公存储的内容,由程序员根据程序计算的需要决定,比较灵活,没有明确的类型和格式。
  • 存取效率的比较
1
2
3
4
5
/// aaaaaaaaaaa 是在运行时刻赋值的;
/// 而 bbbbbbbbbbb是在编译时就确定的;
/// 但是,在以后的存取中,在栈上的数组比指针所指向的字符串(静态区或者堆)快
char s1[] = "aaaaaaaaaaaaaaa"; ///< 栈上的数组
char *s2 = "bbbbbbbbbbbbbbbbb"; ///< 栈上的指针

eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
void main()  
{  
    char a = 1;  
    char c[] = "1234567890";
    char *p ="1234567890";  
    a = c[1];
    /** a = c[1]; 对应的汇编代码
    00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]  
    0040106A 88 4D FC mov byte ptr [ebp-4],cl
    栈上的数组在读取时直接就把**字符**串中的元素读到寄存器cl中
    */
    a = p[1]; 
    /**
    a = p[1];  
    0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]  
    00401070 8A 42 01 mov al,byte ptr [edx+1]  
    00401073 88 45 FC mov byte ptr [ebp-4],al 
    栈上的指针在读取时则要先把**指针**值读到edx中,再根据edx读取字符到寄存器al中,显然多了一步,慢了。
    */
    return;  
}
  • 数据结构
    • 栈是机器系统提供的数据结构,而堆则是C/C++函数库提供的
    • 具体地说,现代计算机(串行执行机制),都直接在代码底层支持栈的数据结构。这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统直接支持的数据类型,并不直接支持其他的数据结构。因为栈的这种特点,对栈的使用在程序中是非常频繁的。对子程序的调用就是直接利用栈完成的。机器的call指令里隐含了把返回地址推入栈,然后跳转至子程序地址的操作,而子程序中的ret指令则隐含从堆栈中弹出返回地址并跳转之的操作。C/C++中的自动变量是直接利用栈的例子,这也就是为什么当函数返回时,该函数的自动变量自动失效的原因。
    • 堆的数据结构并不是由系统(无论是机器系统还是操作系统)支持的,而是由函数库提供的。基本的malloc/realloc/free函数维护了一套内部的堆数据结构。当程序使用这些函数去获得新的内存空间时,这套函数首先试图从内部堆中寻找可用的内存空间,如果没有可以使用的内存空间,则试图利用系统调用来动态增加程序数据段的内存大小,新分配得到的空间首先被组织进内部堆中去,然后再以适当的形式返回给调用者。当程序释放分配的内存空间时,这片内存空间被返回内部堆结构中,可能会被适当的处理(比如和其他空闲空间合并成更大的空闲空间),以更适合下一次内存分配申请。
    • 和栈不同,这套复杂的分配机制实际上相当于一个内存分配的缓冲池(Cache),使用这套机制有如下若干原因:
    • 系统调用可能不支持任意大小的内存分配。有些系统的系统调用只支持固定大小及其倍数的内存请求(按页分配);这样的话对于大量的小内存分类来说会造成浪费。
    • 系统调用申请内存可能是代价昂贵的。系统调用可能涉及用户态和核心态的转换。
    • 没有管理的内存分配在大量复杂内存的分配释放操作下很容易造成内存碎片。

      变量的生命周期

      生命周期:变量从定义到销毁的时间范围。取决于变量的存储位置。
  • 整个程序运行期间:静态区.data、静态区.bss、静态区.rdata
  • 手动释放:堆
  • 所属函数执行期间:栈

    变量的存储长度

    基本类型的长度
  • 调试程序和漏洞分析的时候需要对变量的存储长度有精准的把握。
    sizeof()是运算符,和+-*/类似,运算符在编译阶段提前算好了,而大部分函数只有在运行期间才出结果。
    sizeof()替换硬编码,提高软件的可移植性
  • sizeof(char) = 1
    • sizeof(wchar_t) = 2(window平台),4(Linux平台),虽然wchar_t表示是unicode编码,但unicode有多种编码:utf8(web)、utf16(window)、utf32(Linux平台)
  • sizeof(short) = 2
  • sizeof(int) = 4
    • 无论是x86还是x64都是4Byte
    • 在早期的16位系统,是2Byte
  • sizeof(long) = 4
    • 4,在x86和x64,windows平台
    • 8,在gcc中
    • window平台下_int64LARGE_INTEGER是8Byte,这类关键字是在微软自家的VC编译器所扩展的类型,但没有进入C标准。
    • linux平台下long long 是8Byte,在C99标准中才被引进的。
  • sizeof(float) = 4
  • sizeof(double) = 8
  • sizeof(double *) = 4
    • 在x86,4Byte
    • 在x64,8Byte,因为地址64位,指针是用来存放地址的
  • sizeof(char *) = 4
    • 同上
  • sizeof(bool) = 1
  • sizeof("123456") = 6,包含后面的'\0',"123456\0"
  • sizeof(100i64) = 8,
    • DDK新加一种64位长整型整数,无符号形式,0到2^64-1用LONGLONG表示,比如LONGLONG test = 100i64,后面加上i64结尾
      复合类型的长度
      sizeof(数组)=?
  • sizeof(charstr[10]) = 10
  • sizeof(intstr[10]) = 40
  • sizeof(floatstr[10]) = 40
  • ...
    sizeof(struct)= ?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/// 规则1:结构体对齐,满足自然对齐的法则:结构体成员的起始地址必须是结构体该成员类型长度的整数倍
/// 在x86系统下,单位是Byte
/// bool同char
/// char类型长度是1,所以char成员可以存放在任意地址上(任意地址都是1的整数倍)
/// short类型长度是2,所以short成员可以存放在偶数地址上(偶数地址都是2的整数倍)
/// int类型长度是4,所以int成员可以存放在4的整数倍的地址上
/// long类型长度是4,所以long成员可以存放在4的整数倍的地址上
/// float类型长度是4,所以float成员可以存放在4的整数倍的地址上
/// double类型长度是8,所以double成员可以存放在8的整数倍的地址上
/// 指针类型的长度是4,所以指针成员可以存放在4的整数倍的地址上
/// char arry[n]定长数组是按照数组的基本类型来对齐,所以arry[n]可以存放在sizeof(char)的整数倍的地址
/// 规则2:计算出来的总大小必须是最大类型长度的成员的整数倍,因为最大基本类型的长度跟系统的地址总线是一致的,这样就保证了cpu在一个cpu时钟周期内一次性拿到变量的完整数据
/// eg:先按照规则1来计算
typedef atruct _a
{
    char c1; ///< 1Byte,addr:0
    long i; ///< 4Byte,addr:4
    char c2; ///< 1Byte,addr:8
    double f; ///< 8Byte,addr:16,总长度:24Byte,也符合规则2,所以sizeof(a) / sizeof(f)=24/8=3能整除
}a;
/**
     ------ ...0
    |  c1  |
      ------ ...1
    | 3Byte|
      ------ ...4
    |  i   |
     ------ ...8
    |  c2  |
     ------ ...9
    | 7Bte  |
      ------ ...16
    |  f   |
      ------ ...24
*/
typedef atruct _b
{
    char c1; ///< 1Byte,addr:0
    char c2; ///< 1Byte,addr:1
    long i; ///< 4Byte,addr:4
    double f;///< 8Byte,addr:8,总长度16Byte16除以8能整除,也满足规则2
}b;
/**
     ------ ...0
    |  c1  |
      ------ ...1
    |  c2  |
      ------ ...2
    | 2Byte|
     ------ ...4
    |  i   |
     ------ ...8
    | f    |
      ------ ...16
*/
sizeof(eum)= ?
 

@todo

sizeof(union)= ?
 

@todo

变量存储为什么需要对齐
嵌套对齐@todo
自然对齐
  • 以空间换时间,只要数据按照自然对齐的方式存储的,就能保证cpu在一个cpu时钟周期从寄存器或者内存中一次性拿到变量的完整数据,提高效率。
    一字节对齐
    在网络传输中,空间更重要些,空间小传输速度更快,所以网络协议中很多结构都是pack(1),按1个字节对齐。
    栈对齐
  • 栈对齐与数据的自然对齐不同,而栈上不遵循自然对齐呢?栈是以数组为基础的数据结构,数组存放的数据是定长的(数组只能存放同一种数据类型),所以都是按照一种数据类型长度来对齐,x86下都是按照4字节对齐。
  • 调用约定
    内存节对齐
    PE(.exe)文件有很多节,把.exe从磁盘加载到内存的过程中,这些节是按照4kB来对齐的存储的,即节的基地址是是4K的整数倍。
    文件节对齐
    0000200(512B,即一个扇区大小),PE(.exe)中每个节在磁盘中是按照一个扇区的大小对齐来存储的,即文件的起始地址是512的整数倍,正是每个节在磁盘内存的对齐方式不一致,当把一个PE文件从磁盘中加载到内存中的时候,会产生很多碎片(每条指令的在磁盘中相对于文件起始地址的偏移,和这条指令在内存中相对于文件起始地址的偏移不一致),所以要做一些换算。

    综合实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/// 1.变量类型  2.作用范围 3.存储位置 4.生命周期
 
 
/// 初始化的全局变量,整个项目,静态区.data,整个程序运行期间
int a = 10;
/// 未初始化的全局变量,整个项目,静态区.bss,整个程序运行期间
char *p1;
/// 初始化的全局静态变量,当前源文件,静态区.data,整个程序运行期间
static int x = 10;
int main(void)
{   
    /// 初始化的局部变量,所属函数内部,栈,所属函数执行期间
    int b = 0;
    /// 初始化的局部变量(数组首地址也是常量指针,还是把它当作一个局部变量来看吧),所属函数内部,栈,所属函数执行期间
    /// s1[]与s2[]相比少了个'\0'
    char s0[] = {'1','2','3'};
    /// 初始化的局部变量,所属函数内部,栈,所属函数执行期间
    /// "123" 字符串常量,当前源文件,静态区.rdata,整个程序运行期间
    /// "123" 字符串常量拷贝到栈上,s1数组存放时实际的数据
    char s1[] = "123";
    /// 未初始化的局部变量,所属函数内部,栈,所属函数执行期间
    char *p2;
    /// s2 初始化的局部变量,所属函数内部,栈,所属函数执行期间
    /// "123" 字符串常量,当前源文件,静态区.rdata,整个程序运行期间
    /// "123" 字符串常量的地址拷贝到s2指针上,s2指向静态区.rdata的字符串常量,"123" 字符串在常量区只有一份
    /// s1[0] = 'a'是合法(因为栈的上的数据可以修改)
    /// *(s2+0= 'a'是不合法(因为静态区数据不可以修改),应用层会报错误码0x0000 0005
    char *s2 = "123";
    /// s3 初始化的局部变量,所属函数内部,栈,所属函数执行期间,和s1一样,只是s3数组长度是固定是100
    char s3[100] = "123456";
    /// s4 初始化的局部变量,所属函数内部,栈,所属函数执行期间,和s3一样,只是s4数组里面实际有效的字符串是"123\0",因为处理字符串的库函数处理到"\0"就认为此字符串结束了
    char s4[100] = "123\0456";
    /// s5 初始化的局部变量,所属函数内部,栈,所属函数执行期间,编译会报错,因为"123"长度是4,s5数组长度只有3
    char s5[3] = "123";
    /// sizeof(s0) = 3;sizeof(s1) = 4;sizeof(s2) = 4;sizeof(s3) = 100;sizeof(s4) = 100;sizeof(s5)无法计算,因为编译没有通过,程序中存在s5的代码
    ///strlen(s0)无法预料,可能>3,也可能程序奔溃(栈上s0后面的数据只有虚拟内存,没有物理内存,就会缺页错误,也就是虚拟内存无效),因为stlen()接受包含'\0'的字符串的参数,如果传入的不是'\0'结尾的,会发生溢出;strlen(s1) = 3;strlen(s2) = 3;strlen(s3) = 6;strlen(s4) = 3;strlen(s5) = 3;
    /// 初始化的局部静态变量,所属函数内部,静态区.data,整个程序运行期间
    static int c = 10;
    /// 初始化的局部变量(指向的内存在堆上),所属函数内部,栈,所属函数执行期间
    p1 = (char *)malloc(128);
    /// 给局部变量分配堆上的内存,所属函数内部,栈,所属函数执行期间
    p2 = (char *)malloc(256);
    free(p1);
    free(p2);
    return 0;
}

PE

  • PE(Portable Executable)文件是微软Windows操作系统上的程序文件文件格式,由微软设计的,并于1993年被TIS (tool interface standard,工具接口标准)委员会由(Microsoft,Intel,Borland,Watcom, IBM等等组成)所批准,基于COFF (common object file fromat",通用自标文件格式,多应用于UNIX等系统中的用标文件和可执行文件的格式)文件格式。常见的EXEDLLOCXSYSCOM都是PE文件。
  • 自已开发的Win程序,安装别人的程序,都有大量的PE文件。

    PE文件结构介绍



    一个完整的PE(Portable Executable)文件由DOS头,PE文件头块表调试信息(调试版本)有效组成。
  • 注意,这些结构体都是按照1字节对齐,不是自然对齐
    DOC Header
  • DOS部首由两部分组成:
    • DOS的MZ文件标志(以e_magic开头,它的值是固定的0x5a4d)和DOS stub(DOS存根程序)。
    • 之所以设置DOS部首是微软为了兼容原有的DOS系统下的程序而设立的。
    • DOS stub是一个小程序,为打印一句话“This program Cannot be run in DOS mode”。(比如win32程序放在DOC环境下运行,win32的运行在保护模式下的,在实模式下就不能运行,就会提示这句话)
    • 在DOS头部,有一个结构成员e_Ifanew指向了真正的PE头。
    • PE有效性判断: dos header e_magic和PE头部里的signature必须为固定值: MZ ( 0x5A4D )和PE00 (0x00004550)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
typedef struct _IMAGE_DOS_HEADER{ ///< DOS .EXE header
 
 WORD e_magic; ///< Magic number 4D5A,'MZ'(x86下存储是低位优先,所以反着来放,方便调试的时候正向看到MZ),"MZ"是MS-DOS开发者之一的马克茨柏克沃斯基(Mark Zbikowski) 的姓名首字母缩写
 
 WORD e_cblp; ///< Bytes on last page of file,最后一页的字节数
 
 WORD e_cp; ///< Pages in file,页的数量
 
 WORD e_crlc; ///< Relocations,重定位个数
 
 WORD e_cparhdr; ///< Size of header in paragraphs,段头的数目
 
 WORD e_minalloc; ///< Minimum extra paragraphs needed,所需要的最小附加段
 
 WORD e_maxalloc; ///< Maximum extra paragraphs needed,所需要的最大附加段
 
 WORD e_ss; ///< Initial (relative) SS value,栈段寄存器的初始值, DOC Header有很多字段在保护模式下是用不到的,只在实模式下使用,比如这个e_ss
 
 WORD e_sp; ///< Initial SP value,栈顶指针的初始值
 
 WORD e_csum; ///< Checksum,pe文件的校验码,验证pe文件的有效性,防止pe文件被破解修改,确保PE文件的完整性,not a CRC, MapFileAndCheckSum(ImageHlp.DLL)计算,和IP协议的校验和算法类似(RFC1071),如果不需要校验可设置为0,比如驱动文件加载
 
 WORD e_ip; ///< Initial IP value,指令寄存器的初始值
 
 WORD e_cs; ///< Initial (relative) CS value,代码段寄存器的初始值
 
 WORD e_lfarlc; ///< File address of relocation table,重定向表的地址
 
 WORD e_ovno; ///< Overlay number,覆盖号,现在覆盖技术应该不存在了,覆盖(overlay)技术"解决较小的内存空间上运行较大用户程序的问题。将用户程分为几个部分,根据需要分别调用,每个部分就是个覆盖。覆盖管理程序负责覆盖模块的加载
 
 WORD e_res[4]; ///< Reserved words,保留字段
 
 WORD e_oemid; // OEM identifier (for e_oeminfo)
 
 WORD e_oeminfo; // OEM information; e_oemid specific
 
 WORD e_res2[10]; ///< Reserved words,保留字段2
 
 LONG e_lfanew; ///< File address of new exe header,PE头的起始地址,通过DOC Header中的e_lfanew字段(相对DOC Header偏移是0x0000 003C)找到PE头的起始地址,可以算出来doc头部是64Byte0x3c+4=64),其中IMAGE_DOS_HEADER是40Byte,`DOS stub`程序是24Byte
 
 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
  • 用编辑器解析PE文件
    • CFF explorer
    • Notepad++(以十六进制方式查看)
  • 用代码解析PE文件
    • 判断有效PE:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FILE *fp;
IMAGE _DOS_HEADER dosheader;
unsigned long pesig;
/// 读和以二进制方式打开
fp = fopen(file,"r+b");
if(fp == NULL)
    return FALSE;
/// 读取dosheader
fread(&dosheader,sizeof(dosheader),1,fp);
/// 将读写指针移动到目标地址,即通过dosheader.e_lfanew定位到PE的头部
fseek(fd,dosheader.e_lfanew,SEEK_SET);
/// 读取PE头的前四个字节获取到PE的签名
fread(&pesig,4,1,fp);
fclose(fp);
 
/// dosheader e_magic和PE头部里的signature必须为固定值:
/// MZ(0X5A4D)和PE00(0x00004550)
if((dosheader.e_magic == IMAGE_D0S_SIGNATDRE)&&
(pesig == IMAGE_NT_SIGNATURE))
    return TRUE;
return FALSE;
  • 解析DOS HEAFDER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
FILE *fp;
IMAGE _DOS_HEADER dosheader;
unsigned long pesig;
/// 读和二进制方式打开
fp = fopen(file,"r+b");
if(fp == NULL)
        return -1;
 
/// 读取dosheader
fread(&dosheader,sizeof(dosheader),1,fp);
 
/// 将读写指针移动到目标地址,即通过dosheader.e_lfanew定位到PE的头部
fseek(fp,dosheader.e_lfanew,SEEK_SET);
/// 读取PE头的前四个字节获取到PE的签名
fread(&pesig,4,1,fp),
fclose(fp);
 
printf("IMAGE_DOS_HEADER info:\n");
/// 4个字节为单位长度,以十六进制方式打印,不足四个字节用0填充
printf("e_magic:%04X\n" ,dosheader.e_magic); ///< "MZ"-->"ZM"0X5A4D
printf("e_cblp: %04XAn",dosheader.e_cblp);
pentf("e_cp:%04x\n",dosheader.e_cp);
printf("e_crlc: %04X\n",dosheader.e_crlc);
printf("e_cparndr: %04Xun",dosheader.e_cparhdr);
printf("e_minalloc: %04X\n",dosheader.e minalloc);
printf("e_maxalloc: %04X\n" ,dosheader.e_maxaloc);
printf("e_ss: %04X\n",dosheader.e_ss);
printf("e_sp: %04X\n",dosheader.e_sp);
printf("e_sum: %04X\n",dosheader.e_sum);
printf("e_ip: %04X\n",dosheader.e_ip);
printf("e_cs: %04X\n",dosheader.e_cs);
printf("e_Ifarlc: %04X\n" ,dosheader.e_Ifarlc);
printf("e_evno: %04X\n",dosheader.e_ evno);
printf("e_res[0] : %04X\n" ,dosheader.e_res[0]);
printf("e_oemnid: %04X\n",dosheader.e_oemid);
print("e_oemninfo: %04X\n",dosheader.e_oeminfo);
printf("res2[0]: %04X\n" ,dosheader.e_res2[0]);
printf("lfanew: %08X\n" dosheader.e_lfanew);
NT(pe) Header

  • 在PE头,最开始的值是一个PE文件特有的签名,即PE\0\0,一旦操作系统在执行PE的时候,发现这个位置如果不是这个值,就会报错。然后,PE头分为2个组成部分:File HeaderOptional Header
  • 值得注意的是PE文件头中的_IMAGE_OPTIONAL_HEADER32里面有一个非常重要的结构DataDirectory[16],PE文件中的导入表导出表资源重定位表等数据的位置长度都保存在这个结构里。
  • 其他的相关概念
    • EOP-入口点。
    • OEP-原入口点。壳里面的概念,脱壳就是在找oep
    • EPO-入口模糊-病毒概念
  • PE首先加载到一个基地址:
  • ImageBase:基地址
    • EXE: 0x00400000(4MB)
    • DLL: 0x10000000(256MB)
  • VA: 虚拟地址
  • RVA:相对虚拟地址。RVA=VA-ImageBase
    IMAGE_NT_HEADERS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// 32位程序的PE头
typedef struct _IMAGE_NT_HEADERS {
 
 DWORD Signature; ///< PE头签名 0x0000
 
 IMAGE_FILE_HEADER FileHeader; ///< PE文件头
 
 IMAGE_OPTIONAL_HEADER32 OptionalHeader; ///< PE扩展头(如果32位程序,这个结构体就是32位的,如果是64位程序,那么这个结构体就是64位的)
/// 32位: 224(EO) bytes
/// 64位: 240(F0) bytes
 
} IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;
 
/// 64位程序的PE头
typedef struct _IMAGE_NT_HEADERS {
 
 DWORD Signature; ///< PE头签名 4Byte
 
 IMAGE_FILE_HEADER FileHeader; ///< PE文件头
 
 IMAGE_OPTIONAL_HEADER64 OptionalHeader; ///< PE扩展头(如果32位程序,这个结构体就是32位的,如果是64位程序,那么这个结构体就是64位的)
/// 32位: 224(EO) bytes
/// 64位: 240(F0) bytes
 
} IMAGE_NT_HEADERS64,*PIMAGE_NT_HEADERS64;
IMAGE_FILE_HEADER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// PE文件头 占20个字节
/// 定位fileHeader Fileheader: e_lfanew+4
typedef struct _IMAGE_FILE_HEADER {
 
 WORD Machine; ///< 指定程序运行的平台
/**
#define IMAGE_FILE_MACHINE_UNKINONIN  0
#define IMAGE_FILE_MACHINE_I386       0x184c // Intel 386. (x86)
#define IMAGE_FILE_MACHINE_ALPHA      0x0184 // Alpha_ AXP
#define IMAGE_FILE_MACHINE_POWERPC   0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_ FILE_MACHINE_AMD64     0x8664 // AMD64 (x64)
*/
 WORD NumberOfSections; ///< PE中块的数目
 
 DWORD TimeDateStamp; ///< 时间戳
 
 DWORD PointerToSymbolTable; ///< 指向符号表的地址,这里是程序的调试信息
 
 DWORD NumberOfSymbols; ///< 符号表数量
 
 WORD SizeOfOptionalHeader; ///< 可选首部长度(IMAGE_OPTIONAL_HEADER)的长度
 
 WORD Characteristics; ///< 文件信息标记,区分文件是exe还是dll。每个宏定义的属性的按位或起来。
 
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
IMAGE_OPTIONAL_HEADER
  • 32位: 224(E0) bytes
  • 64位: 240(F0) bytes
  • 在64位中少一个属性BaseOfDataAddressOfEntryPoint的值不一样。和ImageBase,SizeOfStackReserve,SizeOfStackCommit,SizeOfHeapReserve,SizeOfHeapCommit的地址从4Byte变成8Byte,其他和32位一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/// 32
typedef struct _IMAGE_OPTIONAL_HEADER {
 
 //
 
 // Standard fields.
 
 //
 
 WORD Magic; // 010B-IMAGE_NT_OPTIONAL_HDR32_MAGIC
/// MAGIC: 0x10B normal executable file. 0x107 ROM image 0x20B PE32+ executable ( 64位)
 
 BYTE MajorLinkerVersion; //0A-连接器主版本号
 
 BYTE MinorLinkerVersion; //00-连接器小版本号
 
 DWORD SizeOfCode; //0000008A(138)-代码节大小
 
 DWORD SizeOfInitializedData; //0000004C(76)-已初始化数据大小
 
 DWORD SizeOfUninitializedData; //00000000(0)-未初始化数据块大小 
 
 DWORD AddressOfEntryPoint; //PE装载器准备运行的PE文件的第一个指令的RVA,000110AA程序入口地址EOP,wmainCRTStartup或wWinMainCRTStartup(w表示是个unicode工程)
/// 若要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行
 
 
 DWORD BaseOfCode; //00001000代码段起始RVA 
 
 DWORD BaseOfData; //00001000数据段起始RVA
 
 //
 
 // NT additional fields.
 
 //
 
 DWORD ImageBase; //镜像加载基地址00400000,把.exe文件从磁盘加载到内存中,这个就是.exe在内存的基地址,在32位系统中是4个字节
 
 DWORD SectionAlignment; //内存节对齐因子0001000(4096),PE文件有很多节,把.exe从磁盘加载到内存的过程中,这些节是按照4kB来对齐存储的,即节的基地址是4K的整数倍
 
 DWORD FileAlignment; //文件节对齐因子0000200(512B,即一个扇区大小),PE(.exe)中每个节在磁盘中是按照一个扇区的大小对齐来存储的,即文件的起始地址是512B的整数倍,正是每个节在磁盘和内存的对齐方式不一致,当把一个PE文件从磁盘中加载到内存中的时候,会产生很多碎片(每条指令的在磁盘中相对于文件起始地址的偏移,和这条指令在内存中相对于文件起始地址的偏移不一致),所以要做一些换算
 WORD MajorOperatingSystemVersion; //所需操作系统主版本号0005
 
 WORD MinorOperatingSystemVersion; //所需操作系统小版本号0001
 
 WORD MajorImageVersion; //镜像主版本号0000,镜像的意思是,把磁盘上的PE文件映射到内存中,此时内存中pe文件就是磁盘上pe文件的一个镜像
 
 WORD MinorImageVersion; //镜像小版本号0000
 
 WORD MajorSubsystemVersion; //子系统主版本号0005,win32子系统版本。若PE文件是专门为Win32设计的
 
 WORD MinorSubsystemVersion; //子系统小版本号0001,该子系统版本必定是4.0否则对话框不会有3维立体感 
 
 DWORD Win32VersionValue; //0,保留值,系统没用到的,一般被作为是否感染的标志 
 
 DWORD SizeOfImage; //镜像大小00022000
 
 DWORD SizeOfHeaders; //所有头+节表的大小 0400
 
 DWORD CheckSum; //0,校验码
 
 WORD Subsystem; //NT用来识别PE文件属于哪个子系统。console程序:03-IMAGE_SUBSYSTEM_WINDOWS_CUI win32程序:IMAGE_SUBSYSTEM_WINDOWS_GUI 无:IMAGE_SUBSYSTEM_WINDOWS_NATIVE
 
 WORD DllCharacteristics; ///< 用来表示一个DLL映像是否为进程和线程的初始化及终止包含入口点的标记  eg:IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100
 
/// 堆栈大小 这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请。在默认情况下,栈和堆都拥有1个页面的申请值以及16个页面的保留值
 DWORD SizeOfStackReserve; //栈初始化大小 010000
 
 DWORD SizeOfStackCommit; //栈提交大小01000
 
 DWORD SizeOfHeapReserve; //堆初始化大小010000
 
 DWORD SizeOfHeapCommit; //堆提交大小01000
 
 DWORD LoaderFlags; //0 已经被淘汰在32位和64位都没有用到了,告知装载器是否在装载时中止和调试,或者默认地正常运行
 
 DWORD NumberOfRvaAndSizes; //0x10(16个)该字段标识了接下来的DataDirectory数组个数。
 
 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录表,每个表项都是一个重数据结构的RVA(表项指向一个重要的数据结构),比如指向引入地址表等
 
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
  • 在64位中少一个属性BaseOfDataAddressOfEntryPoint的值不一样。和ImageBase,SizeOfStackReserve,SizeOfStackCommit,SizeOfHeapReserve,SizeOfHeapCommit的地址从4Byte变成8Byte,其他和32位一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/// 32
typedef struct _IMAGE_OPTIONAL_HEADER64 {
 
 //
 
 // Standard fields.
 
 //
 
 WORD Magic; // 010B-IMAGE_NT_OPTIONAL_HDR32_MAGIC
/// MAGIC: 0x10B normal executable file. 0x107 ROM image 0x20B PE32+ executable ( 64位)
 
 BYTE MajorLinkerVersion; //连接器主版本号
 
 BYTE MinorLinkerVersion; //连接器小版本号
 
 DWORD SizeOfCode; //代码节大小
 
 DWORD SizeOfInitializedData; //已初始化数据大小
 
 DWORD SizeOfUninitializedData; //未初始化数据块大小 
 
 DWORD AddressOfEntryPoint; //PE装载器准备运行的PE文件的第一个指令的RVA,0x10程序入口地EOP址,wmainCRTStartup或wWinMainCRTStartup(w表示是个unicode工程)
/// 若要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行
 
 
 DWORD BaseOfCode; //00001000代码段起始RVA 
 
 //DWORD BaseOfData; //00001000数据段起始RVA,64位没有这个属性
 
 //
 
 // NT additional fields.
 
 //
 
ULONGLONG ImageBase; //镜像加载基地址0000000140000000,把.exe文件从磁盘加载到内存中,这个就是.exe在内存的基地址,在32位系统中是4个字节
 
 DWORD SectionAlignment; //内存节对齐因子0001000(4096),PE文件有很多节,把.exe从磁盘加载到内存的过程中,这些节是按照4kB来对齐存储的,即节的基地址是4K的整数倍
 
 DWORD FileAlignment; //文件节对齐因子0000200(512B,即一个扇区大小),PE(.exe)中每个节在磁盘中是按照一个扇区的大小对齐来存储的,即文件的起始地址是512B的整数倍,正是每个节在磁盘和内存的对齐方式不一致,当把一个PE文件从磁盘中加载到内存中的时候,会产生很多碎片(每条指令的在磁盘中相对于文件起始地址的偏移,和这条指令在内存中相对于文件起始地址的偏移不一致),所以要做一些换算
 WORD MajorOperatingSystemVersion; //所需操作系统主版本号0005
 
 WORD MinorOperatingSystemVersion; //所需操作系统小版本号0001
 
 WORD MajorImageVersion; //镜像主版本号0000,镜像的意思是,把磁盘上的PE文件映射到内存中,此时内存中pe文件就是磁盘上pe文件的一个镜像
 
 WORD MinorImageVersion; //镜像小版本号0000
 
 WORD MajorSubsystemVersion; //子系统主版本号0005,win32子系统版本。若PE文件是专门为Win32设计的
 
 WORD MinorSubsystemVersion; //子系统小版本号0001,该子系统版本必定是4.0否则对话框不会有3维立体感 
 
 DWORD Win32VersionValue; //0,保留值,系统没用到的,一般被作为是否感染的标志 
 
 DWORD SizeOfImage; //镜像大小00022000
 
 DWORD SizeOfHeaders; //所有头+节表的大小 0400
 
 DWORD CheckSum; //0,校验码
 
 WORD Subsystem; //NT用来识别PE文件属于哪个子系统。console程序:03-IMAGE_SUBSYSTEM_WINDOWS_CUI win32程序:IMAGE_SUBSYSTEM_WINDOWS_GUI 无:IMAGE_SUBSYSTEM_WINDOWS_NATIVE
 
 WORD DllCharacteristics; ///< 用来表示一个DLL映像是否为进程和线程的初始化及终止包含入口点的标记  eg:IMAGE_DLLCHARACTERISTICS_NX_COMPAT 0x0100
 
/// 堆栈大小 这些域控制要保留的地址空间数量,并且负责栈和默认堆的申请。在默认情况下,栈和堆都拥有1个页面的申请值以及16个页面的保留值
 ULONGLONG SizeOfStackReserve; //栈初始化大小 010000
 
 ULONGLONG SizeOfStackCommit; //栈提交大小01000
 
 ULONGLONG SizeOfHeapReserve; //堆初始化大小010000
 
 ULONGLONG SizeOfHeapCommit; //堆提交大小01000
 
 DWORD LoaderFlags; //0 已经被淘汰在32位和64位都没有用到了,告知装载器是否在装载时中止和调试,或者默认地正常运行
 
 DWORD NumberOfRvaAndSizes; //0x10(16个)该字段标识了接下来的DataDirectory数组个数。
 
 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录表,每个表项(8个字节)都是一个重数据结构的RVA(4个字节)和长度(4个字节),比如引入地址表等
 
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
PE地址计算

  • PE文件对齐:0x200(512B,是一个磁盘扇区的大小)
  • 内存对齐:0x1000(4KB,是物理内存页的大小)
  • DOS头(64Byte)+PE头(224or240Byte)没有超过512Byte,所以这部分在磁盘中和内存中存储的一一对应的,没有发生变化。往后,块表,.text块在磁盘中的起始地址是512的整数倍,但.text块在内存的起始地址必须是1K的整数倍,所有就产生了偏移。
  • 磁盘的PE文件首先加载到内存上的一个基地址:
    • ImageBase:基地址 0x00400000(4MB)
    • VA:虚拟地址(磁盘上某一指令随着PE加载到内存中后有一个虚拟地址VA,程序中使用的就是虚拟地址)
    • RVA:相对虚拟偏移量。RVA = VA - ImageBase(该指令VA随着基地址ImageBase的变化而变化,而该指令的RVA不会随着基地址mageBase变化而变化)
    • VOffset:该指令所在的节的起始地址对于内存中基地址ImageBase的偏移量(内存中的偏移量,按1K对齐)
    • ROffset:该指令所在的节的起始地址对于磁盘中PE文件起始位置的偏移量(磁盘中的偏移量,按512对齐)
  • PE地址计算的应用场景,分析逆向的时候,加载到内存中动态分析,找到关键指令之后,需要根据内存的地址算出磁盘上的地址,从而把磁盘上程序的关键指令修改掉,使其永久生效。
    • 上面几个数据(ImageBase,VOffset,ROffset,VA)都应该是已知的。现在求某个虚拟地址VA,对应的磁盘中PE文件偏移地址:fRVA.
    • 解:等式的原理就是:从一个节(或者叫块)的角度来看,某一个关键指令的在内存的虚拟地址相对该指令所在的节起始地址的偏移=该指令在磁盘文件地址相对该指令所在的节起始地址的偏移`
      fRVA - ROffset = RVA - VOffset
      fRVA - ROffset = VA - ImageBase - VOffset
      移项得:fRVA = VA - ImageBase - VOffset + ROffset
      块表(section header)
  • 节(块)表头的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// 块表(section Header)中每个表项占40个字节
#define IMAGE_SIZEOF_SECTION_HEADER 40
/// 节名最长不超过8个字节
#define IMAGE_SIZEOF_SHORT_NAME  8
typedef struct _IMAGE_SECTION_HEADER {
    BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; ///< 节表名称,如“.text”
    union {
        DWORD PhysicalAddress; // 物理地址
        DWORD VirtualSize; ///< 真实长度,一般是节的数据大小,这两个值是一个联合结构,可以使用其中的任何一个,
    }Misc;
    DWORD VirtualAddress; ///< 该节基于ImageBase 的RVA值
    DWORD SizeOfRawData; ///< 磁盘中文件的长度
    DWORD PointerToRawData; ///< 指向磁盘中文件的地址,可以用RVA计算出fRVA进行手工验证
    DWORD PointerToRelocations; //节基于文件的偏移量,重定位的偏移
    DWORD PointerToLinenumbers; //行号表的偏移
    WORD NumberOfRelocations; //重定位项数目
    WORD NumberofLinenumbers; //行号表的数目
    DWORD Characteristics; ///< 节的属性,如可读,可写,可执行等
}IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
节(section,也称为块)
  • 所有的节在载入内存后都按“SectionAlignment”(节对齐 )对齐,在文件中则以“FileAlignment”(文件对齐)对齐。节由节头中的相关项来描述:在文件中你可通过“PointerToRawData”(原始数据指针)来找到,在内存中你可通过“VirtualAddress”(虚拟地址 )来找到;长度由“SizeOfRawData”(原始数据长度)决定。
  • 根据节中包含的内容,可分为好几种节。大多数(并非所有)情况下,节中至少由一个数据目录,并在可选头的数据目录数组中有一个指针指向它。
    代码节(code section)
  • 首先,我将提到代码节。此节,至少,要将“IMAGE_SCN_CNT_CODE”(含有代码节)、“IMAGE_SCN_MEM_EXECUTE”(内存可执行节)和“IMAGE_SCN_MEM_READ”(内存可读节)等标志位设为1,并且“AddressOfEntryPoint”(入口点地址)将指向节中的某个地方,指向开发者希望首先执行的那个函数的开始处。
  • “BaseOfCode”(代码基址 )通常指向这一节的开始处,但是,如果一些非代码字节被放在代码之前的话,它也可能指向节中靠后的某个地方。
  • 通常,除了可执行代码外,本节没有别的东东,并且通常只有一个代码节,但是不要太迷信这一点。
  • 典型的节名有“.text”、“.code”、“AUTO”之类。
    数据节(data section)
  • 我们要讨论的下一件事情就是已初始化变量;本节包含的是已初始化的静态变量(象“static int i = 5;”)。它将,至少,使“IMAGE_SCN_CNT_INITIALIZED_DATA”(含有已初始化数据节)、“IMAGE_SCN_MEM_READ”(内存可读节)和“IMAGE_SCN_MEM_WRITE”(内存可写节)等标志位被置为1。
  • 一些链接器可能会将常量放在没有可写标志位的它们自己的节中。如果有一部分数据可共享,或者有其它的特定情况,那么可能会有更多的节,且它们的合适的标志位会被设置。
  • 不管是一节,还是多节,它们都将处于从“BaseOfData”(数据基址)到“BaseOfData”+“SizeOfInitializedData”(数据基址+已初始化数据的大小)的范围之内。
  • 典型的名称有“.data”、“.idata”、“DATA”、等等。
    BSS节(bss section)
  • 其后就是未初始化的数据(一些象“static int k;”之类的静态变量);本节十分像已初始化的数据,但它的“PointerToRawData”(文件偏移量)却为0,表明它的内容不存储在文件中;并且“IMAGE_SCN_CNT_UNINITIALIZED_DATA”(含有未初始化数据节)而不是“IMAGE_SCN_CNT_INITIALIZEDX_DATA”(含有已初始化数据节)标志位被置为1,表明在载入时它的内容应该被置为0。这就意味着,在文件中只有节头,没有节身;节身将由加载器创建,并全部为0字节。
  • 它的长度由“SizeOfUninitializedData”(未初始化数据大小)确定。
  • 典型的名称有“.bss”、“BSS”之类。
    特殊的节
  • 有些节数据“没有”被数据目录指向。它们的内容和结构是由编译器而不是链接器提供。
  • 栈段和堆段不是二进制文件中的节,它们是由加载器根据可选头中的栈大小和堆大小项来创建的。
    节汇总列表
  • .text 代码段。里面的数据全都是代码
  • .data 可读写的数据段,存放全局变量或静态变量
  • .rdata只读数据区,比如常量字符串
  • .idata导入数据区,存放导入表信息
  • .edata导出数据区,导出表信息
  • .rsrc资源区段,存放程序用到的所有资源,如图表,菜单等
  • .bss未初始化数据区
  • .crt用于支持C++运行时库所添加的数据
  • .tls存储线程局部变量,TLS( Thread Local Storage)保证全局变量或者静态变量,在多线程程序中能访问而不互相影响
  • .reloc包含重定位信息
  • .sdata包含相对于可被全局指针定位的可读写数据
  • .srdata包含相对于可被全局指针定位的只读数据
  • .sbss包含相对于可被全局指针定位的未初始化数据
  • .pdata包含异常表
  • debugss包含OBJ文件中的Codeview格式符号
  • debug$T包含OB]文件中的Codeview格式类型的符号
  • debug$P包含使用预编译头时的一些信息
  • .drectve包含编译时的-些链接命令
  • .didat包含延迟装入的数据
    DataDirectory数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_DIRECTORY_ENTRY_EXPORT θ // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERE_LOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
//IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVAofGP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 //  TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
  • 如何定位DataDirectory数组
    • e_lfanew+4+0x14+0x70
  • 在Optional Header里,有一个重要的数组即DataDirectory数组。在这个数组有16个数组元素,指向很多重要的表,比如导入表(IMP),导出表(EMP),导入地址(IAT)表等。
    • DataDirectory数组元素的定义:
1
2
3
4
typedef struct IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress; ///< VirtualAddress域是一个RVA( Relative Virtual Address)值
    DWORD Size; ///< 表的长度
}IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
导入表
 


  • 一个PE文件(.exe)从另外一个PE文件(.exe)中的dll中导入一些函数,在PE中是使用导入表来记录的。导入表记录了PE使用了多少库函数。在加载前和加载到内存后IAT表是变化的,导入表的结构如下:
1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_IMPORT_DESCRIPTOR { 
    union { 
        DWORD Characteristics; ///< 导入表结束标志
        DWORD OriginalFirstThunk;// 指向一个 IMAGE_THUNK_DATA结构数组(INT表)的RVA,INT表(是一个数组,记录着dll中导入的序号和函数名称)中每一个表项指向一个结构体(一个序号+导入函数名称) 
    
    DWORD TimeDateStamp;// 文件生成的时间]
    DWORD ForwarderChain;// 这个数据一般为0,可以不关心 
    DWORD Name;  // RVA,指向所导入DLL名字的指针,ASCII字符串 
    DWORD FirstThunk; //指向一个 IMAGE_THUNK_DATA结构数组(IAT表)的RVA,在PE文件加载前,IAT表和INT表的每一个表项指向的内容一致(两者指向同一个IMAGE_THUNK_DATA结构数组(序号+导入函数的名称));但在PE文件加载到内存之后,INT没有变化,IAT表发生变化了,每个表项指向的是导入函数的地址
 
}IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR
  • IMAGE_THUNK_DATA 这是一个DWORD类型的集合。通常我们将其解释为指向一个 IMAGE_IMPORT_BY_NAME 结构的指针,其定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef _IMAGE_THUNK_DATA{ 
     union { 
         PBYTE ForwarderString; 
         PDWORD Function; 
         DWORD Ordinal; //判定当前结构数据是不是以序号为输出的,如果是的话该值为0x800000000,此时PIMAGE_IMPORT_BY_NAME不可做为名称使用 PIMAGE_IMPORT_BY_NAME
         DWORD AddressOfData;  // RVA指向_IMAGE_IMPORT_BY_NAME
     }u1; 
} IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;
 
typedef struct _IMAGE_IMPORT_BY_NAME{ 
    WORD Hint; //函数序号,可能为0,编译器决定,如果不为0,则是函数在导出表中的索引
 
    BYTE Name[1];//函数名称,以0结尾
}IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
基址重定位表
  • EXE的默认加载地址是0x400000,DLL的默认加载地址是0x10000000。如果加载的基地址不是默认的地址(比如多个DLL加载,第一个DLL可能可以加载到0x10000000,但第二个DLL无法加载到0x10000000了(被占用了),只能加载到其他地方),那么EXE或者DLL代码里使用的固定地址就需要重定位(原来地址的值+实际基地址-默认基地址)。这些代码的地址就会记录在.reloc块表里。
    • 如果加载dll,不是使用默认的基地址,PE加载器就会改写基址重定位表中所写的地址,把原先的值加上,“实际基地址-默认基地址”的值。
    • 如果加载dll使用的默认基地址,那么该dll的重定位表就不起作用了。
  • eg:比如DLL文件里有一句代码: push 10009050 (该地址指向了一个字符串"1234567890" )。如果DLL默认加载地址是0x10000000被其它DLL占有了,改为了0x20000000,那么这个指令就需要重定位为:
    push 20009050(0x10009050+ (0x20000000-0x10000000)
1
2
3
4
5
6
7
/// .rloc表按页的大小0x1000分为若干个块:
typedef struct _IMAGE_BASE_RELOCATION{
    DWORD VirtualAddress; ///< 需要重定位的指令的基地址
    DWORD SizeOfBlock; ///< IMAGE_BASE_RELOCATION的大小
    WORD TypeOffset[]; ///< sizeof(TypeOffset) = (SizeOfBlock - 8)/2
    /// 16bit,高4bit表示类型,低12bit表示偏移,低12bit的偏移+VirtualAddress表示该需要重定位的指令的地址 
}IMAGE_BASE_RELOCATION;

C语言编程解析PE结构(demo)

  • 关键在于,准确把文件的读写指针定位到PE文件对应各个结构上起始位置
  • @todo
    判断PE是否合法
    区分操作32位PE和64位PE(可能会截断)
    解析节
    解析导入表,基址重定位表

    ELF

  • ELF (Executable and Linking Format)是一种对象文件的格式(.out,.so),用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。
  • 目标文件格式支持8位字节/32位体系结构。不过这种格式是可以扩展的,因此,目标文件以某些机器独立的格式来表达某些控制数据,使得能够以一种的公共的方式来X识别和解释其内容。目标文件中的其它数据使用目标处理器的编码结构,而不管文件在何种机器上创建。
  • 理解ELF格式是非常重要的,安卓底层NDK开发使用的文件是elf格式的.so文件(存放一些用C/C++开发的复杂的、重要的算法,供上层java程序调用),所以在移动安全(安卓),要对.so文件进行保护、分析、脱壳加壳,都需要对ELF文件格式的每个字段了如指掌。

    ELF文件结构介绍

  • ELF和PE的有些概念类似的
    • PE(Portable Executable)文件是微软Windows操作系统上的程序文件文件格式,由微软设计的,并于1993年被TIS (tool interface standard,工具接口标准)委员会由(Microsoft,Intel,Borland,Watcom, IBM等等组成)所批准,基于COFF (common object file fromat",通用自标文件格式,而COFF多应用于UNIX等系统中的用标文件和可执行文件的格式)文件格式。ELF是Linux的执行文件格式,而Linux是从UNIX上演化过来的,所以ELF和PE的有些概念类似的
  • ELF文件格式提供了两种视图:链接视图(程序编译链接完成后存放在磁盘上的静态的视图,类似存在磁盘PE文件结构)和运行视图(把ELF从磁盘加载到内存中执行后的视图,类似加载到内存中PE文件结构)。
  • ELF头部:在两种视图中,ELF头部(ELF Header)都位于文件的开始部分,位置固定,保存了路线图(road map),描述了该文件的组织情况。
  • 程序头部表:在链接视图中,程序头部表(program header table)为可选。从程序的执行来看文件格式,程序头部表告诉系统如何来创建一个进程的内存映象。被用来建立进程映象(执行一个程序)的文件必须要有一个程序头部表,可重定位文件不需要这个头部表。
  • 节区
    • 链接视图看,节区(section)保存着目标文件的信息,包括指令,数据,符号表和重定位信息等等。
    • 执行视图看,一个( segment)通常包含几个节区,同样保存着指令,数据,符号表和重定位等信息。
  • 节区头部表(section header table)(类似PE的块表): 位于ELF文件末尾。包含了描述节区的信息。每个节区在这个表中有一个入口,该入口给出了节区的名字,大小等等信息。
    • 链接过程中的文件必须有一个节区头部表,而在执行视图中这个节区头部表为可选
      ELF头部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct {
    unsigned char e_ident[EL_NIDENT]; /* File identification. */
    Elf32_Half e_type; /* File type. */
    EIf32_Half e_machine; /* Machine architecture. */
    Elf32_Word e_version; /* ELF format version. */
    Elf32_Addr e_entry;/* Entry point. */
    Elf32_Off e_phoff;/* Program header file offset. */
    Elf32_Off e_shoff; /* Section header file offset. */
    Elf32_Word e_flags; /* Architecture-specific flags. */
    EIf32_Half e_ehsize; /* Size of ELF header in bytes. */
    Elf32_Half e_phentsize;/* Size of program header entry. */
    Elf32_Half e_phnum; /* Number of program header entries. */
    Elf32_Half e_shentsize; /* Size of section header entry. */
    Elf32_Half e_shnum; /* Number of section header entries. */
    Elf32_Half e_shstrndx; /* Section name strings section. */
}Elf32_Ehdr;
  • e_ident[EI_NIDENT]字段包含魔数、字节序、字长和版本,后面填充0。对于安卓的linker, 通过verify elf object 函数检验魔数,判定是否为.so文件(用C/C++代码封装成.so文件供java供调用)。
  • 安卓的linker,对e_typee_machinee_versione_flags 字段并不关心,是可以修改成其他数据的
  • 对于动态链接库,e_entry入口地址是无意义的,因为程序被加载时,、设定的跳转地址是动态连接器的地址,这个字段是可以被作为数据填充的。
  • so装载时,与链接视图没有关系,即e_shoff、 e_shentsize、 e_shnum和e_shstrndx这些字 段是可以任意修改的。被修改之后,使用readelf和ida等工具打开,会报各种错误。(readelf和ida是用来静态分析的,即链接视图)
    • 既然so装载与装载视图紧密相关,自然ephoff、 e phentsize 和e_ phnum 这些字段是不能动的

readelf命令解析ELF文件格式

  • readelf是Linux系统提供的命令(类似解析PE,自己写一个解析器),前提是ELF文件没有加密加壳、反逆向等特殊处理,如果ELF做了特殊处理,则需要先还原才能用readelf命令正常解析。
    • readelf -h xxx.so查看so文件的ELF头部信息
    • readelf -S xxx.so查看so文件的节区头部表信息
    • readelf -l xxx.so查看so文件的程序头部表信息(Program)
    • readelf -a xxx. so查看so文件的全部内容(内容多,可以echo到一个文件中去,方便查看)

推荐学习《操作系统真象还原》 作者: 郑钢


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2023-2-7 07:48 被公众号坚毅猿编辑 ,原因: 更新求职意向
收藏
免费 4
支持
分享
最新回复 (3)
雪    币: 295
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
厉害了
2023-3-12 03:30
0
雪    币: 4354
活跃值: (3373)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
写得挺好的
2023-3-12 08:50
0
雪    币: 1802
活跃值: (4000)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
写得好
2023-3-13 10:52
0
游客
登录 | 注册 方可回帖
返回
//