写在前面:emmm,虽说我做出了hgame pwn的最后一题,但是里面涉及io_file的知识实际上再网上是有完全相同的payload,即是在libc2.23下无show函数泄露出libc地址的方式,是有现成的payload的(即修改stdout结构体的方式),但是具体的原理我并不是很明白,这让我心中很不安,其次我对于系统的输出的包装(众所周知,glibc不过也是在user模式下的代码,真正的输出是靠syscall)也一直很好奇,于是我打算花点时间来RTFSC来明白整个io_file的运作方式,这次我也是通过在源码旁边写注释的方式来进行代码解读,而这篇文章的用意只是为了写写心得以及做些总结性的描述
一些结构体
io_file结构体最核心的就是FILE结构体了,即_IO_FILE结构体,但是会发现每一个文件的结构体并不仅仅是IO_FILE,而是_IO_FILE_plus结构体,这里我一直没有搞明白他们到底是哪个结构体,所以,STFSC:FILE *stdin = (FILE *) &_IO_2_1_stdin_;
struct _IO_FILE_plus _IO_2_1_stdin_
,请读者仔细想想到底是哪个结构体,actually,两个都是,对于用户(用户函数)而言,stdin是FILE结构体的指针,对于libc内部而言是_IO_FILE_plus类型的指针,只是通过将IO_FILE结构体作为其第一个成员实现安全转换(c语言特性),也可以这么说:stdin是FILE类型,_IO_2_1_stdin_是plus类型
至于IO_FILE结构,我找了好久找不到😥,主要是我不太会使vscode,ctags和F12都找不到,最后找到是在libio/bits/type/struct_FILE.h找到的,哎,libc中的文件布局,解决函数依赖,函数定义这些东西真够我好好学习的
puts函数
包装函数
下面请欣赏_IO_sputn是如何体现vtable的调用的,首先这个函数实际上是个宏
通过这一番操作最终调用函数指针__xsputn指向的函数,参数分别是fp,data和长度n,其实我很好奇一件事,这些std的io流的file和vtable是如何初始化的,这是在找__xsputn指向的函数冒出来的一个很自然的想法,这个函数指针初始为NULL,是怎么赋值一个函数块的呢,在我的不懈寻找下,在stdfiles.c中有其初始化的代码(首先要知道IO_2_1_stdout才是原始的结构)
用宏来初始化,使得编译时就成功初始化,so,IO_2_1_stdout不过就是libc中的一个有初始值的全局变量罢了,所以pwn中覆盖stdout就要在libc中找,而其他的文件则在堆中
这样我们就顺藤摸瓜的找到了__xsputn的默认函数,即_IO_file_xsputn(这里提醒一点:函数名同时也是函数指针,它的值指向这个函数的第一条指令),这个函数在fileops.c中,其实这个符号只是对外展示的,其真正实现的函数叫做_IO_new_file_xsputn
这个vtable很强呀,C++中对于一个类如果将一个方法声明为virtual,那么编译器会为这个类创建一个vtbl,继承之后,可以根据指针指向的对象来调用相应的方法(基类或继承的类),从而实现动态binding,virtual的实现在c标准库中也能看到,其实目的是为了实现Linux的万物皆文件,File既可以是device,也可以是真正的file,他们都使用同一个接口来操作,但是这个接口背后是多态的,我之前写nanos-lite,为了实现这个我记得只是增加了一个判断来调用不同的函数,我靠不对,我想起来了,似乎也是虚函数,让我看看
有意思,c库和syscall实现都使用了类似虚函数表的操作,值得学习