PDA

Просмотр полной версии : Ворклог реверса одной мобильной игры


testbot
03.05.2020, 01:27
Часть 1- логин сервер
Как-то раз в далеком 2017 году понадобилось мне разобрать одну популярную мобильную игру того времени- Зитву Бамков. Я уверен, многим из вас приходилось играть в нее, когда она была на пике популярности. На данный момент игра стала умирать. Разработчики отдают внутриигровые ресурсы за копеечный донат. По просьбе знакомого, и из-за обесценивания этой информации, публикую обзор полного взлома игры: от реверса, до создания альтернативного сервера и накрутки валюты гильдии, на которую даже сделали обзор некоторые ютуберы (да-да, Князь, привет). За это время у меня сохранились не все материалы, которые стоило бы использовать в статье. Поэтому в части скриншотов будет показана старая версия игры, а в части- новая. Однако мой сервер все еще работает с новой версией игры. Значит, можно надеяться, что обновления обратно совместимы

Изначально разбор игры я прототипировал на питоне, итоговая версия написана на котлине. Примеры кода буду брать из итоговой версии, благо, котлин очень интуитивный, и наглядный. Интересующиеся могут самостоятельно перенести код на свой любимый яп.

0. Анализ стека технологий, и устройства игры
Базовый этап, дающий представление о том, с чем придется иметь дело дальше

Легко заметить, что приложение написано на cocos2dx.
Например, если посмотреть smali код CastleClashActivity, можно заметить

Код:



.method public constructor ()V
.locals 1

.line 50
invoke-direct {p0}, Lorg/cocos2dx/lib/Cocos2dxActivity;->()V

const/4 v0, 0x0

.line 67
iput-boolean v0, p0, Lcom/igg/castleclash/CastleClashActivity;->isOfflineBack:Z

.line 306
iput-boolean v0, p0, Lcom/igg/castleclash/CastleClashActivity;->isChangeAccount:Z

return-void
.end method


что оно наследует Cocos2dxActivity.

Cocos2dx это c++ форк игрового движка cocos2d, написанного на питоне
Если еще немного посмотреть smali код, то можно заметить, что он исключительно определяет не очень интересные функции-хелперы, и через JNI передает управление библиотеке libgame.so. Из чего можно сделать вывод, что самое интересное происходит в нативе

1. Анализ структуры траффика
Самый очевидный ход перед погружением в реверс натива. Может дать представление об устройстве механизма работы с сервером. Для этой цели буду использовать андроид приложение packet capture. Оно удобно тем, что позволяет записывать трафик только нужного приложения.

https://forum.antichat.xyz/attachments/4880895/1588454242742.png

https://forum.antichat.xyz/attachments/4880895/1588454293032.png

https://forum.antichat.xyz/attachments/4880895/1588515533097.png

Что мы видим:

Клиент взаимодействует с двумя серверами. Вначале стучится к логин серверу (порт 9300), у которого получает данные игрового сервера. Потом- собственно к игровому (порт 9339)

Приложение общается с сервером через tcp сокет. Причем почему-то без ssl.

Трафик клиента вначале идет в открытом виде, а потом начинает шифроваться. Трафик сервера всегда не зашифрован (видно по кускам строк).

Используется little endian
Если внимательно посмотреть на данные, то можно угадать структуру пакетов. Вначале идут два short'a: размер пакета, и непонятная константа. Эта константа- идентификатор типа пакета, в зависимости от которого выбирается обработчик.

2. Реверс
Нужен для анализа игровой логики

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

За взаимодействие с сервером тут отвечает класс NetMessage. Вот его методы слева направо:
https://forum.antichat.xyz/attachments/4880895/1588440295163.png

За шифрование пакетов, очевидно, отвечают методы EncryptMsg, и DecryptMsg.
Посмотрим на EncryptMessage

https://forum.antichat.xyz/attachments/4880895/1588443657033.png

Он использует метод ELangh класса Langh- набора криптографических утилит, который используется как синглтон. Всё приложение работает с одним его экземпляром
Внутри творится криптографическая магия. Однако прогуглив интересные методы Langh, я нашел библиотеку, которая, похоже, была перепилина в этот класс- http://read.pudn.com/downloads190/sourcecode/crypt/891222/DES Tool/yxyDES2.cpp__.htm .
Вот метод EncryptData из этой библиотеки, и кусок ELangh из игры. Почти одно и то же

https://forum.antichat.xyz/attachments/4880895/1588446074734.png

https://forum.antichat.xyz/attachments/4880895/1588444435111.png

Значит, игра использует des для шифрования. Но, как мы знаем, des- блочный шифр. Какой паддинг используется для данных, размер которых не кратен 8 байтам?
Как оказалось, никакого паддинга там и нет. Китайцы используют гениальное решение- шифруют packet_data[acket_data_size // 8] (в терминах питона), и дописывают в конце packet_data[packet_data_size // 8:] в исходном виде.
По-моему это очень странно. Оставим это дело на их совести

Для создания декодера пакетов осталась всего одна деталь- ключ des. Поискав по xref'ам метода Langh::InitializeK, я быстро нашел установку ключа

https://forum.antichat.xyz/attachments/4880895/1588446891114.png

Им оказалась строка "L*#)@!&8".

У нас есть все для создания прокси-сервера, который будет декодировать, и логировать пакеты

3. Пишем прокси-сервер
Для начала нам понадобится менеджер шифрования, который мы будем использовать для дешифрования тел пакетов от клиента. На котлине он выглядит так

https://forum.antichat.xyz/attachments/4880895/1588449898472.png

Описываем интерфейс пакета

https://forum.antichat.xyz/attachments/4880895/1588450087330.png

И три сущности: ClientPacket, EncryptedClientPacket, ServerPacket, Packet(для унификации механизма чтения и отправки пакетов. Кастится к EnctyptedClientPacket, и ServerPacket)
Делаем чтение, и отправку

https://forum.antichat.xyz/attachments/4880895/1588493154475.png

Прокси логин-сервер будет состоять из двух корутин
Первая получает пакет у клиента, дешифрует (превращает EncryptedClientPacket в ClientPacket), печатает нам его содержимое и отправляет на сервер
Вторая получает пакет у сервера, печатает содержимое, и отправляет клиенту

Итоговый код прокси логин-сервера у меня выглядит так:

Код:



class LoginServer(serverIp: String = "0.0.0.0", val loginServerIp: String) :
TcpServer(serverIp, LOGIN_SERVER_PORT) {

var serverProcessors = listOf(LoginServerServerPacketProcessor())
var clientProcessors = listOf(LoginServerClientPacketProcessor())

override suspend fun processClient(iggChannel: IGGChannel) {
val loginSocket = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp( )
.connect(InetSocketAddress(loginServerIp, LOGIN_SERVER_PORT))
val loginChannel = IGGChannel(loginSocket)

var stop = false

val gameSession = GameSession()

coroutineScope {
launch {
while (!stop) {
try {
var serverPacket: ServerPacket? = ServerPacket(loginChannel.readPacket())
println("Login server: $serverPacket")

val packetType = serverPacket!!.type

val processor = serverProcessors.firstOrNull {
it.packetType == serverPacket!!.type
}

if (processor != null) {
val newPacket = processor.process(serverPacket, gameSession)
if (serverPacket.smartBuffer.unpackedItemsInfo.isNotE mpty()) {
println("Login server: ${serverPacket.toSplitHexString()}")
println("Login server: ${serverPacket.toPacketContentString()}")
}
serverPacket = newPacket
}

if (serverPacket == null) // Processor has drop the packet
println("Login server: Packet was dropped- $packetType")
else
iggChannel.sendPacket(serverPacket.asPacket())

} catch (e: Exception) {
stop = true
iggChannel.close()
}
}

}

launch {
while (!stop) {
try {
val encryptedClientPacket = EncryptedClientPacket(iggChannel.readPacket(), gameSession.isEncryptionEnabled)

var clientPacket: ClientPacket? = ClientPacket(encryptedClientPacket)
println("Login server: $clientPacket")
val packetType = clientPacket!!.type

val processor = clientProcessors.firstOrNull {
it.packetType == clientPacket!!.type
}

if (processor != null) {
val newPacket = processor.process(clientPacket, gameSession)
if (clientPacket.smartBuffer.unpackedItemsInfo.isNotE mpty()) {
println("Login server: ${clientPacket.toSplitHexString()}")
println("Login server: ${clientPacket.toPacketContentString()}")
}
clientPacket = newPacket
}

if (clientPacket == null) {
println("Packet was dropped: $packetType")
} else {
loginChannel.sendPacket(EncryptedClientPacket(clie ntPacket, gameSession.clientPacketSerialNum?.inc()).asPacket ())
}
} catch (e: Exception) {
stop = true
loginChannel.close()
}
}
}
}

println("Disconnected")
}


Адрес логин сервера клиент получает из конфига (http://config.igg.com/appconf/1030059902/server_config), адрес которого лежит в assets/config.xml
Деплоим свой конфиг, скопировав содержимое оригинального, c нужным адресом в секции LoginServer. Меняем адрес конфига в assets/config.xml на наш. Пересобираем приложение
В расшифрованных телах пакетов клиента в первых четырех байтах идет инкрементирующееся чисто- порядковый id отправленного пакета. К сожалению, архитектура у меня не позволяет выводить полное содержимое пакета, ибо при инжекте пакета старый порядковый id будет все ломать. Поэтому я не смог показать полные данные пакета с ним. Вот вывод моего реверс прокси. Как видите, у меня организована авторазметка пакетов

https://forum.antichat.xyz/attachments/4880895/1588453839358.png

При подключении клиент сервер отправляет igg id, токен, и версию игры
В ответ логин сервер отправляет данные игрового сервера, и игровой токен

Qx_ Bit
03.05.2020, 07:32
Прикольно, можно попробовать)

wooolff
05.05.2020, 01:04
я немного потерялся, ты пишешь про exe, java, более конкретней можешь.
- исследуем такое то приложение, столько то файлов, такие и такие я исследую тем, а такие темто и т.д....
прикольно, но получилось как фильм от первого лица когда главный герой снимает камерой, а все остальное додумуй сам)))))))))))
а так от меня лайк)))

mrtyrel
10.05.2020, 21:19
Я 0 в реверсе но статью прочел полностью очень интересно!