В търсене на LD_PRELOAD

Тази бележка беше написана през 2014 година, но точно тогава попаднах под репресии в Хабра и тя не видя света. През времето на забраната я забравих, а сега я намерих в черновиците. Помислих да я изтрия, но може би ще бъде полезна на някого.

В търсене на LD_PRELOAD

Съответно, малко петнично четиво за администратори относно търсенето на "включена" LD_PRELOAD.

1. Небольшо отклонение за тези, които не са запознати със заместването на функции

Останалите могат директно да преминат към п.2.

Да започнем с класическия пример:

#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);
  }
}

Компилираме без никакви флагове:

$ gcc ./ld_rand.c -o ld_rand

И, очаквано, получаваме 5 случайни числа по-малки от 100:

$ ./ld_rand
53
93
48
57
20

Но да предположим, че нямаме изходния код на програмата, а поведението трябва да бъде изменено.

Създаваме наша библиотека с наш собствен прототип на функция, например:

int rand(){
  return 42;
}

$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so

И сега нашият случаен избор е напълно предсказуем:

# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand
42
42
42
42
42

Трикът изглежда още по-впечатляващ, ако първо експортираме нашата библиотека чрез

$ export LD_PRELOAD=$PWD/ld_rand.so

или предварително я изпълним

# echo "$PWD/ld_rand.so" > /etc/ld.so.preload

и след това стартираме програмата в нормален режим. Ние не променихме нито ред в кода на самата програма, но нейното поведение вече зависи от малката функция в нашата библиотека. Освен това, по времето, когато се пише програмата, фалшивият rand дори не съществуваше.

Какво тогава накара програмата ни да използва фалшивия rand? Разберем по шагам.
Когато приложението се стартира, определени библиотеки се зареждат, които съдържат функции, необходими на програмата. Ние можем да ги видим, използвайки 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)

Този списък може да се различава в зависимост от версията на ОС, но там определено трябва да има файл libc.so. Именно тази библиотека осигурява системни повиквания и основни функции, като open, malloc, printf и т.н. Нашият rand също влиза в тях. Да се уверим в това:

# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$"
000000000003aef0 T rand

Да видим дали наборът от библиотеки ще се промени при използване на 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)

Оказва се, че установената променлива LD_PRELOAD кара да се зареди нашата ld_rand.so дори, въпреки че програмата сама не я изисква. И, тъй като нашата функция "rand" се зарежда преди rand от libc.so, то тя управлява ситуацията.

Добре, успяхме да заменим родната функция, но как да направим така, че и нейният функционал да запази и да добавим някои действия. Модифицираме нашия рандом:

#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();
}

Тук като наша "добавка" просто отпечатваме една строка текст, след което създаваме указател към оригиналната функция rand. За да получим адрес на тази функция, ще ни трябва dlsym — това е функция от библиотеката libdl, която ще намери нашия rand в стека на динамичните библиотеки. След което ще извикаме тази функция и ще върнем нейното значение. Съответно, ще трябва да добавим «-ldl» при компилирането:

$ gcc -ldl -shared -fPIC .\/o_rand_evil.c -o ld_rand_evil.so

$ LD_PRELOAD=$PWD\/ld_rand_evil.so .\/ld_rand
Evil injected code
66
Evil injected code
28
Evil injected code
93
Evil injected code
93
Evil injected code
95

И нашата програма използва «роден» rand, предварително изпълнявайки някои неподходящи действия.

2. Мъки на търсенето

Знайки за потенциалната заплаха, искаме да открием какво preload е било изпълнено. Ясно е, че най-добрият метод за детекция е да се вградим в ядрото, но мен интересуваха именно вариантите за определяне в потребителското пространство.

По-нататък ще представя решения за откритие и тяхното опровержение.

2.1. Да започнем с простото

Както беше споменато по-рано, да укажем зарежданата библиотека можем с помощта на променлива LD_PRELOAD или записвайки я в файла /etc/ld.so.preload. Ще създадем два най-прости детектора.

Първият — за проверка на зададената променливa в средата:

#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");
}

Вторият — за проверка на отварянето на файл:

#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");
}

Ще заредим библиотеките:

$ 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) [+]

Тук и навсякъде [+] показва успешното откритие.
Съответно, [-] означава заобикаляне на детекцията.

Насколько ефикасна такава детектора? Първо ще се занимаем с променливата в средата:

#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) [-]

Аналогично се отърваваме и от проверката 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) [-]

Да, тук могат да се използват и други начини за достъп до файл, като например, open64, stat и т.н., но по същество, за тяхното заблуждаване са необходими същите 5-10 реда код.

2.2. Продължаваме напред

По-горе използвахме getenv() за получаване на стойността LD_PRELOAD, но съществува и по-«нискостоящ» начин да достигнем до ENV-променливите. Няма да използваме междинни функции, а ще се обърнем към масива **environ, в който се съхранява копие на средата:

#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;
}

Тъй като тук четем данни директно от паметта, такова извикване не може да бъде прихванато, и нашият undetect_getenv вече не пречи да се определи инвазията.

$ LD_PRELOAD=.\/ld_undetect_getenv.so .\/detect_environ
LD_PRELOAD (**environ) [+]

На пръв поглед проблемът изглежда решен? Все още всичко едва започва.

След старта на програмата, стойността на променливата LD_PRELOAD в паметта вече не е необходима на хакерите, така че можем да я считаме и да я изтрием преди изпълнението на каквито и да било инструкции. Разбира се, да се манипулира масив в паметта, понеже е лоша практика в програмирането, но дали това може да спре някой, който не се интересува от нас?

За целта ще трябва да създадем собствена фалшива функция init(), в която ще прихванем зададеното LD_PRELOAD и да я предадем на нашия компилатор:

#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;
}

Изпълняваме, проверяваме:

$ 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/

Въпреки това, паметта не е последното място, където може да се открие подмяна LD_PRELOAD, има и /proc/. Нека започнем от очевидното /proc/{PID}/environ.

Всъщност съществува универсално решение за undetect’a **environ и /proc/self/environ. Проблемът е в "неправилното" поведение unsetenv(env).

правилният вариант

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);
        // Вместо unset просто нулираме нашата променлива
        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
$

Но да допуснем, че не го открихме и /proc/self/environ съдържа "проблемни" данни.

Първо ще опитаме с нашето предишно "маскиране":

$ (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 използва за отваряне на файла същия open(), затова решението е сходно с това, което вече бе направено в п.2.1, но сега създаваме временен файл, в който копираме стойностите от истинската памет без стрингове съдържащи 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);
}

И този етап е преминат:

$ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$

Следващото очевидно място е /proc/self/maps. Няма смисъл да се задържаме на него. Решението е абсолютно идентично на предишното: копираме данните от файла, като изключваме стринговете между libc.so и ld.so.

2.4. Вариант от Chokepoint

Това решение ми харесва особено много заради своята простота. Сравняваме адресите на функциите, зареждани директно от libc, и "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;
}

Зареждаме библиотеката с прихватяване на "open()" и проверяваме:

$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (syscall - open) [+]
Libc адрес: 0x7fa86893b160
Следующий адрес: 0x7fa868a26135

Опровержение се оказа още по-лесно:

#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. Системни повици

Казваше се, че това е всичко, но ще се задълбочим малко. Ако насочим системния повик директно към ядрото, това ще ни позволи да заобиколим целия процес на прихващане. Решението по-долу, разбира се, е зависимо от архитектурата (код.). Нека опитаме да реализираме засичането на отваряне на 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 (open syscall) [+]

И тази задачка има решение. Извадка от man‘а:

ptrace — това е средство, позволяващо на родителския процес да наблюдава и контролира протичането на друг процес, да преглежда и променя неговите данни и регистри. Обикновено тази функция се използва за създаване на точки на прекъсване в отстраняването на грешки и проследяване на системни повици.

Родителският процес може да започне трасиране, първо извиквайки функцията fork(2), а след това полученото дъщерно процесче може да изпълни PTRACE_TRACEME, следвано (обикновено) от изпълнението на exec(3). От друга страна, родителският процес може да започне отстраняване на съществуващ процес с помощта на PTRACE_ATTACH.

При трасировка дъщерният процес спира всеки път, когато получи сигнал, дори ако този сигнал е игнориран. (Изключение прави SIGKILL, който работи по обичайния начин.) Родителският процес ще бъде уведомлен за това при извикване на wait(2), след което може да прегледа и промени съдържанието на дъщерния процес преди неговото стартиране. След това родителският процес разрешава на дъщерния да продължи работа, в някои случаи игнорирайки изпратения му сигнал или изпращайки вместо това друг сигнал).

По този начин решението е да се следи процеса, спирайки го преди всеки системен повик и, при необходимост, пренасочвайки потока към функция на улов.

#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, &regs);
                        // 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, &regs);
                    }
                }
                ptrace(PTRACE_SYSCALL, program, 0, 0);
                waitpid(program, &status, 0);
            }
        }
        exit(0);
    }
    else {
        sleep(0);
    }
}

Проверяваме:

$ ./detect_syscall
LD_PRELOAD (open syscall) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (open syscall) [-]

+0-0=5

Огромно благодаря

Charles Hubain
Chokepoint
ValdikSS
Philippe Teuwen
derhass

, чиито статии, изходен код и коментари направиха много повече от мен, за да се появи тази бележка тук.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster