31c3 CTF - pong (pwn30)
pong
pwn (30 pts)
-------------------
To play it, connect to our server via:
socat -,raw,echo=0 TCP:188.40.18.92:2001
Have fun!
We are provided with the binary for the server, a Linux x86_64 binary that uses Lua. The service is a version of pong with configurable logic when the ball hits or misses a paddle that can be specified by the user in the form of Lua code.
First, we checked for a few recent Lua bugs/exploits and confirmed that the server is running the most recent version, and thus this probably isn’t about Lua bugs.
The Lua code gets the following globals only:
math -- (standard math library)
set_leader()
game_over()
score_points()
get_score()
print()
get_tick()
HUMAN = -1.0
NONE = 0.0
AI = +1.0
Auditing these, we quickly find that print()
(C function l_print()
) calls
log_this, which is a printf
-style function, but does not call it with '%s'
as an argument, and instead passes the user-supplied string directly as the
format string - a classic format string vuln.
Unfortunately for us, exploiting this one is trickier than usual. Format string
exploits work by using the %n
format specifier to write the number of
characters written so far (as an 8-, 16-, 32-, or 64-bit integer) to an
address passed to the printf function, that is, on the stack. When we can
control a buffer on the stack that exists before (i.e. at a higher address)
the frame of the function calling printf
, we can use this to build a powerful
write-anything-anywhere primitive. However, we don’t control any such buffers
in this server, so we’re limited to writing to addresses that happen to be on
the stack already. Bummer.
Thankfully, by dumping the stack with gdb locally, we were able to grep for
interesting addresses and found that, when called from the ball_was_hit
or
ball_was_missed
callback in Lua, the address PLT entry for the
lua_pushinteger
function ends up on the stack. This allows us to replace it
with a (almost) arbitrary pointer, and direct execution to an address of our
choosing. Calling get_score()
from Lua will trigger a call to that address.
The server developer conveniently left in a dummy reference to the system()
libc function for our use. Unfortunately, lua_pushinteger
is called with the
Lua state structure as the first argument and a somewhat-controllable integer
as the second, which doesn’t let us call something useful like /bin/sh
, so
we had to find something else to call. After struggling to come up with
something useful, comex reminded me of the LuaL_openlibs()
function, which
also takes in the Lua state as the first (and only) argument, and registers a
lot of useful libraries, including os.execute
which is just system()
.
Jackpot.
But not quite yet. We want to point to a function that is not referenced from
the server binary, so we need to point directly into liblua
. This means
we need to find out the exact version used. The library is also subject to
ASLR, but leaking addresses is trivial with printf - as it turns out, the return
address back into liblua
from C callbacks can be printed with %8$p
, and the
low bits match the location of an instruction after an indirect call in
liblua5.2-0_5.2.3-1_amd64.deb
(Ubuntu 14.04 Trusty).
Here comes the problem: we can only overwrite the base address of the
lua_pushinteger
function with the number of bytes written so far to the target
string (which is thankfully length-limited, so we can make this very large
without making the whole thing explode). Typical format string exploits will
write in 8-bit units to address+3, address+2, address+1, address+0 in sequence
to build a 32-bit (or 64-bit) write without having to “print” ludicrous amounts
of charaters, but we have no such luxury. Addresses inside the binary (which is
an ET_EXEC
binary and thus not itself subject to ASLR) are small enough that
we can afford to do this, but we don’t have a reference to LuaL_openlibs
, so
we need to point directly into the library, which gets loaded at a much higher
address. We’re overwriting the address of one function in the library with
another, so the top bits of the address will be the same. However, the delta
between LuaL_openlibs
and lua_pushinteger
is greater than 16 bits, so we
have to go for a 32-bit overwrite - potentially requiring up to 4 billion
characters to be written. This doesn’t work - the pong code sets up a timeout
for Lua code execution of two seconds, and we need way longer than that to
let the printf
core write billions of characters.
Thus, we reach the lame part of the exploit. We can just reconnect to the server (manually) a few times, leaking the address of liblua each time, and try again until ASLR happens to place it at an address where bits 28-31 are 0 (the first hex character of the bottom 32 bits of the address is 0). This happens one in 16 times on average, so it’s not too bad.
And so, we repeatedly reconnect and run the following config to get the address:
print("a %8$08x")
END_OF_CONFIG_FILE
And once it happens to start with a 0, we feed it to this bit of Python code that generates the right exploit code:
while True:
print
off = raw_input()
# the return address is at 0xb61d, while LuaL_openlibs is at 0x22d60
v = int(off, 16) - 0xb61d + 0x22d60
v = v & 0xffffffff
print """
function ball_was_hit(who)
print("a %8$lx")
print("c %63$lx %64$lx")
print("d %65$lx %66$lx")
print("%!ld%66$n")
get_tick(1)
os.execute("/bin/sh")
end
ball_was_missed = ball_was_hit
END_OF_CONFIG_FILE
""".replace("!", str(v))
The a, b, c lines are for debugging (showing some nearby data in the stack),
but also because at least one print seems to be required before the address of
lua_pushinteger
ends up on the stack for some reason.
Run the new config and… bingo! A shell. For… one second. Turns out the
alarm()
is still active, and kills our shell. Okay, fine, we’ll do this
the hard way. Guesswork time.
...
os.execute("pwd; ls -al; cat *flag*; cat /home/user/flag; /home/user/flag")
...
Well that didn’t work - the game scribbled all over our output in the console after it ran. Meh. We could dump the output to a file for further analysis, or set up a bindshell that is detached from the parent process and immune to the alarm, but this is getting stupid, so let’s just do it the evil way.
...
os.execute("pwd; ls -al; cat *flag*; cat /home/user/flag; /home/user/flag ; sleep 100")
print("%1$n%2$n%3$n%4$n%5$n")
...
This tries to force the shell to time out, and just in case, makes the game segfault if execution reaches the print. Can we have the flag, please?
No, turns out we guessed wrong. ls
says there is a getflag
binary in the
current directory that we have to run. Fourth time’s the charm?
...
os.execute("pwd; ls -al; ./getflag; sleep 100")
print("%1$n%2$n%3$n%4$n%5$n")
...
Yup. 31c3ctf_fdf1c32f1259901a4baa14b50b710cfd
.