~ neuro disassembler ~
Как-то раз понадобился нам дизассемблер длин опкодов под x86. Но чужие
статические таблицы длин опкодов юзать не хотелось, поэтому в результате
мытарств родился самообучающийся динамический дебагер. В нём юзается что-то
вроде нейро-сети, хотя конечно знатоки скорее всего это дело назовут как-нить
иначе. Ну да пох, собственно какова идея:
Есть файл disasm.txt, в нём хранятся записи в таком виде:
{первый опкод, второй опкод, третий опкод, длина инструкции}
Например:
{0x5d, 0, 0, 1},
{0x90, 0, 0, 1},
{0xe8, 0x16, 0, 5},
Это собственно мозг нашего дизассемблера. По мере его обучения, файл
увеличивается. Это своего рода нейросеть и есть =)
Далее есть скрипт, который перегоняет disasm.txt в удобоваримый код
массива структур и сохраняет его в disasm.h, который можно юзать из
какой-нибудь сишной проги.
Теперь есть прога, которая юзает disasm.h и пытается дизасмить на блоки
какой-то бинарник. Пашет это так: читаем байт из бинарника и прогоняем его по
массиву структур из disasm.h, если первый байт совпадает смотрим на второй и
третий байты. Если же других структур, кроме той где указан лишь первый байт
опкода, нету - то берём длину из этой структуры и скипаем на её величину.
Если записи с первым опкодом равным искомому байту нету, то выходим. Всё
просто до безобразия. Прогу назовём ./disasm.
Код в бинарнике:
0x55 0x89 0xe5 0x83 0xec 0x0c 0x90 ...
Структуры:
...
{0x55, 0, 0, 1},
{0x89, 0, 0, 2},
{0x83, 0xec, 0, 3},
{0x90, 0, 0, 1},
...
Вот как будет идти дизасм:
[ 55 ] (длина блока 1)
[ 89 e5 ] (длина блока 2)
[ 83 ec 0c ] (длина блока 3)
[ 90 ] (длина блока 1)
Таким образом мы можем представить бинарный код в виде блоков-инструкций,
каждую из которых можно поменять или проинфектить. Ну об этом далее, а пока
сосредоточимся на том как сделать самообучающийся дизассемблер.
На данный момент всё тупо и статично - если есть записи в файле disasm.txt
то всё норм, если нет - то дизассемблер начинает спотыкаться. Поэтому наша
задача - написать софтину (в данном случае это перловый скрипт), которая
будет изучать причины спотыкания дизассемблера и модифицировать
disasm.txt. Наш вариант основан на перле и юзает objdump - смотрит как
разбирает инструкции на блоки disasm и сверяет с выводом objdump-а, и в
зависимости от этого модифицирует свою память (disasm.txt). Вот лог работы:
learning current neuro-net
try 8049c87
8049c87: a0 3a be 04 08 mov 0x804be3a,%al
i=13 addr = 0x8049c87
len = 5
new = {0xa0, 0x3a, 0xbe, 5},
new array len = 4 (0x, 0x, 0x)
new array len = 4 (0x, 0x, 0)
try 8049e2e
minus 2
try 8049e2c
8049e2c: ff 70 1c pushl 0x1c(%eax)
i=13 addr = 0x8049e2c
len = 3
new = {0xff, 0x70, 0x1c, 3},
new array len = 30 (0x, 0x, 0x)
new array len = 30 (0x, 0x, 0)
new array len = 1 (0x, 0, 0)
nlen=2 lena=3
try 8049fca
minus 2
try 8049fc8
8049fc8: ff 72 0c pushl 0xc(%edx)
i=13 addr = 0x8049fc8
len = 3
new = {0xff, 0x72, 0x0c, 3},
new array len = 31 (0x, 0x, 0x)
new array len = 31 (0x, 0x, 0)
new array len = 1 (0x, 0, 0)
nlen=2 lena=3
try 8049fe3
minus 2
try 8049fe1
8049fe1: ff 71 10 pushl 0x10(%ecx)
i=13 addr = 0x8049fe1
len = 3
new = {0xff, 0x71, 0x10, 3},
new array len = 32 (0x, 0x, 0x)
new array len = 32 (0x, 0x, 0)
new array len = 1 (0x, 0, 0)
nlen=2 lena=3
try 804a171
minus 3
try 804a16e
804a16e: 66 3d 00 20 cmp $0x2000,%ax
i=13 addr = 0x804a16e
len = 4
new = {0x66, 0x3d, 0, 4},
new array len = 15 (0x, 0x, 0x)
new array len = 5 (0x, 0x, 0)
new array len = 1 (0x, 0, 0)
nlen=3 lena=4
try 804a50f
minus 1
try 804a50e
minus 2
try 804a50c
804a50c: f7 35 48 d6 04 08 divl 0x804d648
i=13 addr = 0x804a50c
len = 6
new = {0xf7, 0x35, 0x48, 6},
new array len = 13 (0x, 0x, 0x)
new array len = 13 (0x, 0x, 0)
new array len = 1 (0x, 0, 0)
nlen=2 lena=6
try 804a53b
minus 2
try 804a539
minus 2
try 804a537
minus 3
try 804a534
804a534: 83 7f 0c 01 cmpl $0x1,0xc(%edi)
i=13 addr = 0x804a534
len = 4
new = {0x83, 0x7f, 0x0c, 4},
new array len = 30 (0x, 0x, 0x)
new array len = 30 (0x, 0x, 0)
new array len = 1 (0x, 0, 0)
nlen=3 lena=4
Ну а теперь собственнно детали и код. Disasm, когда спотыкается, дампает
инфу о том, на каком опкоде он споткнулся и какие успел до этого прошагать.
Примерно такой вот вывод:
804a57d - адрес, где произошло спотыкание
cb - опкод, вызвавший сбой
0x8d:3 - последние 6 пройденых опкодов
0x99:1
0x3:2
0x38:2
0x56:1
0x3c:2
Разбирать будем это так: смотрим напрямую objdump-овый дамп по данному
адресу, смотрим где мы находимся. Если опкод, вызвавший сбой - первый байт
инструкции, то это просто неизвестная нам запись. Добавляем её в disasm.txt.
Дальше, если адрес где произошло спотыкание ни показывает ничего через
objdump (мы попали в середину блока какой-то инструкции), то шагаем назад по
списку и проверяем записи в нейро-сети. Либо одна из предидущих длин записей
указана неверно, либо существует случай при котором нужно учитывать второй
опкод, или даже третий. Модифицируем "мозг".
Вот исходник скрипта для обучения нейро-сети:
#!/usr/bin/perl
@phile = `cat file`;
chomp @phile;
if ($#phile < 1)
{
print "BLYA\n";
exit;
}
$IS_LIB = $phile[1];
$PROG = $phile[0];
@argz = `cat status`;
chomp @argz;
($ch[1], $len[1]) = split(/:/, $argz[7]);
($ch[2], $len[2]) = split(/:/, $argz[6]);
($ch[3], $len[3]) = split(/:/, $argz[5]);
($ch[4], $len[4]) = split(/:/, $argz[4]);
($ch[5], $len[5]) = split(/:/, $argz[3]);
($ch[6], $len[6]) = split(/:/, $argz[2]);
if ($IS_LIB)
{
$addr = hex($argz[0]);
} else {
$addr = hex($argz[0]);
}
if ($argz[7] eq "0xc3:1" && $argz[1] eq "0")
{
print "Done\n";
`echo DONE > complete`;
exit;
}
$i = 0;
while ($i < 7)
{
$adrs = sprintf("%x", $addr);
print "try $adrs\n";
$tst = `objdump -d $PROG | grep $adrs:`;
print "$tst\n";
chomp $tst;
if (length($tst) > 2)
{ $i = 12; }
$i++;
if ($i < 7)
{
$addr -= $len[$i];
print "minus $len[$i]\n";
}
}
printf("i=%i addr = 0x%x\n", $i, $addr);
$ls=`objdump -d $PROG | grep $adrs:`;
chomp $ls;
@test = split(/\t/, $ls);
$ls = $test[1];
$ops = "";
$ls =~ s/([a-f0-9][a-f0-9])\s/ $ops .= "$1 " /ge;
@op_r = split(/ /, $ops);
$op_r[0] = "0x$op_r[0]";
$lena = $#op_r + 1;
if ($lena > 1)
{
if ($lena > 2)
{
if ($op_r[2] eq "00")
{
$op_r[2] = 0;
} else {
$op_r[2] = "0x$op_r[2]";
}
}
if ($op_r[1] eq "00")
{
$op_r[1] = 0;
} else {
$op_r[1] = "0x$op_r[1]";
}
}
$lena = $#op_r+1;
if ($lena == 7)
{
print "EXTRA LEN CHECK\n";
$xtr = `objdump -d $PROG | grep -1 $adrs: | tail -1`;
chomp $xtr;
($a1, $a2) = split(/:/, $xtr);
$xtr = $a2;
($b1, $b2, $b3) = split(/\t/, $xtr);
$xtr = $b2;
# $xtr =~ s/\s+/ /g;
@xtra = split(/ /, $xtr);
# print "len xtra = $#xtra; len xtra2 = $#xtra2\n";
# if ($#xtra == $#xtra2 + 2)
if ($b3 eq "")
{
print "found long isnt $xtr\n";
$lena += $#xtra+1;
}
}
print "len = $lena\n";
if ($i == 13) # true unknown op
{
add_op();
exit;
}
#
@data = `cat disasm.txt`;
chomp @data;
if ($lena > $len[$i])
{
open(FD,">disasm.h");
foreach $rec (@data)
{
if ($rec =~ m/{0x$op_r[0], 0/)
{
print FD "{0x$op_r[0], 0x$op_r[1], 0, $lena},\n";
} elsif ($rec =~ m/0, $len[$i]}/)
{
print FD "{0x$op_r[0], 0x$op_r[1], 0, $lena},\n";
} else {
print FD "{0x$op_r[0], 0x$op_r[1], 0x$op_r[2], $lena},\n";
}
print FD "$rec\n";
}
close(FD);
}
if ($lena < $len[$i])
{
print "change len for op $op_r[0] from $len[$i] to $lena\n";
open(FD,">disasm.h");
foreach $rec (@data)
{
if ($rec !~ m/^{0x$op_r[0]/ || $rec !~ m/$len[$i]}/)
{ print FD "$rec\n"; next; }
if ($lena < 6)
{
print FD "{0x$op_r[0], 0, 0, $lena},\n";
} else {
print FD "{0x$op_r[0], 0x$op_r[1], 0, $lena},\n";
}
}
close(FD);
}
if ($lena == $len[$i])
{ exec("echo DONE > complete"); }
exit;
sub add_op()
{
@data = `cat disasm.txt`;
chomp @data;
print "new = {$op_r[0], $op_r[1], $op_r[2], $lena},\n";
open(FD, ">disasm.txt");
# write all other lines
$i = 0;
foreach $line (@data)
{
if ($line !~ m/{$op_r[0]/)
{
print FD "$line\n";
} else {
$dat2[$i] = $line;
$i++;
}
}
if ($i == 0)
{
new_add();
close(FD);
exit;
}
print "new array len = $i (0x, 0x, 0x)\n";
$i = 0;
# {0xa, 0xb, 0xc}
foreach $line (@dat2)
{
if ($lena < 3 || $line !~ m/{$op_r[0], $op_r[1], $op_r[2], /)
{
if ($line !~ m/, 0,/)
{
print FD "$line\n";
} else {
$dat3[$i] = $line;
$i++;
}
next;
}
if ($line =~ m/, $lena}/)
{
print FD "$line\n";
last;
} else {
print "ERROR - two eq rec with diff lens $line vs ($lena)\n";
$c = `echo ASS > complete`;
exit;
}
}
if ($i == 0)
{
new_add();
close(FD);
exit;
}
print "new array len = $i (0x, 0x, 0)\n";
$i = 0;
# {0xa, 0xb, 0}
foreach $line (@dat3)
{
if ($lena < 2 || $line !~ m/{$op_r[0], $op_r[1], 0, /)
{
if ($line !~ m/, 0, 0/)
{
print FD "$line\n";
} else {
$dat4[$i] = $line;
$i++;
}
next;
}
$nlen = get_len($line);
if ($nlen == $lena)
{
print "line $line\n";
print "EQUAL\n";
print FD "$line\n";
close(FD);
exit;
}
if ($lena > $nlen)
{
print FD "{$op_r[0], $op_r[1], $op_r[2], $lena},\n";
print FD "$line\n";
} else {
print FD "{$op_r[0], $op_r[1], 0, $lena},\n";
}
last;
}
if ($i == 0)
{
new_add();
close(FD);
exit;
}
print "new array len = $i (0x, 0, 0)\n";
if ($i > 1)
{
print @dat4;
close(FD);
$c = `echo $i > complete`;
exit;
}
# $i should be == 1
$nlen = get_len($dat4[0]);
print "nlen=$nlen lena=$lena\n";
if ($lena > $nlen)
{
print FD "{$op_r[0], $op_r[1], 0, $lena},\n";
print FD "$dat4[0]\n";
} else {
print FD "{$op_r[0], 0, 0, $lena},\n";
}
close(FD);
return 0;
}
sub get_len()
{
$t = shift @_;
($a,$b,$c,$d) = split(/,/, $t);
$d =~ s/ //;
$d =~ s/[{,}]//;
return $d;
}
sub new_add()
{
if ($lena > 6)
{
print FD "{$op_r[0], $op_r[1], $op_r[2], $lena},\n";
}
if ($lena > 4 && $lena < 7)
{
print FD "{$op_r[0], $op_r[1], 0, $lena},\n";
}
if ($lena < 5)
{
if ($op_r[0] ne "0x00")
{
print FD "{$op_r[0], 0, 0, $lena},\n";
} else {
print FD "{$op_r[0], $op_r[1], 0, $lena},\n";
}
}
return 0;
}
Всё это проще понять, если погонять софтину на своём боксе. В инклудах в дире
neiro лежат все необходимые исходнки. Юзается это всё так:
1. в auto.sh правим вот эти строки:
DPROG="./test" # файл, который собираемся дизасмить
OFFS="0x8048330" # смещение к началу секции с кодом, виртуальный адрес
IS_LIB=0 # исследуемый файл: 1 - библиотека, 0 - обычный бираник
2. если есть желание обучать нейросеть с нуля, то достаточно удалить старые
базы.
# rm -f disasm.h disasm.txt
3. производительность зависит от многих параметров, но к примеру на роутере
p133 и 32mb RAM с FreeBSD система пашет сравнительно медленно =) Поэтому
желательно машину побыстрее.
з.ы. замечаение, могут возникнуть мысли типа - а не проще ли, если система
эта юзает objdump сделать просто сразу полный дизасм-дамп всего кода и
его уже просто раскидать на записи disasm.txt. Это конечно неплохо, но тогда
получится скорее не самообучающийся дизассемблер, а продвинутый скрипт-парсер.
Кому надо - тот напишет )