Комментарии 53
Уважаемый товарищ mnakamura каким-то непостижимым образом нашел KS, когда мы его только начинали выкладывать на github и по сути была выложена только первая версия компилятора. Написал мне, я ответил, завязалась переписка — всегда же здорово, когда обращают внимание и когда по делу. Общались по-английски. И я, разумеется, в жизнь бы не подумал, что товарищ с именем Nakamura Matsuo и японским e-mail'ом может писать по-русски, да и еще отгрохать такую статью на хабр. Накамура, блин. Ну ладно я. Но у меня-то в почте вполне русские имя-фамилия видны, да и на github'е я особенно не скрываюсь. Неужели было сложно посмотреть? И уж тем более если статью на уважаемый ресурс пишешь — хоть спросить?
Сворачивая драму и возвращаясь к исходному вопросу и статье, позволю себе немножко прокомментировать:
1. На самом деле современная версия компилятора написана на Scala, а не на Java.
2. Для запуска на самом деле есть варианта: либо под JRE, либо под node.js (т.к. Scala может компилироваться в JavaScript).
3. Визуализатор действительно есть и он действительно не выложен. Мы планировали выкладывать его на следующей неделе, но раз события так подстегивают — дайте хоть полчаса-час времени, приведу его в более-менее приличное состояние и выложу.
Т.е. у автора статьи визуализатор, которого нет в паблике?
И вправду интересно получилось :)
Пользуясь случаем, хочу поблагодарить тогда еще раз уже по-русски за отличный проект. Извините, что так получилось с визуализатором.
Визуализатор появился в районе https://github.com/kaitai-io/kaitai_struct_visualizer/ — см. update 3.
Визуализатор появился в районе https://github.com/kaitai-io/kaitai_struct_visualizer/ — см. update 3.
Сейчас посмотрел внимательнее — в компиляторе явно есть файл LICENSE, что он GPL, а вот в визуализаторе нигде упоминаний о лицензии нет — соответственно, я немножко в ступоре — могу ли я его куда-то выкладывать или как… Сейчас напишу и спрошу.
Визуализатор появился в районе https://github.com/kaitai-io/kaitai_struct_visualizer/ — см. update 3.
Там своя ВМка с опкодами (коих я насчитал около 300). Больше года колупал — так и не смог понять принципа работы, настолько там всё завернуто. Все скрипты являются бинарниками с секциями кода и прочих данных, включая даже некие таблицы смещений. Все тексты упрятаны там же и не правятся без изменения кучи смещений в коде и т.п.
PS: Я пока колупаю двиг Overflow (School Days & Co). Уже давно 95%, даже свой двиг готов на его основе. осталось только пара фитч нереализованных.
Таблицы смещений в скриптах в количестве трёх штук — просто адреса команд с опкодами 0x71, 3, 0x8F. Первая — список возможных сообщений для хранения флагов прочитанности/непрочитанности, вторая — список возможных внешних вызовов из скрипта, третья — список возможных вызовов процедур. Подозреваю, что так сделано для большей стабильности сейвов: когда в сейвах стек вызовов хранится как массив индексов в отдельной таблице, а не прямо адресами внутри файла, это не будет плыть при каждом изменении скриптов.
Для правки текстов кучу смещений изменять не нужно, достаточно дописывать исправленные тексты в конец файла и править только одно смещение — собственно адрес текста.
from __future__ import print_function
import io, sys, struct
#table generated automatically, minor mistakes are possible
cmdsizes = (
0,1,1,3,3,1,5,3,3,1,5,23,1,9,25,3, #0x0
9,19,3,9,1,11,5,5,0,0,0,0,0,0,17,25, #0x10
13,5,5,5,5,7,9,9,9,0,9,11,11,25,11,9, #0x20
11,9,21,13,25,23,7,23,25,0,0,0,0,0,0,0, #0x30
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x40
7,7,7,7,7,5,7,7,7,7,7,7,7,7,7,7, #0x50
5,7,7,5,5,5,7,7,7,7,7,7,5,1,5,3, #0x60
11,3,3,21,3,3,3,3,3,7,7,5,1,5,3,3, #0x70
3,3,11,7,3,1,3,1,3,9,13,3,3,5,3,3, #0x80
15,3,5,1,1,5,1,11,0,0,0,0,0,0,0,0, #0x90
7,1,5,5,0,0,0,0,0,0,5,5,19,1,1,1, #0xa0
3,3,5,1,5,3,3,3,1,3,3,3,3,3,3,3, #0xb0
3,1,5,3,3,5,5,5,3,1,1,3,5,1,7,1, #0xc0
3,1,3,1,9,3,13,3,5,1,13,0,0,0,0,0, #0xd0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0xe0
0,0,0,0,0,0,0,0,0,0,1,5,1,5,3,1, #0xf0
1,1,7,3,1,3,3,5,3,5,5,5,5,3,5,3, #0x100
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x110
0,0,0,0,0,0,0,0,0,0,0,0,11,15,17,9, #0x120
3,3,3,5,7,5,5,3,5,7,13,15,3,7,5,7, #0x130
9,3,3,1,5,3,3,13,3,3,15,3,5,13,0,0, #0x140
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x150
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x160
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x170
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x180
5,5,5,7,7,7,7,3,7,1,3,1,1,5,5,5, #0x190
19,5,3,3,5,3,5,3,1,3,3,5,7,1,7,7, #0x1a0
7,3,3,1,1,3,3,3,5,5,5,3,1,3,5,1, #0x1b0
3,7,5,5,3,9,5,3,5,7,3,3,3,5,3,3, #0x1c0
7,11,5,11,9,1,5,5,7,5,0,0,0,0,0,0, #0x1d0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x1e0
0,0,0,0,1,1,1,5,9,7,3,17,3,9,11,9, #0x1f0
3,3,11,9,9,13,15,17,7,11,3,15,1,3,1,7, #0x200
3,3,5,7,5,5,5,9,9,9,9,3,1,5,13,15, #0x210
13,9,5,17,1,5,11,13,11,11,7,9,7,11,13,11, #0x220
3,9,9,11,11,11,9,5,3,13,5,15,1,1,5,5, #0x230
9,11,5,1,1,5,5,3,3,7,7,5,5,25,3,21, #0x240
21,25,3,5,11,13,11,11,5,1,3,3,17,7,11,9, #0x250
9,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x260
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x270
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x280
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x290
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x2a0
0,0,0,0,0,0,0,0,0,0,0,0,23,3,3,7, #0x2b0
7,3,13,5,1,5,5,9,9,7,3,3,3,3,3,3, #0x2c0
7,7,7,7,7,5,5,5,7,5,17,3,3,5,5,7, #0x2d0
7,7,7,7,7,3,5,5,3,3,3,3,5,3,3,23, #0x2e0
19,15,13,13,7,9,3,3,5,15,3,3,11,13,0,0, #0x2f0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x300
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, #0x310
21,7,9,11,1,5,9,3,7,5,3,1,13,5,23,3, #0x320
5,9,9,11,3,9,11,9,13,15,13,9,5,7,11,7 #0x330
)
# list created manually, may be incomplete
# (cmd in oplabels[i]) == (i-th operand of cmd is an offset)
oplabels = (
(0x7B,0x8C,0x8F,0xD5),
(0x7B,0x8D,0x92,0x95,0xA0,0xA2,0xA3,0xCC,0xCE,0xFB),
(0xA0,0xCE,0xD4,0x102),
(0xD4,),
(0xD6,0x90),
(0xD6,0x90),
(0x90,),
)
with io.open(sys.argv[1], 'rb') as f:
header = struct.unpack('<4s4s6I', f.read(0x20))
if header[0] != b'SYS3' and header[0] != b'SYS4':
print("invalid signature")
sys.exit(1)
header2size = struct.unpack('<I', f.read(4))[0]
header2 = struct.unpack('<' + str(header2size // 4 - 1) + 'I', f.read(header2size - 4))
data = f.read()
# pass 1: prepare labels
pos = 0
maxAddr = 0
labels = set()
while pos < len(data) // 4:
cmd = struct.unpack_from('<I', data, pos * 4)[0]
if cmd >= len(cmdsizes) or cmdsizes[cmd] == 0:
break
for i in xrange((cmdsizes[cmd] - 1) // 2):
optype, operand = struct.unpack_from('<ii', data, (pos + 1 + 2 * i) * 4)
if i < len(oplabels) and cmd in oplabels[i] and optype == 0 and operand >= 0:
labels.add(operand)
maxAddr = max(maxAddr, operand)
pos += cmdsizes[cmd]
if cmd in (1,2,5) and pos > maxAddr:
break
# pass 2: print
print("\theader '%s', %d, %d, %d, %d, %d, %d" % header[1:])
pos = 0
unprintedLabels = labels
firstStringOffs, lastStringOffs = -1, -1
firstArrayOffs, lastArrayOffs = -1, -1
messageStarts = []
externalCalls = []
internalCalls = []
while pos < len(data) // 4:
if pos in labels:
print("loc_%08X:" % pos)
unprintedLabels.remove(pos)
cmd = struct.unpack_from('<I', data, pos * 4)[0]
if cmd >= len(cmdsizes) or cmdsizes[cmd] == 0:
print("\t[invalid command 0x%X]" % cmd)
break
if cmd == 0x71:
messageStarts.append(pos)
if cmd == 3:
externalCalls.append(pos)
if cmd == 0x8F:
internalCalls.append(pos)
cmdsize = cmdsizes[cmd]
cmdargs = struct.unpack_from('<' + str(cmdsize - 1) + 'i', data, (pos + 1) * 4)
print("\tcmd%03X" % cmd, end='')
for i in xrange((cmdsize - 1) // 2):
if i:
print(",", end='')
print(" ", end='')
if cmdargs[i * 2] == 0:
if i < len(oplabels) and cmd in oplabels[i] and cmdargs[i * 2 + 1] >= 0:
print("loc_%08X" % cmdargs[i * 2 + 1], end='')
elif cmd == 0x64 and i == 1:
# second arg of cmd064 is offset of array in data
offs = cmdargs[i * 2 + 1]
if lastArrayOffs == -1:
firstArrayOffs = offs
elif lastArrayOffs != offs:
print("[Warning: out-of-order array]");
arrsize = struct.unpack_from('<I', data, offs * 4)[0]
arr = struct.unpack_from('<' + str(arrsize) + 'i', data, (offs + 1) * 4)
lastArrayOffs = offs + 1 + arrsize
print('<', end='')
for j in xrange(arrsize):
if j:
print(",", end='')
print('%d' % arr[j], end='')
print('>', end='')
else:
print('%d' % cmdargs[i * 2 + 1], end='')
elif cmdargs[i * 2] == 2:
offs = cmdargs[i * 2 + 1] * 4
if lastStringOffs == -1:
firstStringOffs = offs // 4
elif lastStringOffs != offs // 4:
print("[Warning: out-of-order string: %08X instead of %08X]" % (offs // 4, lastStringOffs), end='');
decoded = b''
while True:
s = ord(data[offs]) ^ 0xFF
if s == 0:
break
decoded += chr(s)
offs += 1
lastStringOffs = (offs + 1) // 4 + 1
print(b'"' + decoded + b'"', end='')
else:
print('op%X[0x%X]' % (cmdargs[i * 2], cmdargs[i * 2 + 1]), end='')
print()
pos += cmdsize
# command 5 can be a normal command with size=1 or nofollow-command depending on ???
# commands 4,9,0x7C are actually nofollow, but codegen seems to treat them as normal ones
if cmd in (1,2,5) and pos > maxAddr:
break
if len(unprintedLabels):
print("Warning: not all labels were printed");
expectedEndAddr = min(len(data) // 4, header2[1], header2[3], header2[5])
if firstArrayOffs != -1:
if lastArrayOffs != expectedEndAddr:
print("Warning: range [%08X,%08X) was not printed" % (lastArrayOffs, expectedEndAddr))
expectedEndAddr = min(expectedEndAddr, firstArrayOffs)
if firstStringOffs != -1:
if lastStringOffs != expectedEndAddr:
print("Warning: range [%08X,%08X) was not printed" % (lastStringOffs, expectedEndAddr))
expectedEndAddr = min(expectedEndAddr, firstStringOffs)
if pos != expectedEndAddr:
print("Warning: range [%08X,%08X) was not printed" % (pos, expectedEndAddr))
if messageStarts != list(struct.unpack_from('<' + str(header2[0]) + 'i', data, header2[1] * 4)):
print("Warning: unexpected messageStarts array")
if externalCalls != list(struct.unpack_from('<' + str(header2[2]) + 'i', data, header2[3] * 4)):
print("Warning: unexpected externalCalls array")
if internalCalls != list(struct.unpack_from('<' + str(header2[4]) + 'i', data, header2[5] * 4)):
print("Warning: unexpected internalCalls array")
В принципе, при некотором желании выдачу дизассемблера выше можно даже скомпилировать назад с помощью fasmg, если пошаманить с его макросами.
Интересно. А cmdsizes вы доставали откуда-то из exe-шника?
mov dword ptr [esi+ecx*4+5D804h], 5
, так что auto a,b,aprev,anext,s,f;
f=fopen("c:\\temp\\eushully_vm_cmdsize.txt","w");
for (b=0x415303;b!=BADADDR;b=NextHead(b,0x416713)){
// установка обработчиков: mov dword ptr [esi+0xA4F24+i*4], offset cmd_i
if (GetOpType(b,0)!=4) continue;
fprintf(f,"0x%X\t",(GetOperandValue(b,0) - 0xA4F24)/4);
s=GetOperandValue(b,1);
aprev=BADADDR;
for (a=s;a!=BADADDR;a=anext){
anext=NextHead(a,BADADDR);
if(!isCode(GetFlags(a))){fprintf(f,"[not a code at %X]\n",a);break;}
if (GetOpType(a,0) == 4 && GetOperandValue(a,0) == 0x5D804) {
if (GetOpType(a,1) == 5)fprintf(f,"%d\n",GetOperandValue(a,1));
else if (GetOpType(a,1) == 1 &&
GetMnem(aprev) == "mov" &&
GetOpType(aprev,0) == 1 &&
GetOperandValue(aprev,0) == GetOperandValue(a,1) &&
GetOpType(aprev,1) == 5)fprintf(f,"%d\n",GetOperandValue(aprev,1));
else fprintf(f,"[unknown write type at %x]\n",a);
break;
}
// если обнаружили ветвление, на всякий случай выходим
if(Rfirst(a)!=anext || Rnext(a,anext)!=BADADDR){a=BADADDR;break;}
aprev=a;
}
if (a==BADADDR) fprintf(f,"[unresolved: %x]\n",s);
}
fclose(f);
заполняет таблицу, за исключением нескольких сложных случаев, которые уже можно добить руками.
Ох. Это уже как-то неспортивно получается и уже совсем не clean-room, но в целом — почему бы и нет ;)
Я все пытаюсь делать максимально без залезания внутрь, кроме совсем уж тяжких случаев — так и интереснее, и куда ближе к боевым условиям. Ну и банально потому, что есть туча новелл типа под всякие PC98, которые даже запустить негде, не то, что дизассемблировать или дебагать.
А 0x415303 и 0xa4f24 — это в age.exe от какой игры?
Тут выше назвали одну конкретную игру, Kami no Rhapsody, это оттуда. Движок версии 4.46.
А то как-то пытался расковырять формат хранения диалогов в одной игре (ресурсы не запакованы), но так ничего и не вышло (нет опыта в реверсе).
Максимум, что выжал — название движка, сайт, где можно его скачать и собственно, триал движка и вроде даже какого-то инструментария. Но он, разумеется, даже не на английском. На этом я остановился и бросил затею.
Однако я бы не был столь категоричен насчёт 010. Распаковщик можно написать даже в рамках его скриптового языка, и мне приходилось вполне успешно это делать (хотя количество багов что в 010, что в питоновой реализации его шаблонизатора убивает).
Более того, лично мне представление данных в 010 очень удобно, так что уйти от него будет нелегко. Да и много больше времени при разборе формата уходит на алгоритм сжатия, чем на написание небольшого куска кода для i/o. Тем более, что почти всегда это копипаст с других утилит.
К конкретно 010 у меня есть несколько крупных претензий:
- Он закрытый и стоит денег.
- Его язык шаблонов только на первый взгляд декларативный. Думаю, вы как раз знаете, что там шаг влево-шаг вправо — привет, старые добрые циклы
while (!FEof()) { ... }
или ручныеReadBytes(...)
,ReadUInt(...)
и т.д. - Как следствие императивности — на любых более-менее больших файлах (гигабайты хотя бы) — 010 превращается в улитку и жрет память, как не в себя. Нельзя даже просто разметить области, не вдаваясь в подробности: приходится искусственно городить в шаблоне некое подобие LOD, только для того, чтобы каждая итерация не занимала по 5-7 минут.
Насчет алгоритма сжатия — с одной стороны соглашусь, с другой — как раз, субъективно, это куда более банальная задача. Алгоритм сжатия особенно нечего угадывать и исследовать — он зачастую выдирается буквально как есть из бинарника, ассемблер транслируется в эквивалентный C, скажем, после чего его можно просто запускать и особенно в нем не копаться.
А вот со сжатием — если дело с популярной платформой типа x86, то часто легче выдрать, в консольных же движках подлезть далеко не всегда проще ручного анализа.
отличие от конкурентов даже после выжирания гигабайтов памяти он не вылетает и не зависает наглухо
Данупрям. Вот только что попробовал загрузить свою разбиралку формата VNки 3-летней давности: исходный бинарник 4 гига (под 1 DVD подгоняли), выжрало 16 гигабайт памяти и 8 с чем-то свопа, потормозило минут 15 и умерло.
А вот со сжатием — если дело с популярной платформой типа x86, то часто легче выдрать, в консольных же движках подлезть далеко не всегда проще ручного анализа.
radare чудесно делает из фактически кода любой платформы C-подобный код, который минимально надо обработать напильником и можно хоть в C, хоть в Java, хоть в JavaScript.
Лукавство лукавством, но как реверсер вы должны понимать, что сложность не столько в декомпиляторах, сколько в возможности подобраться к функции распаковки, начиная с возможности чтения бинарника вообще. Отладчика может не быть, а XREF-ы на библиотеки не находиться.
Впрочем, мы по-моему отошли от темы, к предложенному инструменту конкуренты не относятся; спасибо за дискуссию.
Да ответ, в общем, простой: все эти штуки создают разметку не on demand, а сразу проходя всё. При том, что в реальности читающая процедура делает не так: ей незачем загружать в память (и тем более распаковывать сразу) все содержимое DVD, она зачитает три с половиной нужных прямо сейчас файла, остальное зачитает, когда будет нужно.
Впрочем, мы по-моему отошли от темы, к предложенному инструменту конкуренты не относятся; спасибо за дискуссию.
Вполне интересная тема, я на такие могу часами разговаривать :) На одном небезызвестном сайте, обсуждая эту статью, сейчас подкинули новую интересную задачку — я уже второй вечер над ней голову ломаю. Пока заткнулся на том, что есть 16-байтовые строчки, из которой надо извлечь позицию и размер файла в архиве. Еще пару вечеров помедитирую, а потом, наверное, сдамся, и перейду на тяжелую артиллерию типа Olly или IDA.
radare чудесно делает из фактически кода любой платформы C-подобный код, который минимально надо обработать напильником и можно хоть в C, хоть в Java, хоть в JavaScript.
Не поделитесь как от r2 получить такой C-подобный код?
Периодически проводятся всякие CTF-конкурсы (типа такого).
примерно как переехал сюда и начал язык учить
Т.е. вы переехали в Японию. Я правильно понял? А с какой целью и чем занимаетесь, если не секрет?
В статье вы, кажется, не упоминали.
А с какой целью и чем занимаетесь, если не секрет?
Конечно же, скупить все фигурки на Акихабаре (*•̀ᴗ•́*)
А если серьезно — ну, нравится мне страна, нравится культура. Сакура-но-кайка вон неделю назад буквально закончилась.
А так все примерно как у 90% уезжающих: минимально выучил язык по аниме, сдал на JLPT n4, поступил учиться, за год освоился, в итоге остался. Живу, работаю. Компанию, наверное, напрямую писать от греха подальше не буду, но, с другой стороны гуглится это все за две минуты.
minori и ExtractData — местные копирасты весьма агрессивно относятся к тому, что продаваемые в их магазинах визуальные новеллы распаковывают. Если читаете по-японски — то http://mitiku3.jugem.jp/?eid=5959 — автор ExtractData вроде бы до сих пор settlement им выплачивает, ему что-то в районе 2 миллионов йен выкатили.
По-японски к сожалению не читаю. Но то, что жестко относятся к ковыряниям продуктов — тут бы и я не сильно-то распространялся про себя, в таком случае.
Любопытно. Чтож, успехов вам :)
P.S. И большое спасибо за статью. Хорошая статья про реверс инженеринг — это большая редкость.
Хотя я думаю, они там и без вас свои движки вовсю клепать будут.
Эдак с половина индустрии лицензирует так или иначе kirikiri, вторая половина пишет свое. Как правило, по одной из двух причин: либо требование издателя, либо некий юношеский максимализм и гордость не позволяют освоить готовое, а хочется обязательно написать свое. У мейджоров, опять же, как правило свои уже готовые и десятилетиями отлаженные решения.
Написание движков — по большому счету, относительно простой и нудный процесс. Каких-то программистских сверхзадач там нет, все весьма рутинно: загрузи картинки-музыку-звук-шрифты-текст да показывай/запускай их в нужном порядке. Встроенные минигеймы, если они и есть, опять же, обычно в лучшем случае добавляют к вышеописанному чуть более сложную математику и все.
В общем, не, не нужны там программисты. Там творческие люди нужны, а из меня писатель никакой, тем более на японском.
Реверс-инжиниринг визуальных новелл