Ne bazăm mult pe librării produse de terțe părți pentru a ne dezvolta aplicațiile. În general, avem încredere în autorii acestor librării, dar există mai multe elemente interconectate care sunt implicate în producerea unui executabil decât codul scris de noi sau autorii librăriilor. În ce măsură putem fi siguri că aplicațiile pe care le distribuim nu conțin software malițios?
Au fost foarte multe știrile despre unul dintre cele mai sofisticate atacuri cibernetice din anul 2020, atacul SolarWinds. Acest tip de atac e cunoscut sub denumirea de supply chain attack. Pe scurt, hackerii au atacat un furnizor terță parte, ceea ce le-a permis să obțină acces la un server de build și apoi să injecteze cod malițios. Acest cod a ajuns să fie distribuit pe alte servere, rezultând într-o înșiruire sofisticată de atacuri.
Atacul a fost unul foarte complex, dar faptul că un cod malițios a ajuns într-o aplicație fără a fi observat ne-a amintit de un articol scris în 1984, numit Trusting Trust de Ken Thompson. Acesta ne descrie cât de greu, dacă nu chiar imposibil, este să identificăm un cod malițios. De exemplu, el ne aduce la cunoștință existența unei tehnici prin care un compilator poate fi "educat" să injecteze cod malițios în orice executabil pe care îl produce. Un lucru notabil este că această tehnică nu va lăsa nici o urmă, nici măcar în codul sursă al compilatorului.
Propunem o ipoteză. Dacă un dezvoltator de soft ar fi, teoretic, în cea mai bună poziție pentru a afirma care poate fi adevărata intenție a execuției softului pe care-l dezvoltă, atunci cât de plauzibilă este existența unui instrument care să evalueze efectele secundare ale aplicației, inclusiv ale librăriilor care sunt împachetate în binarele produse?
Să încercăm să ne imaginăm că am avea un instrument ce ne permite să specificăm strict ce interacțiuni externe procesului pot avea loc când aplicația va rula. Dacă avem un executabil de tip linie de comandă care efectuează strict operații locale cu sistemul de fișiere, atunci nu ne-am aștepta să efectueze apeluri de rețea.
Am efectuat câteva experimente în Linux pornind de la ideea că analiza interacțiunilor prin interfața kernelului syscalls ar putea fi de ajutor în evaluarea oricăror interacțiuni externe ale aplicației.
Pentru acest experiment vom folosi o aplicație simplă de tip Hello World scrisă în C:
// simple_hello.c
#include
int main() {
printf("Hello, World!\n");
return 0;
}
Am folosit o mașină virtuală în VirtualBox, pe care am instalat Ubuntu 22.04, și am compilat această aplicație cu GCC versiunea 11.3.0.
Ca să menținem lucrurile simple, am compilat în modul debug:
$ gcc simple_hello.c
-o simple_hello.out
Putem formula niște ipoteze despre acest executabil: - Trebuie să afișeze mesajul "Hello, World!" în consolă - Nu ar trebui să acceseze rețeaua.
Să presupunem că am putea crea un instrument de analiză statică pentru a urmări apelurile către syscalls.
În experimentul nostru, am folosit un instrument open-source pentru a decompila fișierele binare în reprezentarea intermediară LLVM. În termeni LLVM această operațiune se numește lifting. Instrumentul folosit este Retdec.
Precizăm că executabilul nostru depinde și de alte librării, așa că trebuie să le decompilăm pe fiecare dintre acestea. De obicei, am folosi ldd
pentru această acțiune:
$ ldd simple_hello.out
linux-vdso.so.1 (0x00007ffcda3d1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
(0x00007f812ee00000)
/lib64/ld-linux-x86-64.so.2 (0x00007f812f1cd000)
linux-vdso.so.1
este doar un artefact Linux, pe care îl putem ignora. Pentru un rezultat mai clar, noi am folosit lddtree:
/lib64/ld-linux-x86-64.so.2 =>
/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6
ld-linux-x86-64.so.2 => /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
În plus, acest program poate fi folosit și ca librărie. În prototipul nostru am folosit și un pachet llvm-ir-analysis, care ne-a oferit în mod convenabil câteva implementări, cum ar fi crearea unui call-graph, inclusiv între module diferite, derivate din reprezentarea intermediară LLVM. De aici, am urmat funcțiile apelabile din main
, și am traversat graful până la syscalls (lifterul a generat convenabil pentru noi o funcție numită __asm_syscall
). Experimentul a arătat cel puțin 52 de căi către syscalls pentru o aplicație simplă "Hello World!". Ieșirea este prea lungă pentru a o lista aici, dar în libc se pare că a existat o posibilă cale de apel către aproape orice syscall.
Apoi am fost descurajați în a mai continua explorarea analizei statice când am aflat că unii autori ar putea alege să-și cripteze codul într-un executabil, astfel încât codul ce urmează a fi executat să devină disponibil doar în timpul rulării programului. Un exemplu de utilitar ce facilitează astfel de împachetări fiind ELFCrypt. Această tehnică este, în mod specific, o măsură împotriva analizei statice. Cartea Linux Binary Analysis descrie mai detaliat această tehnică.
Să încercăm un strace
în exemplul nostru (unele rânduri au fost eliminate pentru concizie):
$ strace ./simple_hello.out
execve("./simple_hello.out", ["./simple_hello.out"], 0x7ffc041e1eb0 /* 64 vars */) = 0
brk(NULL) = 0x55c64d28d000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffccdef7a40) = -1 EINVAL (Invalid argument)
...
getrandom("\x9e\x2e\xe1\x8e\xed\x27\x1f\x1d", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0x55c64d28d000
brk(0x55c64d2ae000) = 0x55c64d2ae000
write(1, "Hello, World!\n", 14Hello, World!
) = 14 exit_group(0)
Ultimul apel arată că, într-adevăr, cel puțin prima ipoteză e validată și mesajul este afișat în consolă:
write(1, "Hello, World!\n", 14Hello, World!
) = 14
Similar tehnicilor pentru protejarea codului împotriva analizei statice, au fost cercetate și tehnici împotriva analizei dinamice.
Revenim la cartea Linux Binary Analysis, pentru selecta o listă a acestor tehnici: 1. Protecție anti-debug, prin utilizarea ptrace
cu PTRACE_TRACEME
pentru a preveni atașarea oricărui debugger la proces 2. Mecanisme de protecție anti-emulare precum: - Detectarea emulării prin testarea syscalls: Se bazează pe faptul că unele emulatoare la nivel de aplicație ar putea să nu implementeze toată lista de syscalls. - Detectarea inconsecvențelor emulatoarelor de procesoare: Deoarece este aproape imposibil ca un procesor să fie emulat perfect, un program poate detecta că rulează în interiorul unui emulator prin sondarea unor neconcordanțe între modul în care un procesor real și un procesor emulat se comportă atunci când rulează anumite instrucțiuni. - Verificarea diferențelor de timp de execuție ale unor instrucțiuni: În general, un procesor real execută instrucțiuni mai rapid decât un emulator, observație care poate fi folosită ca modalitate de detectare a emulării.
Lista de mai sus nu este exhaustivă, dar poate arăta cât de creativ poate fi procesul de a ascunde adevăratele intenții în interiorul unui cod executabil.
Am vrut să încerc instrumentul maya de protecție al binarelor, scris de autorul cărții Linux Binary Analysis, dar am avut dificultăți în compilarea lui. Așa că, în schimb, am folosit altul, numit kiteshield:
$ kiteshield simple_hello.out simple_hello.ks
Menționăm ca noul executabil rezultat este legat static, în timp ce primul este legat dinamic, de unde pot rezulta diferențe de syscalls.
La o rulare strace
pe binarul nou obținem:
$ strace ./simple_hello.ks
execve("./simple_hello.ks", ["./simple_hello.ks"], 0x7ffcc2145870 /* 64 vars */) = 0
getpid() = 27623
stat("/proc/27623/status", {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
open("/proc/27623/status", O_RDONLY) = 3
read(3, "Name:\tsimple_hello.ks\nUmask:\t000"..., 4095) = 1438
close(3) = 0
exit(0) = ?
+++ exited with 0 +++
Putem observa că lipsește orice asemănare cu:
write(1, "Hello, World!n"
Interacțiunea intenționată cu mediul extern al aplicației noastre simple, nu lăsase nicio urmă.
Putem observa un lucru: chiar și prin monitorizarea în timpul rulării, poate fi dificil să identificăm un cod malițios care a fost proiectat cu acest scop.
Când am început această cercetare, credeam că analiza statică ar putea fi utilă pentru multe aplicații practice. Ne-am gândit că analiza syscalls ar putea fi o opțiune rezonabilă, acoperind atât cazul educării compilatoarelor cât și cel de obfuscation. Dar am aflat că există moduri mult mai ingenioase pentru a face aproape imposibilă ingineria inversă a unui executabil.
Desigur, exemplele noastre au fost în jurul unui executabil simplu, folosind instrumente open-source, însă tehnici similare în mod clar pot fi aplicate și la librăriile dinamice.
După aceste observații, credem că avem încă un motiv pentru care ar trebui să fim mai atenți la ce și câte librării externe folosim, mai ales când sunt distribuite în formă binară.
Cât de sigure sunt aplicațiile noastre? Aplicațiile noastre sunt cel mult atât de sigure pe cât de atent este dezvoltatorul de soft cu resursele pe care le utilizează.
de Péter Török