top of page

Who Can Hack the Gibson? US Cyber Open Champion Round



If you’re not familiar with CTFs (Capture the Flags), visit CTF 101 for more information.


The US Cyber Games recently completed the first event of Season II—the US Cyber Open CTF. A few weeks ago, RII was invited by Head Coach O'Connor to provide a challenge for the "Champion Round," a second group of harder challenges released halfway through the event to truly separate top performers. At the end of the CTF, our challenge was the second-least solved pwnable with 10 solves. In this blog post, we will describe the motivation and development of the Gibson challenge and provide some high-level details about the example solution.


Motivation of the challenge

We knew that we should provide a harder challenge, but we also did not have a lot of extra cycles to put something together before the deadline. RII security research looks at a variety of computer architectures, and we wanted to incorporate something like that into the CTF. To that end, we devised a small and straightforward pwnable challenge in the obsolete IBM mainframe architecture S390X, or z/Architecture. This satisfied several of the characteristics we wanted in our challenge:

  1. Memorable, unique, and challenging (hopefully in all the right ways)

  2. Be symbolically representative of the challenges we encounter in our research

  3. Identify competitors who can learn on-the-fly and adapt quickly in previously unfamiliar domains, including endianness issues

  4. Identify competitors who can modify their tooling and develop a working exploit in the absence of well-supported tools (e.g., pwntools, capstone, and most disassemblers do not support S390X correctly)

  5. Able to develop without an undue time burden given the approaching deadline

Although most available tools did not work properly for S390X, it is notable that QEMU, GDB, and Linux have reasonable support. This allows us to set up and deploy a Docker Compose dynamic analysis environment for competitors to do their engineering with minimal time wasted on boring tasks like setting up a dev environment.


Docker Compose environment

Instructions to build/use two Docker containers were included in the distributed tips.md file to competitors. The Infrastructure container listened only on port 9999 and was representative of the actual setup of the hosted challenge (just with a fake flag included). The Competitor container listened for a connection on port 8888, and then would hang and wait for a GDB connection to port 1234. This remote debugging allowed competitors to have an introspection capability for dynamic analysis while still being able to develop in their usual environment with whatever tools and dependencies they were accustomed to.


This setup also should have lowered player frustration, as they could simply use the Docker environment for debugging and not have to worry about setting up emulation or having a discrepancy between their environment and the hosted challenge. However, "the best laid plans of mice and men"... once deployed, we found multiple players had issues with it not behaving properly. After some diagnosing, we found the Docker and Docker Compose versions needed to be fairly precise (we used 20.10.16 and 2.5.0, respectively) and running docker run --rm --privileged multiarch/qemu-user-static --reset -p yes was a requirement, not just an option.


Finally, Hosted CTFd and Google Cloud Platform both had issues when trying to run the Docker image of our challenge, likely due to it being based from the s390x/ubuntu:22.04 image. Huge shout-out to @cyberskillz who put in a large effort to deploy the challenge, but for the sake of time we ended up taking submissions of scripts and performing manual (local) testing instead of deploying the challenge directly to the cloud.


Development and resources

Through some Google-fu, it is possible to find S390X resources such as the syscall table, annotated assembly examples and debugging information, and other reference materials. This should be enough to learn the fundamentals of the architecture and get you to the point where you can write Just Enough(TM) shellcode or find relevant ROP gadgets.


From a tooling perspective, I found s390x-linux-gnu binutils to be the most effective, and a combination of objdump and grep was my personal setup when creating my solution. Other tools that advertised S390X support did not work completely (e.g., capstone disassembler could not identify most of the instructions during my testing). Some competitors mentioned radare2 supporting it properly, but I did not test that during my development. Pwntools was still useful for input/output and some lookups within the ELFs, but other functionality was not useful as it depended on capstone-engine.


Example (intended) solution

Of course there are many valid approaches to the problem, but this was the exploit technique we used for our testing script.


Finding the vulnerabilities

We will use the following objdump of main() as the basis for our reverse engineering and discussion:

It may be harder to see without syntax highlighting or graph view, but we can still easily identify the function calls to memset() (0x1000896), read() (0x10008c6), and printf() (0x1000928). Even if we haven't seen this ISA before, we can look at the arguments used to make some quick assumptions:

  1. The memset() zeroes out a buffer on the stack of length 1024 (lghi %r4, 1024)

  2. The read() will consume up to 2000 bytes (lghi %r4, 2000)

  3. The memset(), read(), and printf() all take the same stack variable as their buffer argument (aghik %r1, %r11, 160)

Without even running it, we already have a strong suspicion that there is a stack-based buffer overflow and a format string vulnerability. We then run the program with some sane input ("Hello World"):

Hmm, processed data? That seems a bit unexpected, but also looks very simple. Just a bunch of R's? If we look closer at objdump, we see an xilf %r1, 82 at 0x10008ec. Although that mnemonic doesn't seem immediately decipherable, we hypothesize that 82 == 'R' and the X in xilf likely stands for XOR—both things we can quickly confirm to be true. This simple encoding will impact the first 1024 bytes of any payload we use (clgfi %r1, 1023 ; jle 10008e2).


Bypassing mitigations

Looking at mitigations, we see this is a non-PIE binary with no stack canaries, but ASLR is enabled. We also only get one attempt at input/output before the program exits. With such a small binary, there are relatively few ROP gadgets to work with, and we will need more interactions with the program if we are to successfully leak information to be used in an exploit. Thus, the first step is overwriting the return address from main to go again to the start of main, essentially allowing unlimited iterations of the main execution loop.


Next, we need to bypass ASLR so we can find more useful ROP gadgets and/or function addresses in libc. This is achieved by abusing the printf() at 0x1000928. We use the format specifiers %149$p and %195$p to leak both the stack pointer and a libc address. Now, we can successfully reference addresses in libc and chain multiple gadgets together because we can suitably restore the values in the %r15 stack pointer register (note that S390X does not have built-in push/pop instructions for us to leverage).


Diverting control flow

At this point, because input is processed using stdin/stdout, we can use the wonderfully simple magic gadget to automatically set up a call to execve("/bin/sh", NULL, NULL). While one_gadget is my favorite tool for that job, it does not support S390X (surprise surprise). Instead, we find our own magic gadget manually by looking for references to execve() in the objdump of libc. We find this gem:

It's not obvious here, but using breakpoints and/or comparing to x86 versions of libc, we can see %r2 is populated to the string "/bin/sh". There are actually two different magic gadgets in this snippet, just with different preconditions:

  1. 0xda37a - assuming %r8 and %r10 are suitable argv/envp pointers (NULL is good)

  2. 0xda382 - assuming %r3 and %r4 are suitable argv/envp pointers (NULL is good)

Unfortunately, neither of these are true at the time we gain control of the instruction pointer and can alter control flow. However, we can satisfy this by leveraging another gadget: lmg %r8, %r15, 224(%r15) ; br %r14. This "load multiple (giant?)" instruction will load new values from memory into all the registers from %r8 to %r15 in consecutive order starting 224 bytes forward from %r15, then jump to %r14. While the mnemonics are different, the concept is the same. This essentially cleans up a stack frame and returns to the stored instruction pointer on the stack. Variations of this gadget are plentiful, and due to calling conventions, it is much easier for us to use this to control both %r8 and %r10, satisfying both preconditions, and then jumping to the magic gadget.


Voila!


Whoops, made it easier

As often happens during challenge development (especially if in a time crunch), it's easy to accidentally make exploitation easier by introducing a different primitive or vulnerability than intended. In this case, by making a fairly late edit to introduce the printf() for info leak, I did not account for doing regular old format string exploitation with the %n specifier. Pwntools still did a good job with automatically crafting the fmtstr payload, and I saw some competitors choose to simply overwrite the hardcoded printf@GOT address and make the use of printf() actually just call system(). This merely required leaking a libc address, calculating a single offset, and then performing the write-what-where to make a completely controlled call to system() with user data. This circumvented much of the need to learn anything specific about the S390X architecture. In hindsight, I should have found more time to make a more creative infoleak, or at least filtered out the %n format specifier. But great job to the competitors who found this path of least resistance.


Competitor write-ups

If you're interested in seeing this from the competitor perspective, check out these great write-ups from players who solved this during the event!

Conclusion

RII extends a huge thank you to both the organizers and the participants of the US Cyber Open, and the invitation to contribute a challenge. We appreciate the stamina from everyone involved to run a 10-day CTF, and a willingness to stick around and try our challenge when it was released on day 5. Congratulations to the top 120 players who will advance to the combine. We are excited for the selection of the US Cyber Team who will represent the United States at the second International Cybersecurity Challenge!

231 views
bottom of page