V1 with content
This commit is contained in:
137
writeups/2025-07-20-Zeus.md
Normal file
137
writeups/2025-07-20-Zeus.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
title: "DownUnderCTF 2025: Zeus"
|
||||
excerpt: "Reverse-engineering Zeus!"
|
||||
tags: [ctf, rev]
|
||||
---
|
||||
|
||||
## Recon
|
||||
|
||||
The challenge gives us an executable file. As we usually do in reverse engineering challenges, we start by doing some recon on the file type, target architecture, and security:
|
||||
|
||||
```
|
||||
$ file zeus
|
||||
zeus: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=95542c1d888f30465172c1c77dd1eef1109b4c29, for GNU/Linux 3.2.0, not stripped
|
||||
$ checksec --file=zeus
|
||||
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
|
||||
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 39 Symbols No 0 1 zeus
|
||||
```
|
||||
|
||||
We can decompile it with Ghidra to extract some C code; after renaming some variables (and stripping some content for clarity) we get this:
|
||||
|
||||
```c
|
||||
undefined8 main(int argc,long argv)
|
||||
{
|
||||
int result;
|
||||
undefined8 local_98;
|
||||
undefined8 local_90;
|
||||
// [REDACTED FOR SIMPLICITY]
|
||||
char *local_18;
|
||||
char *str1;
|
||||
|
||||
str1 =
|
||||
"To Zeus Maimaktes, Zeus who comes when the north wind blows, we offer our praise, we make you wel come!"
|
||||
;
|
||||
local_18 = "Maimaktes1337";
|
||||
local_58 = 0xc1f1027392a3409;
|
||||
// [REDACTED FOR SIMPLICITY]
|
||||
local_30 = 0x3a110315320f0e;
|
||||
uStack_29 = 0x4e4a5a00;
|
||||
if (((argc == 3) && (result = strcmp(*(char **)(argv + 8),"-invocation"), result == 0)) &&
|
||||
(result = strcmp(*(char **)(argv + 0x10),str1), result == 0)) {
|
||||
puts("Zeus responds to your invocation!");
|
||||
local_98 = local_58;
|
||||
local_90 = local_50;
|
||||
local_88 = local_48;
|
||||
local_80 = local_40;
|
||||
local_78 = local_38;
|
||||
local_70 = local_30;
|
||||
uStack_69 = uStack_29;
|
||||
xor(&local_98,local_18);
|
||||
printf("His reply: %s\n",&local_98);
|
||||
return 0;
|
||||
}
|
||||
puts("The northern winds are silent...");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
What we can understand from this snippet is that the program is waiting for 3 arguments; the program name, an option `-invocation` and some invocative gibberish for Zeus. After this, the program apparently does some calculations (notably XOR) with pre-defined values and gives us a reply that could be the flag we're looking for.
|
||||
|
||||
What we could do is simply provide the arguments to the executable, but it is funnier to do it the other way; as we have the executable, as the flag is embedded in it some way, and as we have complete control over it, we can simply use a debugger to pass each check and get the right answer.
|
||||
|
||||
As the executable is targeting 64-bit x86 for GNU/Linux based systems, we can derive from this the calling convention used. In this case, it is the [System V ABI](https://wiki.osdev.org/System_V_ABI) (Application Binary Interface). There, parameters to the functions are passed into the `rdi`, `rsi`, `rdx`, `r10`, `r8`, and `r9` registers, in this order. The result of the operation is then stored in the `rax` register.
|
||||
|
||||
Here, we'll be looking at the result of the `strcmp` calls that should be zero; so what we'll do is, after each call to strcmp, just set `rax` to zero and step up to the next instruction.
|
||||
|
||||
By disassembling the binary and focusing on the string comparison part, we get:
|
||||
|
||||
```nasm
|
||||
0x0000000000001265 <+157>: add rax,0x8
|
||||
0x0000000000001269 <+161>: mov rax,QWORD PTR [rax]
|
||||
0x000000000000126c <+164>: lea rdx,[rip+0xe0a] # 0x207d
|
||||
0x0000000000001273 <+171>: mov rsi,rdx
|
||||
0x0000000000001276 <+174>: mov rdi,rax
|
||||
0x0000000000001279 <+177>: call 0x1050 <strcmp@plt>
|
||||
0x000000000000127e <+182>: test eax,eax
|
||||
0x0000000000001280 <+184>: jne 0x132c <main+356>
|
||||
```
|
||||
|
||||
Here, as specified in the calling convention, arguments are passed into `rdi` and `rsi`, then the function is called, and then the program tests if `eax` (the codeword for the lower 32-bits of `rax`) is zero.
|
||||
|
||||
## Exploitation
|
||||
|
||||
We set a breakpoint at `*main+177`, and another one at `*main+214` (the second `strcmp` call) and run the program, providing whatever garbage as arguments (as long as `argc == 3`):
|
||||
|
||||
```nasm
|
||||
$ gdb --args ./zeus hello world
|
||||
(gdb) b *main+177
|
||||
Breakpoint 1 at 0x1279
|
||||
(gdb) b *main+214
|
||||
Breakpoint 2 at 0x129e
|
||||
(gdb) r
|
||||
Breakpoint 1, 0x0000555555555279 in main ()
|
||||
(gdb) x/3i $rip
|
||||
=> 0x555555555279 <main+177>: call 0x555555555050 <strcmp@plt>
|
||||
0x55555555527e <main+182>: test eax,eax
|
||||
0x555555555280 <main+184>: jne 0x55555555532c <main+356>
|
||||
```
|
||||
|
||||
Okay, we're at the interesting part. Let's step to the test instruction, and at this point, set the register to zero. Then, we'll step again and pass over the jump, continuing the execution normally until the next check.
|
||||
|
||||
```nasm
|
||||
(gdb) si
|
||||
0x0000555555555050 in strcmp@plt ()
|
||||
(gdb) finish
|
||||
Run till exit from #0 0x0000555555555050 in strcmp@plt ()
|
||||
0x000055555555527e in main ()
|
||||
(gdb) x/i $rip
|
||||
=> 0x55555555527e <main+182>: test eax,eax
|
||||
(gdb) set $rax = 0
|
||||
(gdb) ni
|
||||
0x0000555555555280 in main ()
|
||||
(gdb) x/i $rip
|
||||
=> 0x555555555280 <main+184>: jne 0x55555555532c <main+356>
|
||||
(gdb) ni
|
||||
0x0000555555555286 in main ()
|
||||
(gdb) x/i $rip
|
||||
=> 0x555555555286 <main+190>: mov rax,QWORD PTR [rbp-0xa0]
|
||||
```
|
||||
|
||||
As you can see here, we effectively bypassed the check for the first argument. We will then do the same for the last argument; stepping until `strcmp`, setting `rax` to zero before the test instruction, then step, you know the drill:
|
||||
|
||||
```nasm
|
||||
(gdb) c
|
||||
Continuing.
|
||||
|
||||
Breakpoint 2, 0x000055555555529e in main ()
|
||||
(gdb) ni
|
||||
0x00005555555552a3 in main ()
|
||||
(gdb) set $rax = 0
|
||||
(gdb) c
|
||||
Continuing.
|
||||
Zeus responds to your invocation!
|
||||
His reply: DUCTF{king_of_the_olympian_gods_and_god_of_the_sky}
|
||||
[Inferior 1 (process 7271) exited normally]
|
||||
```
|
||||
|
||||
We bypassed the checks; the flag is now ours!
|
||||
Reference in New Issue
Block a user