PWN入门

调用函数入栈顺序

调用的参数、函数返回地址(eip)、调用函数的基地址(ebp)、局部变量

技术分类

修改返回地址,让其指向溢出数据中的一段指令(shellcode)
修改返回地址,让其指向内存中已有的某个函数(return2libc)
修改返回地址,让其指向内存中已有的一段指令(ROP)
修改某个被调用函数的地址,让其指向另一个函数(hijack GOT)

Shellcode

payload : padding1 + address of shellcode + padding2 + shellcode

NOP Sled(滑雪橇)

我们可以在调试工具里查看返回地址的位置(可以查看 ebp的内容然后再加4(32位机),参见前面关于函数状态的解释),可是在调试工具里的这个地址和正常运行时并不一致,这是运行时环境变量等因素有所不同造成的。所以这种情况下我们只能得到大致但不确切的 shellcode 起始地址,解决办法是在 padding2 里填充若干长度的 “\x90”。这个机器码对应的指令是 NOP (No Operation),也就是告诉CPU什么也不做,然后跳到下一条指令。有了这一段NOP的填充,只要返回地址能够命中这一段中的任意位置,都可以无副作用地跳转到shellcode的起始处,所以这种方法被称为 NOP Sled(中文含义是“滑雪橇”)。这样我们就可以通过增加 NOP 填充来配合试验 shellcode 起始地址。

ASLR

操作系统可以将函数调用栈的起始地址设为随机化(这种技术被称为内存布局随机化,即Address Space Layout Randomization (ASLR)),这样程序每次运行时函数返回地址会随机变化。

NX

很多时候操作系统会关闭函数调用栈的可执行权限,这样shellcode的方法就失效了,不过我们还可以尝试使用内存里已有的指令或函数,毕竟这些部分本来就是可执行的,所以不会受上述执行权限的限制。这就包括 return2libc 和 ROP 两种方法。

Return2libc

payload: padding1 + address of system() + padding2 + address of “/bin/sh”

寄存器

32位x86架构下的寄存器可以被简单分为通用寄存器和特殊寄存器两类,通用寄存器在大部分汇编指令下是可以任意使用的(虽然有些指令规定了某些寄存器的特定用途),而特殊寄存器只能被特定的汇编指令使用,不能用来任意存储数据。
32位x86架构下的通用寄存器包括一般寄存器(eax、ebx、ecx、edx),索引寄存器(esi、edi),以及堆栈指针寄存器(esp、ebp)。
一般寄存器用来存储运行时数据,是指令最常用到的寄存器,除了存放一般性的数据,每个一般寄存器都有自己较为固定的独特用途。eax 被称为累加寄存器(Accumulator),用以进行算数运算和返回函数结果等。ebx 被称为基址寄存器(Base),在内存寻址时(比如数组运算)用以存放基地址。ecx被称为记数寄存器(Counter),用以在循环过程中记数。edx 被称为数据寄存器(Data),常配合 eax 一起存放运算结果等数据。
索引寄存器通常用于字符串操作中,esi 指向要处理的数据地址(Source Index),edi 指向存放处理结果的数据地址(Destination Index)。
堆栈指针寄存器(esp、ebp)用于保存函数在调用栈中的状态。
32位x86架构下的特殊寄存器包括段地址寄存器(ss、cs、ds、es、fs、gs),标志位寄存器(EFLAGS),以及指令指针寄存器(eip)。
段地址寄存器就是用来存储内存分段地址的,其中寄存器 ss 存储函数调用栈(Stack Segment)的地址,寄存器 cs 存储代码段(Code Segment)的地址,寄存器 ds 存储数据段(Data Segment)的地址,es、fs、gs 是附加的存储数据段地址的寄存器。
标志位寄存器(EFLAGS)32位中的大部分被用于标志数据或程序的状态,例如 OF(Overflow Flag)对应数值溢出、IF(Interrupt Flag)对应中断、ZF(Zero Flag)对应运算结果为0、CF(Carry Flag)对应运算产生进位等等。

汇编指令

32位x86架构下的汇编语言有 Intel 和 AT&T 两种格式:
Intel格式,寄存器名称和数值前无符号:

“指令名称 目标操作数 DST,源操作数 SRC”

AT&T格式,寄存器名称前加“%”,数值前加“$”:

“指令名称 目标操作数 DST,源操作数 SRC”

一些最常用的汇编指令如下:
MOV:数据传输指令,将 SRC 传至 DST,格式为

MOV DST, SRC;

PUSH:压入堆栈指令,将 SRC 压入栈内,格式为

PUSH SRC;

POP:弹出堆栈指令,将栈顶的数据弹出并存至 DST,格式为

POP DST;

LEA:取地址指令,将 MEM 的地址存至 REG ,格式为

LEA REG, MEM;

ADD/SUB:加/减法指令,将运算结果存至 DST,格式为

ADD/SUB DST, SRC;

AND/OR/XOR:按位与/或/异或,将运算结果存至 DST ,格式为

AND/OR/XOR DST,SRC;

CALL:调用指令,将当前的 eip 压入栈顶,并将 PTR 存入 eip,格式为

CALL PTR;

RET:返回指令,操作为将栈顶数据弹出至 eip,格式为

RET;

ROP(Return Oriented Programming)(返回导向编程)

包含单个 gadget 的溢出数据

payload : padding + address of gadget

包含多个 gadget 的溢出数据

payload : padding + address of gadget 1 + address of gadget 2 + … + address of gadget n

mprotect

mprotect (void *addr, size_t len, int prot)

可用该函数将栈的属性改为可执行,这样就可以使用shellcode了。假如我们想利用系统调用执行这个函数,eax、ebx、ecx、edx 应该分别为“125”、内存栈的分段地址(可以通过调试工具确定)、“0x10000”(需要修改的空间长度,也许需要更长)、“7”(RWX 权限)。
有若干开源工具可以实现搜索以 ret 结尾的指令片段,著名的包括 ROPgadget、rp++、ropeme 等,甚至也可以用 grep 等文本匹配工具在汇编指令中搜索 ret 再进一步筛选。

gadget

pop eax; ret;    # pop stack top into eax
pop ebx; ret;    # pop stack top into ebx
pop ecx; ret;    # pop stack top into ecx
pop edx; ret;    # pop stack top into edx
int 0x80; ret;   # system call
push esp; ret;   # push address of shellcode

payload : padding + address of gadget 1 + param for gadget 1 + address of gadget 2 + param for gadget 2 + … + address of gadget n + shellcode

Hijack GOT

如何在链接库内定位到所需的函数

这个过程用到了两张表--GOT 和 PLT。GOT 全称是全局偏移量表(Global Offset Table),用来存储外部函数在内存的确切地址。GOT 存储在数据段(Data Segment)内,可以在程序运行中被修改。PLT 全称是程序链接表(Procedure Linkage Table),用来存储外部函数的入口点(entry),换言之程序总会到PLT这里寻找外部函数的地址。PLT存储在代码段(Code Segment)内,在运行之前就已经确定并且不会被修改,所以PLT并不会知道程序运行时动态链接库被加载的确切位置。那么 PLT 表内存储的入口点是什么呢?就是 GOT 表中对应条目的地址。
等等,我们好像发现了一个不合理的地方,外部函数的内存地址存储在GOT而非PLT表内,PLT存储的入口点又指向 GOT 的对应条目,那么程序为什么选择PLT而非GOT作为调用的入口点呢?在程序启动时确定所有外部函数的内存地址并写入GOT表,之后只使用GOT表不是更方便吗?这样的设计是为了程序的运行效率。GOT 表的初始值都指向 PLT 表对应条目中的某个片段,这个片段的作用是调用一个函数地址解析函数。当程序需要调用某个外部函数时,首先到PLT表内寻找对应的入口点,跳转到GOT表中。如果这是第一次调用这个函数,程序会通过 GOT 表再次跳转回 PLT 表,运行地址解析程序来确定函数的确切地址,并用其覆盖掉GOT表的初始值,之后再执行函数调用。当再次调用这个函数时,程序仍然首先通过PLT表跳转到GOT表,此时GOT表已经存有获取函数的内存地址,所以会直接跳转到函数所在地址执行函数。

防御措施

介绍过几种栈溢出的基础方法,我们再来补充一下操作系统内有哪些常见的措施可以进行防御。首先,通常情况下程序在默认编译设置下都会取消栈上数据的可执行权限,这样简单的 shellcode 溢出攻击就无法实现了。其次,可以在操作系统内开启内存布局随机化(ASLR),这样可以增大确定堆栈内数据和动态库内函数的内存地址的难度。编译程序时还可以设置某些编译选项,使程序在运行时会在函数栈上的 ebp 地址和返回地址之间生成一个特殊的值,这个值被称为“金丝雀”(关于这个典故,请大家自行谷歌)。这样一旦发生了栈溢出并覆盖了返回地址,这个值就会被改写,从而实现函数栈的越界检查。最后值得强调的是,尽可能写出安全可靠的代码,不给栈溢出提供写入越界的可能。

你可能感兴趣的:(PWN)