[l] Ich reverse hier gerade ein 64-bit ARM Binary, das OpenSSL statisch reinkompiliert hat. In einer älteren Version, aber das spielt hier keine Rolle.

OpenSSL hat eine Funktion namens OPENSSL_cleans(ptr,len). Das ruft man auf Buffer auf, die gleich out of scope gehen, damit kein Schlüsselmaterial im Speicher rumliegt. Smarte Defense-in-Depth-Maßnahme. Inhaltlich ist das bloß ein memset(ptr,0,len).

Vor ein paar Jahren fiel auf, dass Compiler unter anderem die Funktion haben, sogenannte Dead Stores wegzuoptimieren. Das hier ist z.B. ein dead store:

void foo() {

int i;
for (i=0; i<5; ++i) {
puts("huhu");
}
i=0; /* Hat keine Auswirkungen, kann weg */
}

In welchem Kontext benutzt man jetzt OPENSSL_cleanse? In so einem hier:

  char key[128];

[...]
OPENSSL_cleanse(key,sizeof(key));
return 0;
}

Wenn der Compiler versteht, dass OPENSSL_cleanse keine Seiteneffekte hat außer key zu überschreien, dann ist das ein klarer Fall für die Dead Store Elimination. Ähnlich sieht es mit einem memset() for einem free() aus.

Das ist vor ein paar Jahren aufgefallen, dass Compiler das nicht nur tun können sondern sogar in der Praxis wirklich tun. Plötzlich lagen im Speicher von Programmen Keymaterial herum. Also musste eine Strategie her, wie man den Compiler dazu bringt, das memset nicht wegzuoptimieren. Das ist leider in portablem C nicht so einfach. Hier ist, wie ich das in dietlibc gemacht habe:

 1 #include &lt;string.h>

2
3 void explicit_bzero(void* dest,size_t len) {
4 memset(dest,0,len);
5 asm volatile("": : "r"(dest) : "memory");
6 }

Das magische asm-Statement sagt dem Compiler, dass der Inline-Assembler-Code (der hier leer ist) lesend auf dest zugreift, was er aber nicht tatsächlich tut. Damit ist der memset kein Dead Store mehr und bleibt drinnen. Leider ist das asm-Statement eine gcc-Erweiterung (die aber auch clang und der Intel-Compiler verstehen).

Hier ist die Lösung von OpenSSL:

 18 typedef void \*(\*memset\_t)(void \*, int, size\_t);

19
20 static volatile memset_t memset_func = memset;
21
22 void OPENSSL_cleanse(void *ptr, size_t len)
23 {
24 memset_func(ptr, 0, len);
25 }

Die Idee ist, memset nicht direkt aufzurufen sondern über einen Function Pointer. Wenn man den volatile deklariert, dann muss der Compiler annehmen, dass sich der Wert asynchon ändern kann. Kann er aber nicht, weil da nie jemand was anderes als memset reinschreibt. Das kann gcc leider erkennen, weil die Helden von OpenSSL das static volatile deklariert haben, und es damit nur innerhalb dieser Compilation Unit sichtbar ist, und da sind keine anderen Zugriffe. Niemand nimmt auch nur die Adresse davon.

Wenn ich das mit einem aktuellen gcc 13.1 übersetze, kommt eine Zeiger-Dereferenzierung heraus. Aber in dem Binary kommt ein ... inline memcpy raus. Die haben ihr altes OpenSSL mit einem alten gcc gebaut.

Gut, der alte gcc ist nicht schlau genug, dann Calls zu OPENSSL_cleanse wegzuoptimieren, insofern ... Operation erfolgreich?

Ich blogge das hier, damit ihr mal gehört habt, dass es im Umgang mit immer schlauer werdenden Compilern Untiefen gibt, die man möglicherweise intuitiv nicht auf dem Radar hat.

#fefebot #klarerFall

There are no comments yet.