31c3 CTF - pong (pwn30)

For the 31c3 CTF, Eindbazen and fail0verflow joined forces as 0xffa, the Final Fail Alliance.
Don't miss out on other write-ups at Eindbazen's site!
pwn (30 pts)
To play it, connect to our server via:

socat -,raw,echo=0 TCP:
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)
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")

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:
    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")
ball_was_missed = ball_was_hit

""".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")

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")

Yup. 31c3ctf_fdf1c32f1259901a4baa14b50b710cfd.

comments powered by Disqus