Zdá se, že neexistuje přímočará runtime metoda pro opravu detekce funkcí. K této detekci dochází poměrně brzy v dynamickém linkeru (ld.so).
Binární záplatování linkeru se v tuto chvíli zdá nejjednodušší metodou. @osgx popsal jednu metodu, kdy se skok přepíše. Dalším přístupem je pouze falešný výsledek cpuid. Obvykle cpuid(eax=0)
vrátí nejvyšší podporovanou funkci v eax
zatímco ID výrobce se vrací v registrech ebx, ecx a edx. Tento fragment máme v 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);
}
__cpuid
řádek se převádí na tyto instrukce v /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
Takže spíše než záplatování větví bychom mohli také změnit cpuid
do nop
instrukce, která by vedla k vyvolání posledního else
větev (protože registry nebudou obsahovat "GenuineIntel"). Od počátku eax=0
, cpu_features->max_cpuid
bude také 0 a if (cpu_features->max_cpuid >= 7)
bude také vynechán.
Binární záplatování cpuid(eax=0)
podle nop
to lze provést pomocí tohoto nástroje (funguje pro x86 i 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)
Ekvivalentní varianta jazyka Perl, -0777
zajišťuje, že soubor je přečten najednou namísto oddělování záznamů na řádku:
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
To byla ta snadná část. Nyní jsem nechtěl nahradit systémový dynamický linker, ale spustit pouze jeden konkrétní program s tímto linkerem. Jistě, to lze provést pomocí ./ld-linux-x86-64-patched.so.2 ./a
, ale naivním vyvoláním gdb se nepodařilo nastavit zarážky:
$ 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
Ruční řešení je popsáno v Jak ladit program pomocí vlastního elf interpretu? Funguje to, ale bohužel se jedná o ruční zásah pomocí add-symbol-file
. Mělo by být možné to trochu automatizovat pomocí GDB Catchpoints.
Alternativní přístup, který nepoužívá binární propojení, je LD_PRELOAD
vytvoření knihovny, která definuje vlastní rutiny pro memcpy
, memove
, atd. To pak bude mít přednost před rutinami glibc. Úplný seznam funkcí je k dispozici v sysdeps/x86_64/multiarch/ifunc-impl-list.c
. Současná HEAD má více symbolů ve srovnání s vydáním glibc 2.25 celkem (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,strcmpcase,strppp strpbrk,strspn,strstr,wcschr,wcsrchr,wcscpy,wcslen,wcsnlen,wmemchr,wmemcmp,wmemset,__memcpy_chk,memcpy,__mempcpy_chk,mempcpy,strncmp,__wmemset_chk,
Zdá se, že v posledních verzích glibc je pro to implementováno pěkné řešení:funkce „laditelných funkcí“, která vede výběr optimalizovaných funkcí řetězců. Obecný přehled této funkce naleznete zde a příslušný kód v glibc na ifunc-impl-list.c.
Tady je návod, jak jsem na to přišel. Nejprve jsem vzal adresu, na kterou si stěžoval gdb:
Process record does not support instruction 0xc5 at address 0x7ffff75c65d4.
Pak jsem to vyhledal v tabulce sdílených knihoven:
(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 ...
Můžete vidět, že tato adresa je v glibc. Ale jakou funkci konkrétně?
(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
Mohu se podívat na ifunc-impl-list.c a najít kód, který řídí výběr verze avx2:
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))
Vypadá to jako
AVX2_Usable
je funkce k deaktivaci. Spusťte gdb podle toho znovu:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable gdb...
V této iteraci si stěžoval na
__memmove_avx_unaligned_erms
, která se zdá být povolenaAVX_Usable
- ale našel jsem jinou cestu v ifunc-memmove.h povolenou pomocíAVX_Fast_Unaligned_Load
. Zpět na rýsovací prkno:
GLIBC_TUNABLES=glibc.cpu.hwcaps=-AVX2_Usable,-AVX_Fast_Unaligned_Load gdb ...
V tomto posledním kole jsem objevil
rdtscp
instrukce ve sdílené knihovně ASAN, takže jsem překompiloval bez dezinfekce adres a nakonec to fungovalo.Shrnuto:s trochou práce je možné tyto instrukce zakázat z příkazového řádku a používat funkci záznamu gdb bez závažných hacků.
Nedávno jsem se s tímto problémem také setkal a nakonec jsem jej vyřešil pomocí dynamického chybování CPUID k přerušení provádění instrukce CPUID a přepsání jejího výsledku, což zabraňuje dotyku glibc nebo dynamického linkeru. To vyžaduje podporu procesoru pro chyby CPUID (Ivy Bridge+) a také podporu jádra Linuxu (4.12+) pro jeho vystavení uživatelskému prostoru prostřednictvím
ARCH_GET_CPUID
aARCH_SET_CPUID
podfunkcearch_prctl()
. Když je tato funkce povolena, zobrazí seSIGSEGV
signál bude doručen při každém provedení CPUID, což umožňuje obsluze signálu emulovat provedení instrukce a přepsat výsledek.Úplné řešení je trochu zapojeno, protože také musím vložit dynamický linker, protože tam byla přesunuta detekce hardwarových schopností počínaje glibc 2.26+. Úplné řešení jsem nahrál online na https://github.com/ddcc/libcpuidoverride.
Linux