入坑二进制

入坑二进制

作为一名WEB菜狗,今天决定入坑二进制了,出于极大的好奇心和求知欲(其实就是自己太菜了,急需拓展知识啊~),加上也有些计算机原理的一些基础,想着不能把学的东西给废掉、扔掉,所以决定入坑!

环境配置

1、安装gdb和插件pada

从基础做起,安装gdb以及其插件pada

sudo apt-get install gdb

这里可能还需要安装一些库,依据需要安装即可,之后安装插件pada,二进制必备插件

git clone https://github.com/longld/peda.git ~/peda

git把库拉下来之后,把source ~/peda/peda.py 写入~/.gdbinit

echo "source ~/peda/peda.py" >> ~/.gdbinit

pada的时候也有用pip装的,但是可能导致gdb版本与python版本不匹配的问题,如果gdb自动绑定python3的话,直接pip下来的pada用着会报错。

安装完毕,测试:

2、安装checksec

checksec可以查看目标文件开启了哪些保护机制,是玩二进制不可缺少的工具。上一步装的pada插件里其实是包含checksec的,但是版本比较久。可以自己安装,这样可以有效控制高版本

git clone https://github.com/slimm609/checksec.sh.git
cd checksec.sh
sudo lnsf checksec /usr/bin/checksec

如果装的有旧版的checksec,此时创建软链接可能会失败,而且就算创建成功,启动checksec检查可能还是旧版,因为并没有进行所有的替换:

这里的Arch是程序架构信息,可以得知程序是64位还是32位的,有助于为编写exp提供信息。其他的内容就是保护机制开启的情况了,后面深入学习。

有效办法是先找一下旧版的文件都存在哪些文件夹下面,用find / -name checksec命令查找:

然后全部替换,再次测试:

可以看到新版会进行更多的保护机制的测试。

由于pada内置有旧版的checksec(跟前面替换的旧版不是同一个),在用gdb调试之前进行检测的话,仍然是旧版:

两个版本共存,也挺好~

浅学ELF保护机制

上一节提到了检查文件的保护机制,这一节就记录一下Linux文件的保护机制相关知识,以后的学习免不了都会遇到。

RELRO

Relocation Read Only, 重定位表只读。重定位表即.got.plt两个表。此项技术主要针对 GOT 改写的攻击方式,分为两种:Partial RELROFull RELRO
Partical RELRO易受到攻击,例如攻击者可以atoi.gotsystem.plt,进而输入/bin/sh\x00获得shellFull RELRO使整个GOT只读,从而无法被覆盖,但这样会大大增加程序的启动时间,因为程序在启动之前需要解析所有的符号。

Stack-canary

Canary是金丝雀的意思,技术上表示最先的测试的意思。这个来自以前挖煤的时候,矿工都会先把金丝雀放进矿洞,或者挖煤的时候一直带着金丝雀。金丝雀对甲烷和一氧化碳浓度比较敏感,会先报警。所以大家都用canary来搞最先的测试。stack canary表示栈的报警保护。

在函数返回值之前添加的一串随机数(不超过机器字长),末位为/x00(提供了覆盖最后一字节输出泄露canary的可能),如果出现缓冲区溢出攻击,覆盖内容覆盖到canary处,就会改变原本该处的数值,当程序执行到此处时,会检查canary值是否跟开始的值一样,如果不一样,程序会崩溃,从而达到保护返回地址的目的。

NX

Non-Executable Memory,不可执行内存。NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。栈溢出的核心就是通过局部变量覆盖返回地址,然后加入shellcode,NX策略是使栈区域的代码无法执行。

绕过这类保护最常见的方法为ROP(Return-Oriented Programming,返回导向编程),利用栈溢出在栈上布置地址,每个内存地址对应一个gadget,利用ret等指令进行衔接来执行某项功能,最终达到pwn掉程序的目的。

看到网上师傅说:当NX保护开启,就表示题目给了system('/bin/sh'),如果关闭,表示你需要去构造shellcode。这个慢慢学吧。

ALSR

Address space layout randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化将数据随机放置,增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。

但是地址随机化不是对所有模块和内存区都进行随机化,虽然libc的加载位置被随机化,但主镜像不会,这也是绕过的关键手段。

PIE

Position-Independent Executable, 位置无关可执行文件。与ASLR技术类似,ASLR将程序运行时的堆栈以及共享库的加载地址随机化, 而PIE技术则在编译时将程序编译为位置无关, 即程序运行时各个段(如代码段等)加载的虚拟地址也是在装载时才确定。

这就意味着, 在PIEASLR同时开启的情况下, 攻击者将对程序的内存布局一无所知, 传统的改写GOT表项的方法也难以进行, 因为攻击者不能获得程序的.got段的虚地址。

RPATH/RUNPATH

程序运行时的环境变量,运行时所需要的共享库文件优先从该目录寻找,可以fake lib造成攻击。参考实例

FORTIFY

由GCC实现的源码级别的保护机制,其功能是在编译的时候检查源码以避免潜在的缓冲区溢出等错误。加了这个保护之后,一些敏感函数如read, fgets,memcpy, printf等等可能导致漏洞出现的函数都会被替换成__read_chk__fgets_chk__memcpy_chk__printf_chk等。

这些带了chk的函数会检查读取/复制的字节长度是否超过缓冲区长度,通过检查诸如%n之类的字符串位置是否位于可能被用户修改的可写地址,避免了格式化字符串跳过某些参数(如直接%7$x)等方式来避免漏洞出现。

以上就是了解到的PWN中常见的ELF文件的保护机制了,以后学习过程中逐步深入了解。

关于寄存器

在进行常用的一些技术学习之前,还需温习一下之前学过的关于寄存器的知识,毕竟是基础的基础。

32位x86架构下的寄存器可以被简单分为通用寄存器特殊寄存器两类,通用寄存器在大部分汇编指令下是可以任意使用的(虽然有些指令规定了某些寄存器的特定用途),而特殊寄存器只能被特定的汇编指令使用,不能用来任意存储数据。

通用寄存器包括:一般寄存器eaxebxecxedx,索引寄存器esi、edi,以及堆栈指针寄存器esp、ebp。其中各类寄存器的作用如下:

一般寄存器:用来存储运行时数据,是指令最常用到的寄存器,除了存放一般性的数据,每个一般寄存器都有自己较为固定的独特用途。eax被称为累加寄存器(Accumulator),用以进行算数运算和返回函数结果等。ebx被称为基址寄存器(Base),在内存寻址时(比如数组运算)用以存放基地址。ecx被称为记数寄存器(Counter),用以在循环过程中记数。edx被称为数据寄存器(Data),常配合eax一起存放运算结果等数据。

索引寄存器:通常用于字符串操作中,esi指向要处理的数据地址(Source Index)edi指向存放处理结果的数据地址(Destination Index)

堆栈指针寄存器espebp用于保存函数在调用栈中的状态,即栈顶栈底

特殊寄存器包括:段地址寄存器sscsdsesfsgs,标志位寄存器EFLAGS,以及指令指针寄存器eip

现代操作系统内存通常是以分段的形式存放不同类型的信息的。函数调用栈就是分段的一个部分(Stack Segment)。内存分段还包括堆(Heap Segment)数据段(Data Segment)BSS段以及代码段(Code Segment)
代码段存储可执行代码和只读常量(如常量字符串),属性可读可执行,但通常不可写。数据段存储已经初始化且初值不为0的全局变量和静态局部变量,BSS段存储未初始化或初值为0的全局变量和静态局部变量,这两段数据都有可写的属性。堆用于存放程序运行中动态分配的内存,C语言中的malloc()free()函数就是在堆上分配和释放内存。各段在内存的排列如下图所示:

几种特殊寄存器用途如下:

段地址寄存器:用来存储内存分段地址的,其中寄存器ss存储函数调用栈的地址,寄存器cs存储代码段的地址,寄存器ds存储数据段的地址,esfsgs是附加的存储数据段地址的寄存器。

标志位寄存器(EFLAGS):32位中的大部分被用于标志数据或程序的状态,例如OF(Overflow Flag)对应数值溢出、IF(Interrupt Flag)对应中断、ZF(Zero Flag)对应运算结果为0、CF(Carry Flag)对应运算产生进位等。

指令指针寄存器(eip):存储下一条运行指令的地址。

了解了各类寄存器的用途,下面学习常用的漏洞利用方法手段。

常用技术方法

首先需要深入理解栈的工作原理,参考这里

在发生函数调用或者结束函数调用时,程序的控制权会在函数状态之间发生跳转,这时通过修改函数状态来实现攻击。而控制程序执行指令最关键的寄存器就是eip(存放下一条要执行指令的地址),所以我们的目标就是让eip载入攻击指令的地址。

如果要让eip指向攻击指令,需要做的工作:首先,在退栈过程中,返回地址会被传给eip,所以我们只需要让溢出数据用攻击指令的地址来覆盖返回地址就可以了。其次,我们可以在溢出数据内包含一段攻击指令,也可以在内存其他位置寻找可用的攻击指令。如下图:

再来看看函数调用发生时,如果要让eip指向攻击指令,需要哪些准备?这时,eip会指向原程序中某个指定的函数,我们没法通过改写返回地址来控制,不过我们可以“偷梁换柱”--将原本指定的函数在调用时替换为其他函数。

实现上述的攻击目的,主要有以下技术手段:

  • 1、Shellcode:修改返回地址,让其指向溢出数据中的一段指令
  • 2、Return2libc:修改返回地址,让其指向内存中已有的某个函数
  • 3、ROP:修改返回地址,让其指向内存中已有的一段指令
  • 4、Hijack GOT:修改某个被调用函数的地址,让其指向另一个函数

下面详细记录几种方法的学习:

Shellcode

修改返回地址,让其指向溢出数据中的一段指令

该技术要完成的任务包括:在溢出数据内包含一段攻击指令,用攻击指令的起始地址覆盖掉返回地址。
攻击指令一般都是用来打开shell,从而可以获得当前进程的控制权,所以这类指令片段也被成为shellcodeshellcode可以用汇编语言来写再转成对应的机器码,也可以上网搜索直接复制粘贴,以后深入学习。

溢出数据的组成如下:

payload = padding1 + address of shellcode + padding2 + shellcode

其在栈内的分布如下图:

这里对溢出数据的组成详细说明一下:

padding1处的数据可以随意填充(注意如果利用字符串程序输入溢出数据不要包含\x00,否则向程序传入溢出数据时会造成截断),长度应该刚好覆盖函数的基地址。address of shellcode是后面shellcode起始处的地址,用来覆盖返回地址。padding2处的数据也可以随意填充,长度可以任意。shellcode应该为十六进制的机器码格式。

根据上面的构造,我们要解决以下两个问题:

  • 1、 返回地址之前的填充数据padding1应该多长?

我们可以用调试工具(例如gdb)查看汇编代码来确定这个距离,也可以在运行程序时用不断增加输入长度的方法来试探(如果返回地址被无效地址例如AAAA覆盖,程序会终止并报错)。

  • 2、shellcode起始地址应该是多少?

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

另外,操作系统可以将函数调用栈的起始地址设为随机化(这种技术被称为内存布局随机化,即Address Space Layout Randomization (ASLR)),这样程序每次运行时函数返回地址会随机变化。反之如果操作系统关闭了上述的随机化(这是技术可以生效的前提),那么程序每次运行时函数返回地址会是相同的,这样我们可以通过输入无效的溢出数据来生成core文件,再通过调试工具在core文件中找到返回地址的位置,从而确定shellcode的起始地址。

解决完上述问题,就可以拼接出最终的溢出数据,输入至程序来执行shellcode了。

这种方法生效的一个前提是在函数调用栈上的数据(shellcode)要有可执行的权限(另一个前提是关闭内存布局随机化)。很多时候操作系统会关闭函数调用栈的可执行权限,这样 shellcode 的方法就失效了,不过我们还可以尝试使用内存里已有的指令或函数,毕竟这些部分本来就是可执行的,所以不会受上述执行权限的限制。这种情况包括Return2libcROP两种方法。

Return2libc

修改返回地址,让其指向内存中已有的某个函数

该方法要完成的任务包括:在内存中确定某个函数的地址,并用其覆盖掉返回地址。
由于libc动态链接库中的函数被广泛使用,所以有很大概率可以在内存中找到该动态库。同时由于该库包含了一些系统级的函数,所以通常使用这些系统级函数来获得当前进程的控制权。鉴于要执行的函数可能需要参数,比如调用system函数打开shell的完整形式为system(“/bin/sh”),所以溢出数据也要包括必要的参数。
下面以执行system(“/bin/sh”)为例,先写出溢出数据的组成,再确定对应的各部分填充进去。

溢出数据组成如下:

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

溢出数据各部分含义如下:

padding1处的数据可以随意填充(注意不要包含\x00,否则向程序传入溢出数据时会造成截断),长度应该刚好覆盖函数的基地址。address of system()system在内存中的地址,用来覆盖返回地址。padding2处的数据长度为4(32位机),对应调用system时的返回地址。因为我们在这里只需要打开shell就可以,并不关心从shell退出之后的行为,所以padding2的内容可以随意填充。address of “/bin/sh”是字符串/bin/sh在内存中的地址,作为传给system的参数。

根据上面的构造,我们要解决以下两个问题:

  • 1、返回地址之前的填充数据padding1应该多长?

解决方法和shellcode中提到的答案一样。

  • 2、system函数地址应该是多少?

首先要知道程序是如何调用动态链接库中的函数的:当函数被动态链接至程序中,程序在运行时首先确定动态链接库在内存的起始地址,再加上函数在动态库中的相对偏移量,最终得到函数在内存的绝对地址。
ASLR被关闭的前提下,我们可以通过调试工具在运行程序过程中直接查看system的地址,也可以查看动态库在内存的起始地址,再在动态库内查看函数的相对偏移位置,通过计算得到函数的绝对地址。而当ASLR开启的时候,会将动态库加载的起始地址做随机化处理,程序每次运行时动态库的起始地址都会变化,也就无从确定库内函数的绝对地址。

  • 3、/bin/sh的地址在哪里?

可以在动态库里搜索字符串/bin/sh,如果存在,就可以按照动态库起始地址+相对偏移来确定其绝对地址。如果在动态库里找不到,可以将这个字符串加到环境变量里,再通过getenv等函数来确定地址。

解决完上述问题,就可以拼接出溢出数据,输入至程序来通过system获取shell了。

ROP

修改返回地址,让其指向内存中已有的一段指令

该方法要完成的任务包括:在内存中确定某段指令的地址,并用其覆盖返回地址。
可是既然可以覆盖返回地址并定位到内存地址,为什么不直接用上篇提到的return2libc呢?因为有时目标函数在内存内无法找到,有时目标操作并没有特定的函数可以完美适配。这时就需要在内存中寻找多个指令片段,拼凑出一系列操作来达成目的。假如要执行某段指令(将其称为gadget,意为小工具),溢出数据应该以下面的方式构造(padding长度和内容的确定方式参见上篇):

payload = padding + address of gadget

溢出数据在栈中的分布如下图:

而如果想连续执行若干段指令,就需要每个gadget执行完毕可以将控制权交给下一个gadget。所以gadget的最后一步应该是RET指令,这样程序的控制权(eip)才能得到切换,所以这种技术被称为返回导向编程(Return Oriented Programming)。要执行多个gadget,溢出数据应该以下面的方式构造:

payload = padding + address of gadget 1 + address of gadget 2 + ...... + address of gadget n

在这样的构造下,被调用函数返回时会跳转执行gadget 1,执行完毕时gadget 1RET指令会将此时的栈顶数据(也就是gadget 2的地址)弹出至eip,程序继续跳转执行gadget 2,以此类推。

多个gadget的溢出数据在栈中分布如下:

现在任务可以分解为:针对程序栈溢出所要实现的效果,找到若干段以ret作为结束的指令片段,按照上述的构造将它们的地址填充到溢出数据中。所以我们要解决以下几个问题:

  • 1、栈溢出之后要实现什么效果?

ROP常见的拼凑效果是实现一次系统调用,Linux系统下对应的汇编指令是int 0x80。执行这条指令时,被调用函数的编号应存入eax,调用参数应按顺序存入ebxecxedxesiedi中。
例如,编号125对应函数:mprotect (void *addr, size_t len, int prot),可用该函数将栈的属性改为可执行,这样就可以使用shellcode了。假如我们想利用系统调用执行这个函数,eaxebxecxedx应该分别为:125内存栈的分段地址(可以通过调试工具确定)、0x10000(需要修改的空间长度,也许需要更长)、7(RWX 权限)。

  • 2、如何寻找对应的指令片段?

有若干开源工具可以实现搜索以ret结尾的指令片段,著名的包括ROPgadgetrp++ropeme等,甚至也可以用grep等文本匹配工具在汇编指令中搜索ret再进一步筛选。

  • 3、如何传入系统调用的参数?

对于上面提到的mprotect函数,我们需要将参数传输至寄存器,可以用pop指令将栈顶数据弹入寄存器。如果在内存中能找到直接可用的数据,也可以用mov指令来进行传输,不过写入数据再 pop要比先搜索再mov来的简单,如果要用pop指令来传输调用参数,就需要在溢出数据内包含这些参数,所以上面的溢出数据格式需要一点修改。对于单个gadgetpop所传输的数据应该在 gadget地址之后,如下图所示。

在调用mprotect()为栈开启可执行权限之后,我们希望执行一段shellcode,所以要将shellcode也加入溢出数据,并将shellcode的开始地址加到int 0x80gadget之后。但确定shellcode在内存的确切地址是很困难的事,我们可以使用push esp这个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

对于所有包含pop指令的gadget,在其地址之后都要添加pop的传输数据,同时在所有gadget最后包含一段shellcode,最终溢出数据结构应该变为如下格式:

payload = padding + address of gadget 1 + param for gadget 1 + address of gadget 2 + param for gadget 2 + ...... + address of gadget n + shellcode

为了简单,假定输入溢出数据不受\x00字符的影响,所以payload可以直接包含\x7d\x00\x00\x00(传给eax的参数125)。如果希望实现更为真实的操作,可以用多个gadget通过运算得到上述参数。比如可以通过下面三条gadget来给eax传递参数:

pop eax; ret;         # pop stack top 0x1111118e into eax
pop ebx; ret; # pop stack top 0x11111111 into ebx
sub eax, ebx; ret; # eax -= ebx

解决完上述问题,我们就可以拼接出溢出数据,输入至程序来为程序调用栈开启可执行权限并执行 shellcode。同时,由于ROP方法带来的灵活性,现在不再需要痛苦地试探shellcode起始地址了。回顾整个输入数据,只有栈的分段地址需要获取确定地址。如果利用gadget读取ebp的值再加上某个合适的数值,就可以保证溢出数据都具有可执行权限,这样就不再需要获取确切地址,也就具有了绕过内存随机化的可能。

实际搜索及拼接gadget时,并不会像上面一样顺利,有两个方面需要注意:

  • 1、很多时候并不能一次凑齐全部的理想指令片段,这时就要通过数据地址的偏移、寄存器之间的数据传输等方法来“曲线救国”。举个例子,假设找不到下面这条gadget:
pop ebx; ret;

但假如可以找到下面的gadget:

mov ebx, eax; ret;

就可以将它和

pop eax; ret;

组合起来实现将数据传输给ebx的功能。上面提到的用多个gadget避免输入\x00也是一个实例应用。

  • 2、要小心gadget是否会破坏前面各个gadget已经实现的部分,比如可能修改某个已经写入数值的寄存器。另外,要特别小心gadget对ebpesp的操作,因为它们的变化会改变返回地址的位置,进而使后续的gadget无法执行。

Hijack GOT

修改某个被调用函数的地址,让其指向另一个函数

该方法要完成的任务包括:在内存中修改某个函数的地址,使其指向另一个函数。
为了便于理解,不妨假设修改printf()函数的地址使其指向system(),这样修改之后程序内对 printf()的调用就执行system()函数。要实现这个过程,我们就要弄清楚发生函数调用时程序是如何找到被调用函数的。

程序对外部函数的调用需要在生成可执行文件时将外部函数链接到程序中,链接的方式分为静态链接动态链接。静态链接得到的可执行文件包含外部函数的全部代码,动态链接得到的可执行文件并不包含外部函数的代码,而是在运行时将动态链接库(若干外部函数的集合)加载到内存的某个位置,再在发生调用时去链接库定位所需的函数。

程序是如何在链接库内定位到所需的函数呢?

这个过程用到了两张表:GOTPLT。GOT全称是全局偏移量表(Global Offset Table),用来存储外部函数在内存的确切地址。GOT存储在数据段内,可以在程序运行中被修改。PLT全称是程序链接表(Procedure Linkage Table),用来存储外部函数的入口点(entry),换言之程序总会到PLT这里寻找外部函数的地址。PLT存储在代码段内,在运行之前就已经确定并且不会被修改,所以PLT并不会知道程序运行时动态链接库被加载的确切位置。那么PLT表内存储的入口点是什么呢?

就是GOT表中对应条目的地址:

外部函数的内存地址存储在GOT而非PLT表内,PLT存储的入口点又指向GOT的对应条目,那么程序为什么选择PLT而非GOT作为调用的入口点呢?
这样的设计是为了程序的运行效率。GOT表的初始值都指向PLT表对应条目中的某个片段,这个片段的作用是调用一个函数地址解析函数。当程序需要调用某个外部函数时,首先到PLT表内寻找对应的入口点,跳转到GOT表中。如果这是第一次调用这个函数,程序会通过GOT表再次跳转回PLT表,运行地址解析程序来确定函数的确切地址,并用其覆盖掉GOT表的初始值,之后再执行函数调用。当再次调用这个函数时,程序仍然首先通过PLT表跳转到GOT表,此时GOT表已经存有获取函数的内存地址,所以会直接跳转到函数所在地址执行函数。

第一次调用整个过程如下:

第二次调用整个过程如下:

上述实现遵循的是一种被称为LAZY的设计思想,它将需要完成的操作(解析外部函数的内存地址)留到调用实际发生时才进行,而非在程序一开始运行时就解析出全部函数地址。

这个过程也启示了我们如何实现函数的伪装,那就是到GOT表中将函数A的地址修改为函数B的地址。这样在后面所有对函数A的调用都会执行函数B。
那么我们的目标可以分解为如下几部分:确定函数A在GOT表中的条目位置,确定函数B在内存中的地址,将函数B的地址写入函数A在GOT表中的条目。

  • 1、如何确定函数A在GOT表中的条目位置?

程序调用函数时是通过PLT表跳转到GOT表的对应条目,所以可以在函数调用的汇编指令中找到PLT表中该函数的入口点位置,从而定位到该函数在GOT中的条目。例如:

call 0x08048430 <printf@plt>

就说明printf在PLT表中的入口点是在0x08048430,所以0x08048430处存储的就是GOT表中printf的条目地址。

  • 2、如何确定函数B在内存中的地址?

如果系统开启了内存布局随机化,程序每次运行动态链接库的加载位置都是随机的,就很难通过调试工具直接确定函数的地址。假如函数B在栈溢出之前已经被调用过,我们当然可以通过前一个问题的答案来获得地址。但我们心仪的攻击函数往往并不满足被调用过的要求,也就是GOT表中并没有其真实的内存地址。幸运的是,函数在动态链接库内的相对位置是固定的,在动态库打包生成时就已经确定。所以假如我们知道了函数A的运行时地址(读取 GOT 表内容),也知道函数A和函数B在动态链接库内的相对位置,就可以推算出函数B的运行时地址。

  • 3、如何实现 GOT 表中数据的修改?

很难找到合适的函数来完成这一任务,不过我们还有强大的ROP。假设我们可以找到以下若干条gadget,就不难改写GOT表中数据,从而实现函数的伪装。

pop eax; ret; 		# printf@plt -> eax
mov ebx [eax]; ret; # printf@got -> ebx
pop ecx; ret; # addr_diff = system - printf -> ecx
add [ebx] ecx; ret; # printf@got += addr_diff

从修改GOT表的过程可以看出,这种方法也可以在一定程度上绕过内存随机化。

四种常见的方法学习完毕,还得多动手实践才好,以上内容均参考长亭师傅们的文章,本文只是记录学习用。

Comments


:D 一言句子获取中...

Loading...Wait a Minute!