Часть 1- логин сервер
Как-то раз в далеком 2017 году понадобилось мне разобрать одну популярную мобильную игру того времени- Зитву Бамков. Я уверен, многим из вас приходилось играть в нее, когда она была на пике популярности. На данный момент игра стала умирать. Разработчики отдают внутриигровые ресурсы за копеечный донат. По просьбе знакомого, и из-за обесценивания этой информации, публикую обзор полного взлома игры: от реверса, до создания альтернативного сервера и накрутки валюты гильдии, на которую даже сделали обзор некоторые ютуберы (да-да, Князь, привет). За это время у меня сохранились не все материалы, которые стоило бы использовать в статье. Поэтому в части скриншотов будет показана старая версия игры, а в части- новая. Однако мой сервер все еще работает с новой версией игры. Значит, можно надеяться, что обновления обратно совместимы
Изначально разбор игры я прототипировал на питоне, итоговая версия написана на котлине. Примеры кода буду брать из итоговой версии, благо, котлин очень интуитивный, и наглядный. Интересующиеся могут самостоятельно перенести код на свой любимый яп.
0. Анализ стека технологий, и устройства игры
Базовый этап, дающий представление о том, с чем придется иметь дело дальше
Легко заметить, что приложение написано на cocos2dx.
Например, если посмотреть smali код CastleClashActivity, можно заметить
что оно наследует Cocos2dxActivity.
Cocos2dx это c++ форк игрового движка cocos2d, написанного на питоне
Если еще немного посмотреть smali код, то можно заметить, что он исключительно определяет не очень интересные функции-хелперы, и через JNI передает управление библиотеке libgame.so. Из чего можно сделать вывод, что самое интересное происходит в нативе
1. Анализ структуры траффика
Самый очевидный ход перед погружением в реверс натива. Может дать представление об устройстве механизма работы с сервером. Для этой цели буду использовать андроид приложение packet capture. Оно удобно тем, что позволяет записывать трафик только нужного приложения.
Что мы видим:
2. Реверс
Нужен для анализа игровой логики
Первым делом определим, как устроено взаимодействие с сервером на самом деле, и каким образом шифруются данные на клиенте
За взаимодействие с сервером тут отвечает класс NetMessage. Вот его методы слева направо:
За шифрование пакетов, очевидно, отвечают методы EncryptMsg, и DecryptMsg.
Посмотрим на EncryptMessage
Он использует метод ELangh класса Langh- набора криптографических утилит, который используется как синглтон. Всё приложение работает с одним его экземпляром
Внутри творится криптографическая магия. Однако прогуглив интересные методы Langh, я нашел библиотеку, которая, похоже, была перепилина в этот класс-
Вот метод EncryptData из этой библиотеки, и кусок ELangh из игры. Почти одно и то же
Значит, игра использует des для шифрования. Но, как мы знаем, des- блочный шифр. Какой паддинг используется для данных, размер которых не кратен 8 байтам?
Как оказалось, никакого паддинга там и нет. Китайцы используют гениальное решение- шифруют packet_data[acket_data_size // 8] (в терминах питона), и дописывают в конце packet_data[packet_data_size // 8:] в исходном виде.
По-моему это очень странно. Оставим это дело на их совести
Для создания декодера пакетов осталась всего одна деталь- ключ des. Поискав по xref'ам метода Langh::InitializeK, я быстро нашел установку ключа
Им оказалась строка "L*#)@!&8".
У нас есть все для создания прокси-сервера, который будет декодировать, и логировать пакеты
3. Пишем прокси-сервер
Для начала нам понадобится менеджер шифрования, который мы будем использовать для дешифрования тел пакетов от клиента. На котлине он выглядит так
Описываем интерфейс пакета
И три сущности: ClientPacket, EncryptedClientPacket, ServerPacket, Packet(для унификации механизма чтения и отправки пакетов. Кастится к EnctyptedClientPacket, и ServerPacket)
Делаем чтение, и отправку
Прокси логин-сервер будет состоять из двух корутин
Первая получает пакет у клиента, дешифрует (превращает EncryptedClientPacket в ClientPacket), печатает нам его содержимое и отправляет на сервер
Вторая получает пакет у сервера, печатает содержимое, и отправляет клиенту
Итоговый код прокси логин-сервера у меня выглядит так:
Адрес логин сервера клиент получает из конфига (
Деплоим свой конфиг, скопировав содержимое оригинального, c нужным адресом в секции LoginServer. Меняем адрес конфига в assets/config.xml на наш. Пересобираем приложение
В расшифрованных телах пакетов клиента в первых четырех байтах идет инкрементирующееся чисто- порядковый id отправленного пакета. К сожалению, архитектура у меня не позволяет выводить полное содержимое пакета, ибо при инжекте пакета старый порядковый id будет все ломать. Поэтому я не смог показать полные данные пакета с ним. Вот вывод моего реверс прокси. Как видите, у меня организована авторазметка пакетов
При подключении клиент сервер отправляет igg id, токен, и версию игры
В ответ логин сервер отправляет данные игрового сервера, и игровой токен
Как-то раз в далеком 2017 году понадобилось мне разобрать одну популярную мобильную игру того времени- Зитву Бамков. Я уверен, многим из вас приходилось играть в нее, когда она была на пике популярности. На данный момент игра стала умирать. Разработчики отдают внутриигровые ресурсы за копеечный донат. По просьбе знакомого, и из-за обесценивания этой информации, публикую обзор полного взлома игры: от реверса, до создания альтернативного сервера и накрутки валюты гильдии, на которую даже сделали обзор некоторые ютуберы (да-да, Князь, привет). За это время у меня сохранились не все материалы, которые стоило бы использовать в статье. Поэтому в части скриншотов будет показана старая версия игры, а в части- новая. Однако мой сервер все еще работает с новой версией игры. Значит, можно надеяться, что обновления обратно совместимы
Изначально разбор игры я прототипировал на питоне, итоговая версия написана на котлине. Примеры кода буду брать из итоговой версии, благо, котлин очень интуитивный, и наглядный. Интересующиеся могут самостоятельно перенести код на свой любимый яп.
0. Анализ стека технологий, и устройства игры
Базовый этап, дающий представление о том, с чем придется иметь дело дальше
Легко заметить, что приложение написано на cocos2dx.
Например, если посмотреть smali код CastleClashActivity, можно заметить
Код:
.method public constructor <init>()V
.locals 1
.line 50
invoke-direct {p0}, Lorg/cocos2dx/lib/Cocos2dxActivity;-><init>()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
Cocos2dx это c++ форк игрового движка cocos2d, написанного на питоне
Если еще немного посмотреть smali код, то можно заметить, что он исключительно определяет не очень интересные функции-хелперы, и через JNI передает управление библиотеке libgame.so. Из чего можно сделать вывод, что самое интересное происходит в нативе
1. Анализ структуры траффика
Самый очевидный ход перед погружением в реверс натива. Может дать представление об устройстве механизма работы с сервером. Для этой цели буду использовать андроид приложение packet capture. Оно удобно тем, что позволяет записывать трафик только нужного приложения.
Что мы видим:
- Клиент взаимодействует с двумя серверами. Вначале стучится к логин серверу (порт 9300), у которого получает данные игрового сервера. Потом- собственно к игровому (порт 9339)
- Приложение общается с сервером через tcp сокет. Причем почему-то без ssl.
- Трафик клиента вначале идет в открытом виде, а потом начинает шифроваться. Трафик сервера всегда не зашифрован (видно по кускам строк).
- Используется little endian
2. Реверс
Нужен для анализа игровой логики
Первым делом определим, как устроено взаимодействие с сервером на самом деле, и каким образом шифруются данные на клиенте
За взаимодействие с сервером тут отвечает класс NetMessage. Вот его методы слева направо:
За шифрование пакетов, очевидно, отвечают методы EncryptMsg, и DecryptMsg.
Посмотрим на EncryptMessage
Он использует метод ELangh класса Langh- набора криптографических утилит, который используется как синглтон. Всё приложение работает с одним его экземпляром
Внутри творится криптографическая магия. Однако прогуглив интересные методы Langh, я нашел библиотеку, которая, похоже, была перепилина в этот класс-
Ссылка скрыта от гостей
.Вот метод EncryptData из этой библиотеки, и кусок ELangh из игры. Почти одно и то же
Значит, игра использует des для шифрования. Но, как мы знаем, des- блочный шифр. Какой паддинг используется для данных, размер которых не кратен 8 байтам?
Как оказалось, никакого паддинга там и нет. Китайцы используют гениальное решение- шифруют packet_data[acket_data_size // 8] (в терминах питона), и дописывают в конце packet_data[packet_data_size // 8:] в исходном виде.
По-моему это очень странно. Оставим это дело на их совести
Для создания декодера пакетов осталась всего одна деталь- ключ des. Поискав по xref'ам метода Langh::InitializeK, я быстро нашел установку ключа
Им оказалась строка "L*#)@!&8".
У нас есть все для создания прокси-сервера, который будет декодировать, и логировать пакеты
3. Пишем прокси-сервер
Для начала нам понадобится менеджер шифрования, который мы будем использовать для дешифрования тел пакетов от клиента. На котлине он выглядит так
Описываем интерфейс пакета
И три сущности: ClientPacket, EncryptedClientPacket, ServerPacket, Packet(для унификации механизма чтения и отправки пакетов. Кастится к EnctyptedClientPacket, и ServerPacket)
Делаем чтение, и отправку
Прокси логин-сервер будет состоять из двух корутин
Первая получает пакет у клиента, дешифрует (превращает 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.isNotEmpty()) {
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.isNotEmpty()) {
println("Login server: ${clientPacket.toSplitHexString()}")
println("Login server: ${clientPacket.toPacketContentString()}")
}
clientPacket = newPacket
}
if (clientPacket == null) {
println("Packet was dropped: $packetType")
} else {
loginChannel.sendPacket(EncryptedClientPacket(clientPacket, gameSession.clientPacketSerialNum?.inc()).asPacket())
}
} catch (e: Exception) {
stop = true
loginChannel.close()
}
}
}
}
println("Disconnected")
}
Ссылка скрыта от гостей
), адрес которого лежит в assets/config.xmlДеплоим свой конфиг, скопировав содержимое оригинального, c нужным адресом в секции LoginServer. Меняем адрес конфига в assets/config.xml на наш. Пересобираем приложение
В расшифрованных телах пакетов клиента в первых четырех байтах идет инкрементирующееся чисто- порядковый id отправленного пакета. К сожалению, архитектура у меня не позволяет выводить полное содержимое пакета, ибо при инжекте пакета старый порядковый id будет все ломать. Поэтому я не смог показать полные данные пакета с ним. Вот вывод моего реверс прокси. Как видите, у меня организована авторазметка пакетов
При подключении клиент сервер отправляет igg id, токен, и версию игры
В ответ логин сервер отправляет данные игрового сервера, и игровой токен
Последнее редактирование: