Tato poznámka byla napsána v roce 2014, ale já jsem byl právě pod represí na Habru a tato poznámka se nedostala na světlo světa. Během doby zákazu jsem na ni zapomněl, a nyní jsem ji našel v návrzích. Chtěl jsem ji smazat, ale možná se někomu hodí.

Celkově jde o malé páteční adminské čtení na téma hledání "aktivovaného". LD_PRELOAD.
1. Malé odbočení pro ty, kteří nejsou obeznámeni se substitucí funkcí.
Ostatní mohou přejít rovnou k čl. 2.
Začneme klasickým příkladem:
#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);
}
}
Kompilujeme bez jakýchkoli příznaků:
$ gcc ./ld_rand.c -o ld_rand
A, jak se očekávalo, získáme 5 náhodných čísel menších než 100:
$ ./ld_rand
53
93
48
57
20
Ale představme si, že nemáme zdrojový kód programu a nutno změnit chování.
Vytvoříme si vlastní knihovnu s naším prototypem funkce, například:
int rand(){
return 42;
}
$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so
A nyní je náš náhodný výběr zcela předvídatelný:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand
42
42
42
42
42
Tenhle trik vypadá ještě působivěji, pokud nejprve exportujeme naši knihovnu přes
$ export LD_PRELOAD=$PWD/ld_rand.so
nebo ji nejprve zavoláme
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
a poté spustíme program normálně. Nezměnili jsme ani řádek v kódu samotného programu, ale jeho chování nyní závisí na kousíčku funkce v naší knihovně. Navíc, v době psaní programu podvodný rand ani neexistoval.
Co tedy způsobilo, že náš program použil falešný rand? Разберем по шагам.
Když se aplikace spouští, nahrávají se určité knihovny, které obsahují pro program potřebné funkce. Můžeme je zobrazit pomocí ) a tommath (:
# 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)
Tento seznam se může lišit v závislosti na verzi OS, ale soubor libc.so. Tento soubor je zodpovědný za systémové volání a základní funkce, jako jsou open, malloc, printf atd. Naše rand je také jednou z nich. Ujistíme se o tom:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$"
000000000003aef0 T rand
Podíváme se, jestli se nezmění sada knihoven při použití 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)
Ostává, že nastavená proměnná LD_PRELOAD způsobuje, že se načte naše ld_rand.so i přesto, že program ji sám nevyžaduje. A protože se naše funkce "rand" načítá dříve než rand od libc.so, určuje podmínky.
Dobře, podařilo se nám nahradit původní funkci, ale jak udělat, aby zůstala její funkčnost a zároveň se přidaly další akce. Modifikujeme náš 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();
}
Zde jako naši "přísadu" jen vytiskneme jeden textový řetězec, po kterém vytvoříme ukazatel na původní funkci. randPro získání adresy této funkce budeme potřebovat dlsym – to je funkce z knihovny libdl, která najde náš rand ve stohu dynamických knihoven. Poté tuto funkci zavoláme a vrátíme její hodnotu. Odpovídajícím způsobem budeme potřebovat přidat "-ldl" při kompilaci:
$ gcc -ldl -shared -fPIC ./o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD/ld_rand_evil.so ./ld_rand
Vložený kód
66
Vložený kód
28
Vložený kód
93
Vložený kód
93
Vložený kód
95
A náš program používá "nativní" rand, po předchozím provedení některých nežádoucích akcí.
2. Mučení při hledání
Vědoma si potenciální hrozby, chceme zjistit, co preload bylo vykonáno. Je jasné, že nejlepší způsob detekce je zahrnout to do jádra, ale zajímaly mě konkrétně možnosti určení v uživatelském prostoru.
Dále následují řešení pro detekci a jejich vyvrácení.
2.1. Začneme s jednoduchým
Jak již bylo zmíněno, načítanou knihovnu lze specifikovat pomocí proměnné LD_PRELOAD nebo ji zapsat do souboru /etc/ld.so.preload. Vytvoříme dva jednoduché detektory.
První – pro kontrolu nastavené proměnné prostředí:
#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");
}
Druhý – pro kontrolu otevření souboru:
#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");
}
Načteme knihovny:
$ 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) [+]
Zde a dále [+] znamená úspěšnou detekci.
Odpovídajícím způsobem [-] znamená obejití detekce.
Jak účinný je takový detektor? Nejprve se pustíme do proměnné prostředí:
#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) [-]
Podobně se zbavujeme kontroly 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) [-]
Ano, zde mohou být použity jiné způsoby přístupu k souboru, jako například open64, stat atd., ale v podstatě ke klamání potřebujeme stejné 5-10 řádků kódu.
2.2. Pokračujeme dále
Výše jsme použili getenv() pro získání hodnoty LD_PRELOAD, ale existuje i "nízkoúrovňový" způsob, jak se dostat k ENV-proměnným. Nebudeme používat mezifunkce, ale obrátíme se na pole **environ, ve kterém je uložena kopie prostředí:
#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;
}
Protože zde čteme data přímo z paměti, nelze takový zásah zachytit a náš undetect_getenv už nebrání identifikaci vniknutí.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ
LD_PRELOAD (**environ) [+]
Na první pohled by byl problém vyřešen? Všechno teprve začíná.
Jakmile je program spuštěn, hodnota proměnné LD_PRELOAD v paměti již není potřebná hackerům, takže ji lze považovat za zbytečnou a odstranit ji před vykonáním jakýchkoliv instrukcí. Samozřejmě, upravovat pole v paměti je minimálně špatný styl programování, ale může to zastavit toho, kdo nám očividně nepřeje nic dobrého?
K tomu potřebujeme vytvořit naši falešnou funkci init(), ve které zachytíme nastavenou LD_PRELOAD a předáme ji našemu linkeru:
#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;
}
Provádíme, kontrolujeme:
$ 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/
Nicméně, paměť není posledním místem, kde lze zjistit podvod LD_PRELOAD, existuje ještě /proc/. Začneme s očividným /proc/{PID}/environ.
Ve skutečnosti existuje univerzální řešení pro undetect **environ a /proc/self/environ. Problém spočívá v "nesprávném" chování unsetenv(env).
správná varianta
void evil_init()
{
// Nejprve uložíme aktuální hodnotu 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) {
// Zbavíme se aktuálního LD_PRELOAD
//unsetenv(env);
// Místo unset prostě nulujeme naši proměnnou
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
$
Ale představme si, že jsme ho nenašli a /proc/self/environ obsahuje „problematická“ data.
Nejprve se pokusíme s naší předchozí „maskovací“ metodou:
$ (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 používá pro otevírání souborů stále stejný open(), takže řešení je podobné tomu, co už bylo provedeno v bodě 2.1, ale nyní vytváříme dočasný soubor, do kterého kopírujeme hodnoty z skutečné paměti bez řetězců obsahujících 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);
}
A tento krok byl dokončen:
$ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$
Dalším očividným místem je /proc/self/maps. Není třeba se na něm zdržovat. Řešení je naprosto identické s předchozím: kopírujeme data z souboru s vyloučením řetězců mezi libc.so a ld.so.
2.4. Varianta od Chokepoint
Toto řešení mě obzvlášť zaujalo svou jednoduchostí. Porovnáme adresy funkcí, které jsou načítány přímo z libc, a „NEXT“ adresy.
#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;
}
Načítáme knihovnu s přesměrováním "open()" a kontrolujeme:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (syscall - open) [+]
Libc address: 0x7fa86893b160
Next address: 0x7fa868a26135
Odmítnutí bylo ještě snazší:
#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. Syscally
Zřejmě to tím končí, ale pojďme se ještě trochu pohrabat. Pokud nasměrujeme systémový hovor přímo na jádro, můžeme tím obejít celý proces zachycování. Řešení níže je samozřejmě závislé na architektuře (x86_64). Zkusíme implementovat pro detekci otevření 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) [+]
A tento úkol má řešení. Vytržení z man‘a:
ptrace — to je nástroj, který umožňuje rodičovskému procesu sledovat a kontrolovat průběh jiného procesu, prohlížet a měnit jeho data a registry. Obvykle se tato funkce používá k vytváření bodů přerušení v laděním a sledování systémových hovorů.
Rodičovský proces může začít trasování nejprve vyvoláním funkce fork(2), a poté nově vytvořený dceřiný proces provede PTRACE_TRACEME, po kterém (obvykle) následuje vykonání exec(3). Na druhou stranu, rodičovský proces může začít ladit existující proces pomocí PTRACE_ATTACH.
Při trasování se dceřiný proces zastaví pokaždé, když obdrží signál, i když je tento signál ignorován. (Výjimkou je SIGKILL, který funguje obvyklým způsobem.) Rodičovský proces bude o tomto informován při volání wait(2), po kterém může prohlížet a měnit obsah dceřiného procesu před jeho pokračováním. Poté rodičovský proces povolí dceřinému procesu pokračovat v práci, v některých případech ignorujíc přijímaný signál nebo místo toho posílajíc jiný signál).
Tímto způsobem spočívá řešení ve sledování procesu, zastavováním ho před každým systémovým voláním a v případě potřeby přesměrováním toku do funkce ukazatele.
#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);
}
}
Kontrolujeme:
$ ./detect_syscall
LD_PRELOAD (open syscall) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (open syscall) [-]
+0-0=5
Obrovské díky
, jejichž články, zdrojové kódy a komentáře udělaly mnohem víc, než já, aby se tato poznámka objevila zde.
Zdroj: habr.com
