Ответ 1
Ниже приведен пример того, что, по моему мнению, классифицируется как метаморфический код, написанный на C. Я боюсь, что у меня нет большого опыта написания переносимого кода C, поэтому для компиляции на других платформах может потребоваться некоторая модификация (Я использую старую версию Borland в Windows). Кроме того, он полагается на целевую платформу, являющуюся x86, поскольку она связана с генерированием машинного кода. Теоретически он должен компилироваться на любой ОС x86.
Как это работает
Каждый раз, когда программа запускается, она генерирует произвольно измененную копию самого себя с другим именем файла. Он также распечатывает список смещений, которые были изменены, поэтому вы можете видеть, что он действительно что-то делает.
Процесс модификации очень упрощен. Исходный код просто чередуется с последовательностями инструкций сборки, которые фактически ничего не делают. Когда программа запускается, она находит эти последовательности и случайным образом заменяет их на другой код (что явно также ничего не делает).
Hardcoding список смещений явно не реалистичен для чего-то, что другие люди должны иметь возможность компилировать, поэтому последовательности генерируются таким образом, чтобы их было легко идентифицировать при поиске через объектный код, надеюсь, без соответствия любые ложные срабатывания.
Каждая последовательность начинается с операции push в определенном регистре, набора инструкций, которые изменяют этот регистр, а затем поп-операции для восстановления регистра до его начального значения. Чтобы все было просто, в исходном источнике все последовательности - это просто PUSH EAX, восемь NOP и POP EAX. Тем не менее, во всех последующих поколениях приложения последовательности будут полностью случайными.
Объяснение кода
Я разделил код на несколько частей, чтобы попытаться объяснить его шаг за шагом. Если вы хотите скомпилировать его самостоятельно, вам просто нужно объединить все части.
В первый довольно стандартный стандарт входит:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
Далее мы определяем для различных кодов операций x86. Обычно они объединяются с другими значениями для генерации полной инструкции. Например, PUSH определяет (0x50) сам по себе PUSH EAX, но вы можете получить значения для других регистров, добавив смещение в диапазоне от 0 до 7. То же самое для POP и MOV.
#define PUSH 0x50
#define POP 0x58
#define MOV 0xB8
#define NOP 0x90
Последние шесть представляют собой префиксные байты нескольких двухбайтовых кодов операций. Второй байт кодирует операнды и будет объяснен более подробно позже.
#define ADD 0x01
#define AND 0x21
#define XOR 0x31
#define OR 0x09
#define SBB 0x19
#define SUB 0x29
const unsigned char prefixes[] = { ADD,AND,XOR,OR,SBB,SUB,0 };
JUNK - это макрос, который вставляет нашу последовательность мусорных операций в любом месте в коде. Как я уже объяснял ранее, изначально он просто писал PUSH EAX, NOP и POP EAX. JUNKLEN - количество NOP в этой последовательности, а не полная длина последовательности.
И если вы не знаете, __emit__
- это псевдофункция, которая вводит литеральные значения непосредственно в код объекта. Я подозреваю, что вам может понадобиться порт, если вы используете другой компилятор.
#define JUNK __emit__(PUSH,NOP,NOP,NOP,NOP,NOP,NOP,NOP,NOP,POP)
#define JUNKLEN 8
Некоторые глобальные переменные, в которых будет загружен наш код. Глобальные переменные являются плохими, но я не очень хороший кодер.
unsigned char *code;
int codelen;
Далее у нас есть простая функция, которая будет читать наш объектный код в памяти. Я никогда не освобождаю эту память, потому что мне просто все равно.
Обратите внимание на вызовы макроса JUNK, вставленные в случайные точки. Вы увидите намного больше этого кода. Вы можете вставлять их почти в любом месте, но если вы используете настоящий компилятор C (в отличие от С++), он будет жаловаться, если вы попытаетесь поместить их перед объявлениями переменных или в промежутках.
void readcode(const char *filename) {
FILE *fp = fopen(filename, "rb"); JUNK;
fseek(fp, 0L, SEEK_END); JUNK;
codelen = ftell(fp);
code = malloc(codelen); JUNK;
fseek(fp, 0L, SEEK_SET);
fread(code, codelen, 1, fp); JUNK;
}
Еще одна простая функция для записи приложения после его изменения. Для нового имени файла мы просто заменяем последний символ исходного имени файла цифрой, которая увеличивается каждый раз. Не пытайтесь проверить, существует ли файл и что мы не перезаписываем критическую часть операционной системы.
void writecode(const char *filename) {
FILE *fp;
int lastoffset = strlen(filename)-1;
char lastchar = filename[lastoffset];
char *newfilename = strdup(filename); JUNK;
lastchar = '0'+(isdigit(lastchar)?(lastchar-'0'+1)%10:0);
newfilename[lastoffset] = lastchar;
fp = fopen(newfilename, "wb"); JUNK;
fwrite(code, codelen, 1, fp); JUNK;
fclose(fp);
free(newfilename);
}
Эта следующая функция выписывает случайную инструкцию для нашей мусорной последовательности. Параметр reg представляет собой регистр, с которым мы работаем, - то, что будет нажато и вытолкнуто с обоих концов последовательности. Смещение - это смещение в коде, в котором будет записана инструкция. И пространство дает количество оставшихся байтов в нашей последовательности.
В зависимости от того, сколько места у нас есть, мы можем быть ограничены инструкциями, которые мы можем выписать, иначе мы произвольно выбираем, является ли это NOP, MOV или одним из других. NOP - это всего лишь один байт. MOV - пять байтов: наш код MOV (с добавленным параметром reg) и 4 случайных байта, представляющих число, перемещенное в регистр.
Для двух байтовых последовательностей первое является одним из наших префиксов, выбранных случайным образом. Второй - это байт в диапазоне от 0xC0 до 0xFF, где наименее значащие 3 бита представляют собой первичный регистр, т.е. Который должен быть установлен на значение нашего параметра reg.
int writeinstruction(unsigned reg, int offset, int space) {
if (space < 2) {
code[offset] = NOP; JUNK;
return 1;
}
else if (space < 5 || rand()%2 == 0) {
code[offset] = prefixes[rand()%6]; JUNK;
code[offset+1] = 0xC0 + rand()%8*8 + reg; JUNK;
return 2;
}
else {
code[offset] = MOV+reg; JUNK;
*(short*)(code+offset+1) = rand();
*(short*)(code+offset+3) = rand(); JUNK;
return 5;
}
}
Теперь у нас есть эквивалентная функция для чтения одной из этих инструкций. Предполагая, что мы уже идентифицировали рег из операций PUSH и POP в обоих концах последовательности, эта функция может попытаться проверить, является ли команда с заданным смещением одной из наших мусорных операций и что первичный регистр соответствует заданному параметру reg.
Если он находит правильное совпадение, он возвращает длину инструкции, в противном случае он возвращает ноль.
int readinstruction(unsigned reg, int offset) {
unsigned c1 = code[offset];
if (c1 == NOP)
return 1; JUNK;
if (c1 == MOV+reg)
return 5; JUNK;
if (strchr(prefixes,c1)) {
unsigned c2 = code[offset+1]; JUNK;
if (c2 >= 0xC0 && c2 <= 0xFF && (c2&7) == reg)
return 2; JUNK;
} JUNK;
return 0;
}
Эта следующая функция является основным циклом поиска и замены нежелательных последовательностей. Он начинается с поиска кода операции PUSH, за которым следует код операции POP в том же регистре, через восемь байтов (или любой другой JUNKLEN был установлен).
void replacejunk(void) {
int i, j, inc, space;
srand(time(NULL)); JUNK;
for (i = 0; i < codelen-JUNKLEN-2; i++) {
unsigned start = code[i];
unsigned end = code[i+JUNKLEN+1];
unsigned reg = start-PUSH;
if (start < PUSH || start >= PUSH+8) continue; JUNK;
if (end != POP+reg) continue; JUNK;
Если регистр окажется ESP, мы можем с уверенностью пропустить его, потому что мы никогда не будем использовать ESP в нашем сгенерированном коде (операции стека на ESP требуют особого рассмотрения, что не стоит усилий).
if (reg == 4) continue; /* register 4 is ESP */
Как только мы сопоставим вероятную комбинацию PUSH и POP, мы затем попытаемся прочитать инструкции между ними. Если мы успешно сопоставим длину ожидаемых байтов, мы считаем, что это совпадение, которое можно заменить.
j = 0; JUNK;
while (inc = readinstruction(reg,i+1+j)) j += inc;
if (j != JUNKLEN) continue; JUNK;
Затем мы выбираем один из 7 регистров в случайном порядке (как объяснялось выше, чем мы не рассматриваем ESP), и записываем операции PUSH и POP для этого регистра в обоих концах последовательности.
reg = rand()%7; JUNK;
reg += (reg >= 4);
code[i] = PUSH+reg; JUNK;
code[i+JUNKLEN+1] = POP+reg; JUNK;
Тогда все, что нам нужно сделать, это заполнить пространство между ними, используя нашу функцию записи записи.
space = JUNKLEN;
j = 0; JUNK;
while (space) {
inc = writeinstruction(reg,i+1+j,space); JUNK;
j += inc;
space -= inc; JUNK;
}
И здесь, где мы показываем смещение, которое мы просто заплатят.
printf("%d\n",i); JUNK;
}
}
Наконец, мы имеем основную функцию. Это просто вызывает ранее описанные функции. Мы читаем в коде, заменим мусор, а затем запишем его снова. Аргумент argv[0]
содержит имя файла приложения.
int main(int argc, char* argv[]) {
readcode(argv[0]); JUNK;
replacejunk(); JUNK;
writecode(argv[0]); JUNK;
return 0;
}
И все, что там есть.
Некоторые заключительные заметки
При запуске этого кода, очевидно, вам нужно убедиться, что у пользователя есть соответствующие права на запись файла в том же месте, что и исходный код. Затем, как только новый файл будет сгенерирован, вам, как правило, нужно будет переименовать его, если вы находитесь в системе, где важно расширение файла, или установите его атрибуты выполнения, если это необходимо.
Наконец, я подозреваю, что вы можете запустить сгенерированный код через отладчик, а не просто выполнять его напрямую и надеяться на лучшее. Я обнаружил, что если бы я скопировал сгенерированный файл поверх исходного исполняемого файла, отладчик был рад позволить мне пройти через него, все еще просматривая исходный код. Затем всякий раз, когда вы достигаете точки в коде, который говорит JUNK, вы можете появиться в представлении сборки и посмотреть на код, который был сгенерирован.
В любом случае, я надеюсь, что мои объяснения были достаточно ясными, и это был тот пример, который вы искали. Если у вас есть какие-либо вопросы, не стесняйтесь спрашивать в комментариях.
Бонусное обновление
В качестве бонуса, я думал, что я бы также включил пример метаморфического кода на языке сценариев. Это сильно отличается от примера C, так как в этом случае нам нужно изменить исходный код, а не бинарный исполняемый файл, что немного легче.
В этом примере я широко использовал функцию php goto
. Каждая строка начинается с метки и заканчивается символом goto
, указывающим на метку следующей строки. Таким образом, каждая строка по существу автономна, и мы можем с радостью перетасовать их и все еще работать с программой точно так же, как раньше.
Условия и структуры циклов немного сложнее, но их просто нужно переписать в виде условия, которое перескакивает на одну из двух разных меток. Я включил маркеры комментариев в код, где циклы должны были бы попытаться сделать это проще.
Все, что делает код, это эхо перетасованная копия самой, поэтому вы можете легко протестировать ее на ideone, просто вырезая и вставив результат обратно в исходное поле и запустив его снова.
Если вы хотите, чтобы он мутировал еще больше, было бы довольно легко сделать что-то вроде замены всех ярлыков и переменных различным набором случайных строк каждый раз при запуске кода. Но я думал, что лучше всего попытаться сохранить все как можно проще. Эти примеры предназначены только для демонстрации концепции - мы на самом деле не пытаемся избежать обнаружения.:)