กำลังค้นหา LD_PRELOAD

บันทึกนี้เขียนขึ้นในปี 2014 แต่ฉันเพิ่งถูกกดขี่ Habré และไม่เห็นแสงแห่งวัน ในระหว่างการแบนฉันลืมมันไป แต่ตอนนี้ฉันพบมันในร่างแล้ว ฉันคิดที่จะลบมัน แต่อาจจะเป็นประโยชน์กับใครบางคน

กำลังค้นหา 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

แต่ลองจินตนาการว่าเราไม่มีซอร์สโค้ดของโปรแกรม แต่เราจำเป็นต้องเปลี่ยนพฤติกรรม

มาสร้าง Library ของเราเองด้วย Function Prototype ของเราเองกัน เช่น:

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

แล้วจึงรันโปรแกรมตามปกติ เราไม่ได้เปลี่ยนแปลงโค้ดแม้แต่บรรทัดเดียวในโปรแกรม แต่พฤติกรรมของมันตอนนี้ขึ้นอยู่กับฟังก์ชันเล็กๆ ในไลบรารีของเรา นอกจากนี้ในขณะที่เขียนโปรแกรมนั้น ทั้งหมด ไม่มีอยู่ด้วยซ้ำ

อะไรทำให้โปรแกรมของเราใช้ของปลอม ทั้งหมด? ลองทำทีละขั้นตอน
เมื่อแอปพลิเคชันเริ่มทำงาน ไลบรารีบางตัวจะถูกโหลดซึ่งมีฟังก์ชันที่โปรแกรมต้องการ เราสามารถดูได้โดยใช้ แอลดีดี:

# 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. เป็นไลบรารีที่ให้การเรียกระบบและฟังก์ชันพื้นฐานเช่น เปิด, มัลลอค, 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();
}

ในฐานะ "ส่วนเสริม" ของเรา เราจะพิมพ์ข้อความเพียงบรรทัดเดียว หลังจากนั้นเราจะสร้างตัวชี้ไปยังฟังก์ชันดั้งเดิม ทั้งหมด. เพื่อให้ได้ที่อยู่ของฟังก์ชันนี้เราต้องการ ดีแอลซิม เป็นฟังก์ชันจากห้องสมุด libdlซึ่งจะพบของเรา ทั้งหมด ในสแต็กของไดนามิกไลบรารี หลังจากนั้นเราจะเรียกใช้ฟังก์ชันนี้และส่งกลับค่าของมัน ดังนั้นเราจะต้องเพิ่ม "-แอลดีแอล" ระหว่างการประกอบ:

$ 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. ความเจ็บปวดจากการค้นหา

เมื่อทราบถึงภัยคุกคามที่อาจเกิดขึ้น เราต้องการตรวจจับสิ่งนั้น Preload มันถูกดำเนินการ เห็นได้ชัดว่าวิธีที่ดีที่สุดในการตรวจจับคือการดันมันเข้าไปในเคอร์เนล แต่ฉันสนใจตัวเลือกการตรวจจับในพื้นที่ผู้ใช้

ต่อไป วิธีแก้ปัญหาสำหรับการตรวจจับและการหักล้างจะมาเป็นคู่กัน

2.1. มาเริ่มกันง่ายๆ

ตามที่กล่าวไว้ก่อนหน้านี้ คุณสามารถระบุไลบรารีที่จะโหลดโดยใช้ตัวแปรได้ ld_preload หรือเขียนเป็นไฟล์ /etc/ld.so.โหลดล่วงหน้า. มาสร้างตัวตรวจจับแบบง่ายสองตัวกัน

สิ่งแรกคือการตรวจสอบตัวแปรสภาพแวดล้อมที่ตั้งไว้:

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

ในทำนองเดียวกันเรากำจัดเช็ค เปิด:

#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. เดินหน้าต่อไป

ข้างบนเราใช้ รับเท็นวี() เพื่อให้ได้คุณค่า 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/ตนเอง/

อย่างไรก็ตาม ความทรงจำไม่ใช่สถานที่สุดท้ายที่คุณสามารถหาสิ่งทดแทนได้ ld_preload, นอกจากนี้ยังมี /โปรค/. เริ่มจากสิ่งที่ชัดเจนกันก่อน /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

แมว ใช้วิธีเดียวกันในการเปิดไฟล์ เปิด()ดังนั้นวิธีแก้ปัญหาจะคล้ายกับสิ่งที่ทำไปแล้วในส่วน 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และที่อยู่ “ถัดไป”

#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.โหลดล่วงหน้า.

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

และปัญหานี้ก็มีทางแก้ไข ตัดตอนมาจาก มนุษย์'ก:

ptrace เป็นเครื่องมือที่ช่วยให้กระบวนการหลักสังเกตและควบคุมความคืบหน้าของกระบวนการอื่น ดูและเปลี่ยนแปลงข้อมูลและลงทะเบียน โดยทั่วไปแล้ว ฟังก์ชันนี้ใช้เพื่อสร้างจุดพักในโปรแกรมดีบักและติดตามการเรียกของระบบ

กระบวนการหลักสามารถเริ่มต้นการติดตามได้โดยการเรียก fork(2) ก่อน จากนั้นกระบวนการลูกที่เป็นผลลัพธ์สามารถดำเนินการ PTRACE_TRACEME ได้ ตามด้วย (ปกติ) โดยการเรียกใช้งาน exec(3) ในทางกลับกัน กระบวนการหลักสามารถเริ่มการดีบักกระบวนการที่มีอยู่ได้โดยใช้ PTRACE_ATTACH

เมื่อติดตาม กระบวนการลูกจะหยุดทุกครั้งที่รับสัญญาณ แม้ว่าสัญญาณจะถูกละเว้นก็ตาม (ข้อยกเว้นคือ SIGKILL ซึ่งทำงานได้ตามปกติ) กระบวนการหลักจะได้รับแจ้งเรื่องนี้โดยการเรียก wait(2) หลังจากนั้นจะสามารถดูและแก้ไขเนื้อหาของกระบวนการลูกก่อนที่จะเริ่มได้ จากนั้นกระบวนการหลักจะอนุญาตให้ลูกทำงานต่อไป ในบางกรณี โดยไม่สนใจสัญญาณที่ส่งไปหรือส่งสัญญาณอื่นแทน)

ดังนั้น วิธีแก้ไขคือตรวจสอบกระบวนการ หยุดก่อนการเรียกระบบแต่ละครั้ง และหากจำเป็น ให้เปลี่ยนเส้นทางเธรดไปที่ฟังก์ชัน hook

#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

ขอบคุณมาก

ชาร์ลส์ ฮูเบน
โชคพอยท์
วาลดิกSS
ฟิลิปเป้ ทอยเวน
เดอร์ฮาส

ซึ่งบทความ แหล่งที่มา และความคิดเห็นของเขาได้ทำอะไรมากกว่าฉันในการทำให้บันทึกย่อนี้ปรากฏที่นี่

ที่มา: will.com

เพิ่มความคิดเห็น