前言
年三十晚上边看电视边搞了下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
|
|
编译下
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脚本
这个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就想写了,结果春节期间各(da)种(you)事(xi),愣是拖到现在,不过也总算是写下来了吧
参考文章:
https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/
http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html