Как стать автором
Обновить

Секреты Lineage II: скрытые возможности клиента

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров19K

Ровно 20 лет назад, 8 июня 2004 года, в корейской компании NCSoft был скомпилирован файл L2Server.exe - основной компонент игрового сервера новейшей на тот момент ММО игры Lineage II The chaotic chronicle: Chronicle 1 - Harbingers of War. В результате произошедших затем событий, всех подробностей которых мы вероятно никогда не узнаем, этот файл, вместе с сопутствующими компонентами, данными и скриптами стал, так скажем, "достоянием общественности", дав начало эре неофициальных серверов, а также огромной популярности Lineage 2 в СНГ, и не только. В этой, и последующих, статьях мы познакомимся с техническими подробностями и секретами как клиента, так и сервера этой игры, некоторые из которых не были широко известны не только игрокам, но и администраторам серверов.

Дисклеймер: Вся информация предоставляется исключительно в ознакомительных целях и получена из открытых источников. Автор не является, и никогда не являлся, сотрудником компании NCSoft или её партнёров, не заключал с ними никаких соглашений, и не имеет никакого отношения и никакой информации касательно произошедших сливов.

В качестве подопытного будем использовать клиент и сервер C1, как в те времена, а также инструментарий тех времён: отладчик OllyDbg с плагином ODbgScript и дизассемблер IDA. Несмотря на то, что некоторую информацию и инструменты сейчас можно просто нагуглить, пройдем весь путь с нуля, как если бы мы оказались в 2004 году, так что никаких питонов, гидр, и прочего новодела.

Начнём с изучения клиента. В корне клиента лежит файл LineageII.exe - но это лаунчер, нам он не нужен. Можно удалить, чтоб не мешался. Настоящий exe клиента, вместе с dll и конфигами, находится в System и называется l2.bin. Переименуем его в l2.exe и убедимся что клиент нормально запускается.

Экран логина клиента
Экран логина клиента

Настраиваем клиент

Итак, свежеустановленный оригинальный клиент стартует в полноэкранном режиме, а при попытке логина будет пытаться залогиниться на официальный сервер. Нужно разобраться с этими недоразумениями - сделать так, чтоб клиент запускался в окне и логинился на локальный сервер. Клиент основан на модифицированном движке UnrealEngine. В System находится много ini файлов, однако они зашифрованы или запакованы:

l2.ini - виден заголовок, а дальше - ничего не понятно
l2.ini - виден заголовок, а дальше - ничего не понятно

Разберёмся с этим. Загрузим клиент под дебаггером, поставим брейкпоинт на CreateFileA и CreateFileW, запустим, проскипаем до места, где будет открываться l2.ini. Выйдя из функции kernel32 попадаем сюда:

После срабатывания брейкпоинта на CreateFileW и выхода из функции
После срабатывания брейкпоинта на CreateFileW и выхода из функции

Теперь поставим брейкпоинт на ReadFile. После брейка видим такой стек:

Стек после срабатывания брейкпоинта на ReadFile
Стек после срабатывания брейкпоинта на ReadFile

Поставим брейкпоинт на доступ к памяти по адресу Buffer + 0x20 (пропускаем заголовок файла), запускаем, срабатывает брейк в core.dll. Выходим из функции, попадаем сюда:

После срабатывания брейкпоинта на доступ к памяти и выхода из функции шифрования
После срабатывания брейкпоинта на доступ к памяти и выхода из функции шифрования

Функция шифрования оказалась в экспорте dll-ки, так что даже не нужно гадать чем зашифровано. Добрые корейцы делают всё для людей :) Выйдя еще на уровень выше, попадаем сюда:

Функция шифрования с захардкоженным ключом имеет криптостойкость равную нулю
Функция шифрования с захардкоженным ключом имеет криптостойкость равную нулю

А вот и ключ шифрования. Теперь можно написать дешифровщик и шифровщик, реализацию алгоритма BlowFish возьмём из OpenSSL.

#define La2IniHeaderSize 0x1C

const unsigned char l2_ini_enc_key[] = "[;'.]94-&@%!^+]-31==";

void La2IniDecryptVer212(uint8_t * buf, size_t buf_size) {
	BF_KEY key;

	BF_set_key(&key, sizeof(l2_ini_enc_key), l2_ini_enc_key);

	for (size_t offs = La2IniHeaderSize; offs + BF_BLOCK < buf_size; offs += BF_BLOCK) {
		BF_decrypt((unsigned int *)(buf + offs), &key);
	}
}

Впрочем, все давно сделано за нас, так что можно воспользоваться тулзой dstuff l2encdec. В расшифрованном l2.ini пропишем:

ServerAddr=127.0.0.1
StartupFullscreen=False

Затем зашифруем обратно и положим файл в System. Теперь клиент стартует в безрамочном окне и логинится на 127.0.0.1:2106. Можно запустить локальный сервер и начать развлекаться. Запуск сервера - это отдельная большая тема, которую я не буду рассматривать в этой статье, отмечу лишь что большинство экспериментов проводилось на самописном минималистичном сервере, на который можно только зайти и соло побегать, так что не удивляйтесь отсутствию мобов и НПЦ.

Находим аргументы командной строки

Теперь посмотрим, принимает ли клиент какие-то аргументы командной строки. Ставим брейкпоинт на GetCommandLineA и GetCommandLineW, запускаем. Срабатывает брейк на GetCommandLineW. Трейсим до выхода из функции, ставим брейк на доступ к памяти по адресу EAX. Брейк срабатывает в функции:

Core.?ParseParam@@YAHPBG0@Z ; ParseParam(ushort const *,ushort const *)

Поскольку она вызывается много раз, воспользуемся скриптом для ODbgScript чтобы сдампить все аргументы:

cycle:
log [esp+4]
log [esp+8]
run
jmp cycle

В результате получаем вот такой список:

Hidden text

STRICT
CONFLICTS
NOGC
NOMMX
NOSSE
SERVER
LAZY
LOG
server
BENCHMARK
320x240
512x384
640x480
800x600
1024x768
1280x960
1280x1024
1600x1200
FirstRun
safe
defaultres
nodeviceid
lanplay
nomusic
NOSOUND
nomusic
windowed
RECORDMOVIE
MEMSTAT

А декомпилировав в IDA функцию ParseParam выясняем, что аргументы должны быть с префиксом '-' или '/':

int __cdecl ParseParam(const unsigned __int16 *a1, wchar_t *Str)
{
  ...
  if ( !v2 )
  {
    while ( 1 )
    {
      v3 = appStrfind(v3 + 1, Str);
      if ( !v3 )
        break;
      if ( v3 > a1 )
      {
        v4 = *(v3 - 1);
        if ( v4 == '-' || v4 == '/' )
          return 1;
      }
    }
  }
  return 0;
}

Настало время воспользоваться полученными знаниями. В первую очередь интересен аргумент LOG, который запускает клиент с открытой игровой консолью:

Клиент запущен с открытой консолью
Клиент запущен с открытой консолью

Из прочих команд:

  • windowed - по идее должна включать оконный режим но не работает, вероятнее StartupFullscreen из l2.ini приоритетнее;

  • RECORDMOVIE - таки записывает видео, но в виде кучи скриншотов;

  • MEMSTAT - пишет в лог статистику при закрытии клиента.

Добываем список консольных команд

Исследуем что же можно сделать через игровую консоль. Подбором обнаруживаем что работают команды exit и quit. Эти строки встречаются в файлах Engine.dll и Core.dll. Грузим Engine.dll и находим где используются эти строки:

.text:104D14A2                 lea     edx, [ebp+arg_0]
.text:104D14A5                 push    offset aExit    ; "EXIT"
.text:104D14AA                 push    edx
.text:104D14AB                 call    edi ; ParseCommand(ushort const * *,ushort const *) ; ParseCommand(ushort const * *,ushort const *)
.text:104D14AD                 add     esp, 8
.text:104D14B0                 cmp     eax, ebx
.text:104D14B2                 jnz     loc_104D17C6
.text:104D14B8                 lea     eax, [ebp+arg_0]
.text:104D14BB                 push    offset aQuit    ; "QUIT"
.text:104D14C0                 push    eax
.text:104D14C1                 call    edi ; ParseCommand(ushort const * *,ushort const *) ; ParseCommand(ushort const * *,ushort const *)

ParseCommand - это функция ?ParseCommand@@YAHPAPBGPBG@Z из Core.dll.

Теперь запустим клиент под дебаггером и поставим брейкпоинт на ParseCommand. Введя произвольную команду, обнаруживаем что команды действительно парсятся этой функцией. Теперь можно сдампить список похожим скриптом:

cycle:
log [[esp+4]]
log [esp+8]
run
jmp cycle

В итоге получаем такой список:

Hidden text

OPEN
START
SERVERTRAVEL
DISCONNECT
RECONNECT
EXIT
QUIT
GETCURRENTTICKRATE
GETMAXTICKRATE
GSPYLITE
SAVEGAME
CANCEL
SOUND_REBOOT
SHOWEXTENTLINECHECK
SHOWLINECHECK
SHOWPOINTCHECK
KDRAW
KSTEP
KSTOP
KSAFETIME
MEMSTAT
CONFIGHASH
EXIT
QUIT
RELAUNCH
DEBUG
DIR
MEM
DUMPNATIVES
GET
SET
OBJ
GTIME
DUMPCACHE
SHOWLOG
TakeFocus
EditDefault
EditActor
CopyToClipboard
HideLog
Preferences
BRIGHTNESS
CONTRAST
GAMMA
PAUSESOUNDS
UNPAUSESOUNDS
STOPSOUNDS
WEAPONRADIUS
ROLLOFF
GRAPH
L2Debug
L2DebugWindow
FLUSH
STAT
CRACKURL
PACKETCOUNTSTART
PACKETCOUNTSTOP

Введя команду PACKETCOUNTSTART, получаем такую статистику:

Человеческие названия пакетов - как удобно, спасибо корейцам.
Человеческие названия пакетов - как удобно, спасибо корейцам.

Нужно больше команд!

Известно, что существуют команды, отдаваемые через чат клиента, например /loc - показывает текущие координаты персонажа. Поэкспериментировав с разными префиксами обнаруживаем, команды с префиксом /// исполняются аналогично введенным в консоли. Кроме того, список xrefs на ParseCommand в IDA намекает, что команд может быть больше, чем мы обнаружили. Еще есть префикс //, используемый для административных команд, и оба эти префикса работают только если у персонажа статус GM-а.

Попробуем аналогично сдампить список команд, введенных с префиксом. Правда оказывается, что ParseCommand также используется движком для обработки команд PlayerPawnMoveTo, CameraYaw и прочих, так что придется помучиться с этим спамом прежде чем получить искомый результат. В итоге получаем вот такой список:

Hidden text

BUTTON
PULSE
TOGGLE
AXIS
JOYPAD
COUNT
KEYNAME
LOCALIZEDKEYNAME
KEYBINDING
L2Restart
Warp
rwarp
MoveWarp
L2WaterInfo
L2WaterReflect
EnterChat
L2EVENTON
L2EVENTOFF
GETITEM
TARGETCHANGE
SHOWCOMPASS
HIDECOMPASS
SCENE0
SCENE1
ANTIPORTAL
TELEPORT
WAITMODECHANGE
MOVEMODECHANGE
CONTROLLERVIEW
PAWNVIEW
SPAWNPLAYERPAWN
DeletePlayer
SPAWNACTOR
SPAWNNPCS
SPAWNPCS
AUTOSPAWNPC
PlayerMove
DumpActor
SPAWNVEHICLES
SPAWNITEM
SPAWNEDPAWNMOVETO
STOPPAWNMOVING
DEFAULTCAMERA
FIXEDDEFAULTCAMERA
TURNBACK
MESHCHANGE
TEXTURECHANGE
DISTANCEFOG
DISTANCEFOGRANGE
PERSPECTIVE
GROUNDSPEEDUP
GROUNDSPEEDDOWN
CAMERAVIEWHEIGHTADJUST
ZOOMINHOLD
ZOOMOUTHOLD
ZOOMINPRESS
ZOOMOUTPRESS
SELECTINGCANCEL
TextCapture
Crash
PLAYERPAWNMOVETO
KEYBOARDBACKMOVESTART
KEYBOARDBACKMOVEFINISH
KEYBOARDMOVESTART
KEYBOARDMOVEFINISH
JOYSTICKMOVE
LEFTTURNINGSTART
LEFTTURNINGFINISH
RIGHTTURNINGSTART
RIGHTTURNINGFINISH
STEPMOVE
COMBOANIMPLAY
CHANGEANIM
CheckGrp
Addabnormal
deleteabnormal
Lodchange
fh
shake
Env Reload
SETTIME
SETTIMERATIO
CancelMAGICTEST
DeleteSelectedActor
pv
PawnViewer
nv
NpcViewer
sv
SkillViewer
Cast
SkillRemain
addcubic
decubic
cubicskill
ATTACKSPEEDDOWN
ATTACKSPEEDUP
setwyvern
SVS
BoneSim
ReduceLOD
KeepMinFrame
SkipAnim
Hitwater
SHADOW
DEFAULTSHADOW
RIDE
UNRIDE
ANIMPLAY
CAMERAPITCH
CAMERAYAW
YAWTURN
TRANSFER
BuildZone
LoadPath
Limit
C_TELEPORT
C_RMODE
GEODATA
SEAMLESS
MAPLOC
SHOWBORDERLINE
SHOWSECTORS
SaveMemInfo
CacheTexture
DUMPRESOURCEHASH
FIRSTCOLOREDMIP
NEARCLIP
D3DRESOURCES
SUPPORTEDRESOLUTION
ISFULLSCREEN
GETPING
INJECT
NETSPEED
LANSPEED
SHOWALL
REPORT
SHOT
SHOWACTORS
HIDEACTORS
RMODE
REND
SHOW
CINEMATICS
CINEMATICSRATIO
FIXEDVISIBILITY
TOGGLEREFRAST
EXEC
SHOWEXTENTLINECHECK
SHOWLINECHECK
SHOWPOINTCHECK
KDRAW
KSTEP
KSTOP
KSAFETIME
OPEN
START
SERVERTRAVEL
DISCONNECT
RECONNECT
EXIT
QUIT
GETCURRENTTICKRATE
GETMAXTICKRATE
GSPYLITE
SAVEGAME
CANCEL
SOUND_REBOOT
SHOWEXTENTLINECHECK
SHOWLINECHECK
SHOWPOINTCHECK
KDRAW
KSTEP
KSTOP
KSAFETIME
MEMSTAT
CONFIGHASH
EXIT
QUIT
RELAUNCH
DEBUG
DIR
MEM
DUMPNATIVES
GET
SET
OBJ
GTIME
DUMPCACHE
SHOWLOG
TakeFocus
EditDefault
EditActor
CopyToClipboard
HideLog
Preferences
BRIGHTNESS
CONTRAST
GAMMA
PAUSESOUNDS
UNPAUSESOUNDS
STOPSOUNDS
WEAPONRADIUS
ROLLOFF
GRAPH
L2Debug
L2DebugWindow
FLUSH
STAT
CRACKURL
PACKETCOUNTSTART
PACKETCOUNTSTOP
STOPMOUSE
MOVEMOUSE
ENDFULLSCREEN
TOGGLEFULLSCREEN
GETCURRENTRES
GETCURRENTCOLORDEPTH
GETCOLORDEPTHS
GETCURRENTRENDERDEVICE
SETRES
TEMPSETRES

Оказывается через клиент доступно гораздо больше команд: 222 из клиента, и только 57 - из консоли! Настало время посмотреть, что же они делают.

Проверяем найденные команды

В списке есть 2 команды телепорта - TELEPORT и C_TELEPORT, очевидно у них должны быть аргументы. Находим в IDA как они парсятся:

.text:104E667E                 mov     ecx, [ebp+arg_0]
.text:104E6681                 lea     eax, [ebp+var_164]
.text:104E6687                 push    eax
.text:104E6688                 push    offset asc_10791768 ; "X="
.text:104E668D                 push    ecx
.text:104E668E                 call    ds:?Parse@@YAHPBG0AAM@Z ; Parse(ushort const *,ushort const *,float &)
.text:104E6694                 add     esp, 0Ch
.text:104E6697                 cmp     eax, esi
.text:104E6699                 jz      loc_104E5DC0
.text:104E669F                 mov     eax, [ebp+arg_0]
.text:104E66A2                 lea     edx, [ebp+var_160]
.text:104E66A8                 push    edx
.text:104E66A9                 push    offset aY       ; "Y="
.text:104E66AE                 push    eax
.text:104E66AF                 call    ds:?Parse@@YAHPBG0AAM@Z ; Parse(ushort const *,ushort const *,float &)
.text:104E66B5                 add     esp, 0Ch
.text:104E66B8                 cmp     eax, esi
.text:104E66BA                 jz      loc_104E5DC0
.text:104E66C0                 mov     edx, [ebp+arg_0]
.text:104E66C3                 lea     ecx, [ebp+var_15C]
.text:104E66C9                 push    ecx
.text:104E66CA                 push    offset aZ       ; "Z="
.text:104E66CF                 push    edx
.text:104E66D0                 call    ds:?Parse@@YAHPBG0AAM@Z ; Parse(ushort const *,ushort const *,float &)

В принципе это было очевидно - аргументы для телепорта: X=<float> Y=<float> Z=<float>, обе команды работают идентично. Разумеется, нормальный сервер не даст игроку просто так телепортироваться куда угодно - сразу откинет назад, однако на сервере, который не проверяет координаты - все работает.

SHOWLOG, HideLog - открывает и закрывает окно консоли. Альтернатива аргументу /LOG.

Preferences - открывает вот такие настройки:

Preferences открывает гораздо более богатые настройки, чем те, которые доступны в клиенте.
Preferences открывает гораздо более богатые настройки, чем те, которые доступны в клиенте.

SPAWNNPCS Num=<int> - спавнит указанное число рандомных мобов вокруг персонажа:

Результат команды SPAWNNPCS
Результат команды SPAWNNPCS

SPAWNPCS Num=<int> - аналогично предыдущей, но спавнит игроков в рандомном шмоте:

Результат команды SPAWNPCS
Результат команды SPAWNPCS

RIDE TYPE=<int> - 0 - садимся на страйдера, 1 - на виверну, 2 - на виверну - альбиноса

А вы знали, что страйдеры были уже в C1?
А вы знали, что страйдеры были уже в C1?
И неправильные виверны
И неправильные виверны

Правда работает кривовато - после этого клиент практически зависает. Забегая вперед скажу, что маунты есть и в сетевом протоколе, и оно работает!

RMODE, C_RMODE <int> - изменяет режим рендеринга, например на такой

RMODE 1 - можно видеть сквозь стены
RMODE 1 - можно видеть сквозь стены

или такой:

RMODE 7 - текстуры не завезли
RMODE 7 - текстуры не завезли

SHOWBORDERLINE, SHOWSECTORS - включает это:

SHOWBORDERLINE, SHOWSECTORS - вероятно, оно зачем-то нужно
SHOWBORDERLINE, SHOWSECTORS - вероятно, оно зачем-то нужно

SEAMLESS ON/OFF - включает/выключает подгрузку соседних фрагментов карты. Если выключить - соседние фрагменты не будут подгружаться, можно дойти до края земли и упасть:

Земля не только плоская, но и квадратная
Земля не только плоская, но и квадратная

MAPLOC - выводит в консоль X Y текущего фрагмента карты, например MapX=21, MapY=19 - эльфятник.

GEODATA - пытается грузить файл по пути формата: .\GeoData%d_%d_conv.dat, где числа вместо %d соответствуют координатам MAPLOC. Очевидно, это серверная геодата. Дадим клиенту то, что он хочет - скопируем геодату в System\GeoData. Теперь мы можем взглянуть на мир LA2 глазами сервера:

Серверная геодата
Серверная геодата

Видно, что "разрешение" тут гораздо ниже, чем в клиенте - мир состоит из клеток примерно с ширину персонажа, а если точнее - 16x16 координатных единиц (которые отображаются по /loc). Зеленые стрелочки показывают проходимые направления, а красные - непроходимые. А если участок 8x8 клеток не содержит больших перепадов высот и непроходимых направлений - он объединяется в одну большую клетку:

Разные типы геодаты
Разные типы геодаты

Заключение

Мы познакомились лишь с частью скрытого в клиенте LA2 функционала, часть из которого очевидно была предназначена для тестирования и разработки игры, однако могла пригодиться и игрокам. Однако в клиенте есть ещё кое-что очень интересное. В следующей статье мы выясним, что же делает команда BuildZone.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
В какую самую раннюю версию Lineage 2 вы играли?
2.71% Prelude13
12.71% C161
5.42% C226
10.83% C352
24.38% C4117
0.63% C53
9.79% Interlude47
5.42% Версия с камаэлями (любая)26
0.83% Версия с сильфами или современная ей (любая)4
27.29% Ни в какую не играл131
Проголосовали 480 пользователей. Воздержался 31 пользователь.
Теги:
Хабы:
+63
Комментарии41

Публикации

Истории

Работа

Ближайшие события

AdIndex City Conference 2024
Дата26 июня
Время09:30
Место
Москва
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область