返回导向编程

ldd命令 :查看一个可执行文件用到的所有的动态链接库。

软连接相当于一个快捷方式。linux下的symbolic link 就相当于windows的快捷方式

动态连接库会不定时的更新,软连接指向动态链接库,无论动态链接库怎样更新,用软连接打开,都是此动态连接库。软连接的名字是不会变的。执行软连接或者直接执行动态链接库的效果是一样的。

可以把动态链接库当成一个可执行文件直接执行。

将libc file之后发现其被编译为地址无关代码,地址无关代码本意是用来编写动态链接库,还可以作为程序的保护措施,使得ELF文件的载入位置不断的在变化。

所谓的动态链接库就存在了pwd目录下的一个可执行文件

eax保存系统调用的编号,ebx,ecx,edx依次保存系统调用所用的参数,在执行int 0x80,0x80是中段号,0x80这个中断号还对应一个系统调用号。进入中断号后就要系统调用一个系统调用号(execve所对应的系统调用号),execve所对应的系统调用号为11转换为16进制是0xb

缺乏连续的代码,就可以用ROP来执行分离的代码片段组合来达到相同的效果。

标红的数据为栈溢出的数据

因为没有连续的代码片段,所以只溢出到return adress时无法返回到我们想要的地址,所以我们应该继续向上溢出,溢出的内容为pop_ret 的地址。形成一个链状结构,call一个函数返回到比他地址更高的一个函数。在后续return到的位置填上gadget的地址。

xor eax ,eax :将eax清空,为eax赋值为0。异或指相同为0,不同为1。同一寄存器保存的东西相同,所以进行异或后,eax为0

text段中的代码片段(比如pop_ebx_ret)就是gadget,这些gadget组合起来就可以成为一个payload达到攻击的目的。

ROPgadget --binary 二进制文件名 only"pop|ret"  :输出只含有return或pop的指令

补充在IDA中鼠标停留在汇编页面,按G会弹出一个窗口,输入地址即可传送到此地址并指向相应的代码段。

函数的第一个栈帧是main函数的栈帧,main函数所调用的子函数的栈帧会在下面迭代。

ret2syscall

1.首先checksec ret2syscall

x86 32位的文件,canary保护和PIE保护都关闭,但是栈不可执行打开了,无法向栈中写入shellcode。

file ret2syscall出的结果与checksec不太一样,这里可以看到statically linked是静态链接。静态链接在编译的时候就已经将库代码保存在了文件当中 

2.其次用IDA进行反编译,进入IDA后首先应当想到其是否有后门函数,如果有后门函数直接返回到后门函数即可。查找system最简单的方法是可以在IDA左侧fouctions window按住ctrl +f输入system搜索,此题目并未找到system。

 也可以在代码区域按住shift+f12打开字符串窗口搜索/bin/sh,此题目中有。

双击进入后找到引用/bin/sh的位置。只有后门函数对应的参数,但是没有后门函数 。

  3.进行反编译看c语言查看漏洞

发现有gets函数,有gets函数的地方就有溢出。

4.寻找含有pop和ret函数的代码

4.进行gdb调试,一直r直到遇到gets函数输入垃圾数据

stack 35查到eap与ebp相差的位置为108个字节,返回地址下面还有4个字节,所以一共需要112个字节垃圾数据。

5.查找此文件中有关pop ret的指令

管道符的作用是前一个命令的输出与后一个命令的输入连接

grep作用是筛选出对它的输入中含有其后面第一个参数的 。

呈现出了含有eax的pop_ret。

再用此方法寻找含有ebx的pop_ret

选中0x0806eb90 

寻找int 0x80

 (1)寻找/bin/sh的地址

                

双击进入找到/bin/sh的地址。

 (2)寻找/bin/sh的地址

用python3寻找

from pwn import *

elf = ELF("./retsyscall")

hex(next(elf.search(b"/bin/sh“)))

hex函数是将找到的地址转换为16进制,找出含有/bin/sh的数据

(3)还可以通过ROPgadget获得

6.查看调用execve函数所需要用的指令

0xb是调用execve函数的系统调用号 

主要找eax和ebx,运气好的话不需要找ecx和edx,调用时默认赋值为0。

找到了pop_eax_ret与pop _edx_ecx_ebx_ret与/bin/sh与int 0x80的地址。

将需要的内容依次赋值,并逐渐ret形成链状结构,最后返回到int 0x80的地址。

进行ROP的第一步就是覆盖return address,这是ROP的入口。

首先溢出的应是pop_eax_ret的地址,经过return 0 的指令返回到此位置此时eip = gadget1,然后执行pop指令,eax = 0xb,执行ret返回,此时eip = gadget2,然后依次执行pop赋值的三个指令,此时edx = 0,ecx = 0,ebx = 0x80be408(/bin/sh)的地址,在执行ret返回到int 0x80,这就相当于执行了execve。

payload中用到flat函数,flat接收列表参数,会把其中每一项转为字节型数据,不足一个字节的数据填补起来 。

用python3执行此脚本攻击成功获得shell

本地攻击没有flag,需要在进行远程攻击 

 关掉test的PIE保护,并且将test改为动态链接dytest

添加static函数将test改成静态链接 statest

将dytest改名为dyntest 

ll指令:查看文件大小

dyntest为动态链接文件,statest为静态链接文件,原c语言文件为test。

静态链接文件大小远远大于动态链接文件,但运行效果一样

静态链接已经把库函数写进了elf文件,动态链接只是把它要用的库函数标记了一下 

plt段中存放的代码是用来解析code段中函数的真实地址,解析后将他填入data 段中got.plt节中

.got保存了整个程序的虚拟内存空间中全局变量各个符号的偏移量,也可以说成地址 

got.plt保存的是整个程序的函数地址

text段中的代码 call foo@plt,plt表中会产生函数的表项,然后call foo函数后会跳转到它对应的foo的表项。因为plt里本身也是代码,表项的第一个代码即是跳转到.got.plt表中的foo位置,由于是第一次调用foo函数,所以got.plt表中还未保存foo函数的真实地址,保存的是plt表中第二条指令的地址(foo@plt+1),然后就从got.plt表跳回去plt表了。跳回执行第二条代码,index是一个参数,再执行第三条指令,跳转到PLT0,然后依次执行PLT0中的指令 。

index指的是所调用函数在got.plt表中的位置(角标),此处为1

PLT表里的指令是在告诉你我用到了哪个链接库的函数,然后jump到 dl_resolve函数

dl_resolve函数会将解析到的真实地址填到got.plt表中foo函数的位置 

  

第二次调用foo函数,会jump到got.plt对应的位置直接跳转到foo的真实地址 。

gdb中输入plt 可以查看plt表中的程序,此程序只调用了put函数。

输入“x 地址 ”可以查看此地址的内容,默认是4字节。plt表中每一个表项的长度为16个字节。gpt.plt表中一个表项8字节,因为它只是用来存真实地址。

 

第一次调用put函数

查看got表发现其中的内容“0x401036”只占用了3字节,那么其他的5字节去哪了,实际上是省略了前导的0,原本为“0x0000000000401036”。后面的“push 0”即push index。

第二次调用put函数

发现got表中put函数的真实地址出现,而printf 和 exit 函数仍然是存表项。

用“disass 地址 ”反汇编一下得到put函数的汇编代码。

gdb调试的过程中在有c语言的前提下 , “b 行号 ”,可以在那一行打个断点。

输入“info b”可以查看断点信息。

输入“ d 断点编号”可以删除此断点。

输入“start”命令如果存在main函数那么会停留在main函数的第一条指令,如果没有则停留在libc_start_main处。

输入“backtrace”显示整个函数调用栈的状态。

输入“return”返回父函数。

杂论

ROPgadget一下发现gadget远远不够用。ret2syscall是静态链接,代码片段较多,而ret2libc1是动态链接,代码片段相对较少。

pwntools工具用elf = ELF("./文件") 就可以分析这个文件

elf.got["puts"] 此命令返回的是got表中puts函数表项的地址,不是puts函数的真正地址。

32位地址空间与64位地址空间的区别(左64右32)

 32位字节的栈地址为4个字节,64为8字节,但是由于8字节用不完,所以只显示6个字节。未开启PIE的时候默认的程序载入地址,64位是0x400,32位是0x804。这里保存的是数据段和代码段,保存的是ELF文件自己的东西。32位0xf7开头的是libc的内容。,0xfff打头的是栈的地址。

此时是将两个保护都关闭。 

-fno-stack-protector 是canary的保护开关

-no-pie   是 PIE保护的开关 

由于ASLR保护shellcode的地址每次运行都会发生改变,所以可以用nop指令(什么也不做),每当我们进行payload传送时指到nop时就可以一路nop到shellcode。  

子函数所需要的参数在父函数里,距离两个字长。local var距离参数是一个字长,但是通过system函数push一个ebp,也是两个字长。 exit函数同样。

最底下为system libc

这个就是call一个函数的方法:第一种是.got.plt表中还没有system函数的真实地址,它 到plt表函数的表项后调用got 表中的system不会返回真实地址,会直接返回到plt表此时需要执行resolve将解析函数得到的真实地址存放在got表中。第二种就是got表中已经存放了system的真实地址,直接调用got就返回了system的真实地址。

构造栈帧的时候并不知道system的真实地址所以就可以用system@plt代替。 

ret2libc1

先checksec

显而易见是gets出问题 

gdb调试得出所需溢出的数据为108+4位

这是我们所需构造的栈帧,padding即112位垃圾数据

在将之前所得的payload简化之后,将exit和他的参数删除,用4个字节的垃圾数据来代替exit函数

通过elf = ELF("./文件名")来连接此文件,就可以使用 elf.plt["system"]获得plt表中system表项的地址,即system@plt,然后再通过next(elf.search(b"/bin/sh"))来获取/bin/sh的地址。 

ret2libc2

老规矩先checksec一下

然后看到是32位的进入IDA反编译一下

shift +f12发现它没有/bin/sh

所以我们应该首先向程序中写入/bin/sh,然后再向payload中加上/bin/sh。/bin/sh不可以写入像栈一样被地址随机化的段,需要写入地址不会变化的位置,比如bss段。 

查看其bss段,发现其中开辟了一些文件,且开辟了一个数组char,我们就可以把/bin/sh放到这里。 

 

 此题目的栈应当为这样,从下往上依次,首先就是112位垃圾数据,然后调用get函数将/bin/sh写入bss段,再通过调用system函数得到shell。

 通过在gdb中输入plt可以看到程序所调用到的函数的plt表项的地址,然后可以找到gets@plt和system@plt的地址,buf2的地址IDA里可以找到。

在pytnon3中输入elf.symbols["符号"]因为buf2也是程序里的一个符号所以可以用这个函数获得其地址。输入elf.plt["调用函数"],填入gets可以得出plt表中gets函数的表项的地址。

 

这是攻击脚本,从IDA中得出程序会先给我们发送一些信息,所以我们应该先recv。因为我们payload中调用了gets函数,所以发送完payload后会执行gets函数,应当输入/bin/sh,将其传入bss段。\x00是个截断符。然后我们就得到了shell。

也可以这样构造payload。用完就扔。

原函数中的ret将gets@plt清空,执行完gets函数后就该执行pop函数,pop_ebp_ret转到eip中,将buf2  pop掉,然后顺势ret到system函数,然后依次弹出 

ret2libc3

首先依然是checksec

32位的elf 且是动态链接

首先程序输出了一些无关紧要的话,fflush函数是用来清零缓冲区的,手动将缓冲区的内容呈现出来,没有关闭缓冲区的时候程序会等到缓冲区满了之后才会呈现内容。read函数从缓冲区读进来10个字节的数据,0表示标准输入,1表示标准输出,2表示标准错误,&buf(存放地址)指向buf中读入数据,0xAu后面的u表示是无符号数。

最后一句程序让我们输入一个地址,当我们输入 12的时候,计算机看到的是一个字符串"12\n",然后转为ASCLL码就是”0x31320a"(16进制)

strol函数是将字符串转换成长整型long型,并将结果存放的v5里。将我们输入的字符串转换成了对应的整数值。  

有符号数据(int)的最高位表示正负,0是正数,1是负数。int型数据4个字节32位,所以有符号整数的范围是-2*31 +1 ~ 2*31 -1;   无符号数据(unsigned int)范围是0 ~ 2*32 -1

see_something函数是接收了一个指针,然后将指针对应的内容打印了出来。 

在IDA中选中一个变量名之后按住键盘的N就可以重命名此变量。

DWORD表示2*2  4字节,QWORD表示 2*4  8字节

进入Print_message函数后发现strcpy函数出现漏洞,所要复制的数据的长度远远大于自身的数据大小。

 

我们执行这个程序,由IDA我们已经知道我们应该先输入一个地址

当我们输入16进制的地址时它显示段错误,我们不妨将地址转换为10进制的

可见它See_something函数给我们传递了以此数据为地址的数据,但是他发出的是16进制的,转换为10进制在换成对应的ASCLL码就是该地址所存数据的内容。

第二个read函数读取0x100的数据,然后我们发现定义src时它距离ebp是0x10E个字节所以不会发生溢出漏洞, 第一个read亦是如此。

这道题目既没有/bin/sh也没有system,plt表中也没有system函数表项的地址,那我们可以从哪里泄露system函数的地址呢,不难想象see_something函数正好给我们提供了一个泄露的平台,我们之前提到.got.plt表中存放了函数的真实地址,我们可以通过发送got表的地址泄露system函数的真实地址。但是因为此程序没有调用system函数所以我们无法查到got表中system的真实地址,只能找到plt表项的地址,是elf保存的,不是libc所对应的地址。

在gdb中查看got表中的内容 0x80开头的是一次都没有调用的就是got表中还是ELF的地址,而0xf7开头的是已经调用过一次的函数,是libc中对应的地址。左边对应的是函数在got表中表项的地址。

我们可以在第一次read函数时输入puts@got的地址(注意转换为10进制),他就会给我们返回puts@got里的内容即0xf7deb2a0,然后我们再次输入这个地址就可以返回puts函数的内容,可能我们会疑惑,我们为什么不能直接输入puts@got里的内容呢,还要多输出一次,因为我们每一次运行程序puts的地址可能会发生变化,因为ASLR保护,远程的libc的地址是随机化的。

构造的栈帧如下

由于Libc.so是直接复制粘贴过来的所以里面的函数的地址差是不变的,我们可以调试libc.so得到这个值,我们又知道puts函数的真实地址,就可以推出system函数的真实地址。 

开始攻击

首先我们在gdb中步过等到程序调用完puts函数时就输入got,得到puts函数在plt表的表项的地址,然后继续步过第一次read将它读进去,步过到See_something函数时他就会给我们返回puts函数在got表中保存的真实地址。 上面的图片最顶端显示出0xf7deb2a0就是真实地址。

我们不难发现虽然存在ASLR保护使puts函数的地址发生变化,但是后三位却不变化,这涉及到了,程序从虚拟内存到物理内存的知识,我们知道4gb的内存空间有3gb是用来存程序的内容,而另1gb是存kernel的,但是假如物理内存只有2gb的空间,那是不是不够用了呢?答案是 不是的。因为虚拟内存虽然有3gb大小,但实际程序所占用的也就几kb而已虚拟内存上的程序转移到物理内存时是会分页的,比如虚拟内存上有大小12kb的内容,但Linux下的是以4kb为一页分页的,这就会导致虚拟内存原本的12kb内容会被分成3块4kb的内容,在虚拟内存上是连续的,但是转移到物理内存上就是分散的不连续的。那为什么后三位一定呢?原因是这个4kb,12位二进制数据的大小就是2*12(4096),就是4kb,同时12位二进制就是3位十六进制。一页正好是4kb,而puts函数所在的这个页偏移后三位的页为141位,所以无论puts函数所在的这页怎么装载,它的后三位永远是140.

在python3中查看libc中俩函数的距离时不可以用got或plt,因为这两个都是程序里的,是在libc找东西的,用symols查看,然后输入此命令得到他们之间的距离,发现system是在低地址的位置。 

然后用之前得到的puts函数的真实地址加上这个差值就得到了system函数的地址。 

drop=true即表示去掉最后的\n。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值