Es scheint keine einfache Laufzeitmethode zum Patchen der Feature-Erkennung zu geben. Diese Erkennung erfolgt ziemlich früh im dynamischen Linker (ld.so).
Das binäre Patchen des Linkers scheint im Moment die einfachste Methode zu sein. @osgx hat eine Methode beschrieben, bei der ein Sprung überschrieben wird. Ein anderer Ansatz besteht darin, das CPUID-Ergebnis einfach zu fälschen. Normalerweise cpuid(eax=0)
gibt die höchste unterstützte Funktion in eax
zurück während die Hersteller-IDs in den Registern ebx, ecx und edx zurückgegeben werden. Wir haben dieses Snippet in glibc 2.25 sysdeps/x86/cpu-features.c
:
__cpuid (0, cpu_features->max_cpuid, ebx, ecx, edx);
/* This spells out "GenuineIntel". */
if (ebx == 0x756e6547 && ecx == 0x6c65746e && edx == 0x49656e69)
{
/* feature detection for various Intel CPUs */
}
/* another case for AMD */
else
{
kind = arch_kind_other;
get_common_indeces (cpu_features, NULL, NULL, NULL, NULL);
}
Die __cpuid
Zeile übersetzt in diese Anweisungen in /lib/ld-linux-x86-64.so.2
(/lib/ld-2.25.so
):
172a8: 31 c0 xor eax,eax
172aa: c7 44 24 38 00 00 00 mov DWORD PTR [rsp+0x38],0x0
172b1: 00
172b2: c7 44 24 3c 00 00 00 mov DWORD PTR [rsp+0x3c],0x0
172b9: 00
172ba: 0f a2 cpuid
Anstatt Zweige zu patchen, könnten wir also genauso gut den cpuid
ändern in nop
Anweisung, die zum Aufruf des letzten else
führen würde (da die Register kein "GenuineIntel" enthalten). Seit Anfang eax=0
, cpu_features->max_cpuid
wird auch 0 sein und der if (cpu_features->max_cpuid >= 7)
wird ebenfalls umgangen.
Binäres Patchen cpuid(eax=0)
durch nop
Dies kann mit diesem Dienstprogramm erfolgen (funktioniert sowohl für x86 als auch für x86-64):
#!/usr/bin/env python
import re
import sys
infile, outfile = sys.argv[1:]
d = open(infile, 'rb').read()
# Match CPUID(eax=0), "xor eax,eax" followed closely by "cpuid"
o = re.sub(b'(\x31\xc0.{0,32}?)\x0f\xa2', b'\\1\x66\x90', d)
assert d != o
open(outfile, 'wb').write(o)
Eine äquivalente Perl-Variante, -0777
stellt sicher, dass die Datei sofort gelesen wird, anstatt Datensätze bei Zeilenvorschüben zu trennen:
perl -0777 -pe 's/\x31\xc0.{0,32}?\K\x0f\xa2/\x66\x90/' < /lib/ld-linux-x86-64.so.2 > ld-linux-x86-64-patched.so.2
# Verify result, should display "Success"
cmp -s /lib/ld-linux-x86-64.so.2 ld-linux-x86-64-patched.so.2 && echo 'Not patched' || echo Success
Das war der einfache Teil. Nun wollte ich nicht den systemweiten dynamischen Linker ersetzen, sondern nur ein bestimmtes Programm mit diesem Linker ausführen. Klar, das geht mit ./ld-linux-x86-64-patched.so.2 ./a
, aber die naiven gdb-Aufrufe konnten keine Haltepunkte setzen:
$ gdb -q -ex "set exec-wrapper ./ld-linux-x86-64-patched.so.2" -ex start ./a
Reading symbols from ./a...done.
Temporary breakpoint 1 at 0x400502: file a.c, line 5.
Starting program: /tmp/a
During startup program exited normally.
(gdb) quit
$ gdb -q -ex start --args ./ld-linux-x86-64-patched.so.2 ./a
Reading symbols from ./ld-linux-x86-64-patched.so.2...(no debugging symbols found)...done.
Function "main" not defined.
Temporary breakpoint 1 (main) pending.
Starting program: /tmp/ld-linux-x86-64-patched.so.2 ./a
[Inferior 1 (process 27418) exited normally]
(gdb) quit
Eine manuelle Problemumgehung wird in How to debug program with custom elf interpreter beschrieben? Es funktioniert, aber es ist leider eine manuelle Aktion mit add-symbol-file
. Es sollte jedoch möglich sein, es mit GDB Catchpoints ein wenig zu automatisieren.
Ein alternativer Ansatz, der keine binäre Verknüpfung verwendet, ist LD_PRELOAD
Erstellen einer Bibliothek, die benutzerdefinierte Routinen für memcpy
definiert , memove
usw. Dies hat dann Vorrang vor den glibc-Routinen. Die vollständige Liste der Funktionen ist in sysdeps/x86_64/multiarch/ifunc-impl-list.c
verfügbar . Das aktuelle HEAD hat insgesamt mehr Symbole im Vergleich zur Version glibc 2.25 (grep -Po 'IFUNC_IMPL \(i, name, \K[^,]+' sysdeps/x86_64/multiarch/ifunc-impl-list.c
):
memchr,memcmp,__memmove_chk,memmove,memrchr,__memset_chk,memset,rawmemchr,strlen,strnlen,stpncpy,stpcpy,strcasecmp,strcasecmp_l,strcat,strchr,strchrnul,strrchr,strcmp,strcpy,strcspn,strncasecmp,strncasecmp_l,strcpy,strcpy,strcpy,strcpy strpbrk,strspn,strstr,wcschr,wcsrchr,wcscpy,wcslen,wcsnlen,wmemchr,wmemcmp,wmemset,__memcpy_chk,memcpy,__mempcpy_chk,mempcpy,strncmp,__wmemset_chk,
Es sieht so aus, als gäbe es eine nette Problemumgehung dafür, die in neueren Versionen von glibc implementiert ist:eine "tunables"-Funktion, die die Auswahl optimierter String-Funktionen steuert. Einen allgemeinen Überblick über diese Funktion finden Sie hier und den relevanten Code innerhalb von glibc in ifunc-impl-list.c.
Hier ist, wie ich es herausgefunden habe. Zuerst nahm ich die Adresse, über die sich gdb beschwerte:
Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.
Ich habe es dann in der Tabelle der Shared Libraries nachgeschlagen:
(gdb) info shared
From To Syms Read Shared Object Library
0x00007ffff7fd3090 0x00007ffff7ff3130 Yes /lib64/ld-linux-x86-64.so.2
0x00007ffff76366b0 0x00007ffff766b52e Yes /usr/lib/x86_64-linux-gnu/libubsan.so.1
0x00007ffff746a320 0x00007ffff75d9cab Yes /lib/x86_64-linux-gnu/libc.so.6
...
Sie können sehen, dass diese Adresse innerhalb von glibc liegt. Aber welche Funktion konkret?
(gdb) disassemble 0x7ffff75c65d4
Dump of assembler code for function __strcmp_avx2:
0x00007ffff75c65d0 <+0>: mov %edi,%eax
0x00007ffff75c65d2 <+2>: xor %edx,%edx
=> 0x00007ffff75c65d4 <+4>: vpxor %ymm7,%ymm7,%ymm7
Ich kann in ifunc-impl-list.c nach dem Code suchen, der die Auswahl der avx2-Version steuert:
IFUNC_IMPL (i, name, strcmp,
IFUNC_IMPL_ADD (array, i, strcmp,
HAS_ARCH_FEATURE (AVX2_Usable),
__strcmp_avx2)
IFUNC_IMPL_ADD (array, i, strcmp, HAS_CPU_FEATURE (SSE4_2),
__strcmp_sse42)
IFUNC_IMPL_ADD (array, i, strcmp, HAS_CPU_FEATURE (SSSE3),
__strcmp_ssse3)
IFUNC_IMPL_ADD (array, i, strcmp, 1, __strcmp_sse2_unaligned)
IFUNC_IMPL_ADD (array, i, strcmp, 1, __strcmp_sse2))
Es sieht aus wie AVX2_Usable
ist die zu deaktivierende Funktion. Lassen Sie uns gdb entsprechend erneut ausführen:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable gdb...
Bei dieser Iteration hat es sich über __memmove_avx_unaligned_erms
beschwert , die offenbar durch AVX_Usable
aktiviert wurde - aber ich habe einen anderen Pfad in ifunc-memmove.h gefunden, der durch AVX_Fast_Unaligned_Load
aktiviert wurde . Zurück zum Reißbrett:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...
In dieser letzten Runde habe ich eine rdtscp
entdeckt Anweisung in der gemeinsam genutzten ASAN-Bibliothek, also habe ich ohne den Address Sanitizer neu kompiliert und endlich hat es funktioniert.
Zusammenfassend:Mit etwas Arbeit ist es möglich, diese Anweisungen von der Befehlszeile aus zu deaktivieren und die Aufzeichnungsfunktion von gdb ohne schwerwiegende Hacks zu verwenden.
Ich bin kürzlich auch auf dieses Problem gestoßen und habe es schließlich mit dynamischem CPUID-Fehler gelöst, um die Ausführung der CPUID-Anweisung zu unterbrechen und ihr Ergebnis zu überschreiben, wodurch vermieden wird, glibc oder den dynamischen Linker zu berühren. Dies erfordert Prozessorunterstützung für CPUID-Fehler (Ivy Bridge+) sowie Linux-Kernel-Unterstützung (4.12+), um es über ARCH_GET_CPUID
dem Userspace zugänglich zu machen und ARCH_SET_CPUID
Unterfunktionen von arch_prctl()
. Wenn diese Funktion aktiviert ist, wird ein SIGSEGV
Das Signal wird bei jeder Ausführung von CPUID geliefert, sodass ein Signalhandler die Ausführung der Anweisung emulieren und das Ergebnis überschreiben kann.
Die vollständige Lösung ist etwas umständlich, da ich auch den dynamischen Linker zwischenschalten muss, da die Hardwarefähigkeitserkennung ab glibc 2.26+ dorthin verschoben wurde. Ich habe die vollständige Lösung online unter https://github.com/ddcc/libcpuidoverride hochgeladen.