Thumbnail: PatriotCTF

PatriotCTF 2020 - Give Nom Nom

on under writeups
7 minute read

This challenge was posted late in the contest, and I had some other things I had to take care of at the time. So in usual form, I threw angr at it and, eventually, got a solve. The first part is solving a constraint system where your input ends up getting permuted and then checked against some constraints. The second part is where you get to run a command is fed into system, of course after it has been run through the permutation as well.

Triage

As usual, let’s give it a run.

$ ./pwn_constraint 
Giv nom nom. I lik good nom nom
flag
BAD NOM NOM

strings shows a base64 encoded blob lFtkesoc2AuJ91Xp8s3g6x10SgnwaArVWcogixFRGgrnS1GBDvL9kxr7LAhjVd05LR1qnakKtxjxRT2TFI2hTCqw90yfP2O==. ltrace with the input of test shows a ton of strlen calls.

<clipped>
[pid 10029] [0x55710c875390] strlen("tste") = 4
[pid 10029] [0x55710c8753c4] strlen("sste") = 4
[pid 10029] [0x55710c87532f] strlen("stte") = 4
[pid 10029] [0x55710c875362] strlen("stte") = 4
[pid 10029] [0x55710c875390] strlen("stte") = 4
[pid 10029] [0x55710c8753c4] strlen("stte") = 4
[pid 10029] [0x55710c87532f] strlen("stte") = 4
[pid 10029] [0x55710c875362] strlen("stte") = 4
[pid 10029] [0x55710c875390] strlen("stte") = 4
[pid 10029] [0x55710c8753c4] strlen("stte") = 4
[pid 10029] [0x55710c87532f] strlen("stte") = 4
[pid 10029] [0x55710c875362] strlen("stte") = 4
[pid 10029] [0x55710c875390] strlen("stte") = 4
[pid 10029] [0x55710c8753c4] strlen("stee") = 4
[pid 10029] [0x55710c87532f] strlen("stet") = 4
[pid 10029] [0x55710c875362] strlen("stet") = 4
[pid 10029] [0x55710c875390] strlen("stet") = 4
[pid 10029] [0x55710c8753c4] strlen("ttet") = 4
[pid 10029] [0x55710c87532f] strlen("tset") = 4
[pid 10029] [0x55710c875362] strlen("tset") = 4
[pid 10029] [0x55710c875390] strlen("tset") = 4
[pid 10029] [0x55710c8753c4] strlen("tsst") = 4
[pid 10029] [0x55710c87532f] strlen("test") = 4
[pid 10029] [0x55710c875362] strlen("test") = 4
[pid 10029] [0x55710c875390] strlen("test") = 4
<clipped>

To be honest, I didn’t reverse this manually too much. Looking at the control flow and instructions, it seemed reasonable that angr would be able to solve this. I discovered two sections, which I solved concurrently with two different methods.

Constraint Check

Before you can do anything else, this binary is asking for your input, running some permutations on it, and then checking it against what it expects as output.

We can see the permutation by running some example data through it using revenge:

from revenge import Process, common, types
p = Process("./pwn_constraint")

# Grab the function you want to call
func = p.memory["pwn_constraint:0x12cf"]

# Create a string in memory so we can read it back after it's permuted
mem = p.memory.alloc_string(types.StringUTF8("ABCDEFG"))

# Run the string
func(mem.address)
# 70

# Read what it got permuted into
mem.string_utf8
# 'CEGAFBD'

You can play around with different inputs, but it seems to just scramble them.

The reasonable thing to do here would be to see if you can map out the input to the output, since it’s going to be deterministic. However, I just decided to throw angr at it instead.

My angr solve script is as follows:

import angr, claripy

proj = angr.Project("pwn_constraint")
base = 0x400000

# Avoid anything that goes to Bad nom nom
avoid = [base+0x14E9, base+0x154D] 

# Find my way PAST the first check, to the point where it prompts for input
# again
find = base+0x158B

# 96 based on the strlen check at the beginning
flag = claripy.BVS('flag', 8*96)

# Null terminate my flag
stdin = angr.SimFile('stdin', claripy.Concat(flag, 0))

state = proj.factory.entry_state(stdin=stdin, add_options=angr.options.unicorn)

# THIS IS IMPORTANT! See notes below for why.
state.libc.buf_symbolic_bytes = 128

# Tell angr that my flag should have no nulls and no newlines
for i in range(96):
    state.add_constraints(flag.get_byte(i) != 0, flag.get_byte(i) != 10)

simgr = proj.factory.simgr(state)

# go find it!
simgr.explore(find=find, avoid=avoid)

The key to this working is the step state.libc.buf_symbolic_bytes = 128. This is because angr needs to set some reasonable defaults for how big things can be. They do this to trade performance and completeness. In this case, however, their default was too small. When you run the above script without that line, you get unsat. As of writing, the default size for buf_symbolic_bytes is 60.

After kicking off angr, I decided to revenge to brute force a smaller search space to find /bin/sh, which I would use once I got past the first constraint.

System Constraint

Assuming that I would eventually get past the first constraint check, I needed to have something to give to system that would allow me to get the flag. Since I wasn’t actually reversing the permutation, I decided to just brute force something small, such as /bin/sh.

To brute force this, I used revenge, as follows:

#!/usr/bin/env python

from revenge import Process, common, types
import itertools

# This function simply runs the permutation function against some given input
# and returns the output
def try_permute(inp, mem): 
    mem.string_utf8 = inp 
    do_permute(mem) 
    return mem.string_utf8

p = Process("./pwn_constraint")

do_permute = p.memory['pwn_constraint:0x12CF'] 

# Just allocate a block of memory to mess with
mem = p.memory.alloc(128)

target = "/bin/sh"

# Brute force the answer by trying all permutations
for perm in itertools.permutations(target): 
    x = "".join(perm) 
    if try_permute(x, mem) == target: 
        print("Winner: " + x) 
        break 

# ns/hb/i

The script took a few minutes to run and find that ns/hb/i would get me what I wanted.

Final

The angr script probably took an hour or so to complete. It also found a solution that worked but was likely not the intended solution given that most of it was not ascii printable.

Non ascii wasn’t really an issue since it did validate, so I wrote that answer to disk and used cat and piping to communicate with the binary:

(cat win.bin; echo ns/hb/i; cat -) | nc chal.pctf.competitivecyber.club 5555
cat ./gmu/patriotCTF/pwn_constraint/flag_dir/flag.txt
pctf{f1b0N4Cc1_4nD_A_sHuffL3_n0m_n0m_n0m}

Downloads

patriotctf, 2020, writeup, reverse, radare2, ghidra, revenge