• 15 апреля стартует «Курс «SQL-injection Master» ©» от команды The Codeby

    За 3 месяца вы пройдете путь от начальных навыков работы с SQL-запросами к базам данных до продвинутых техник. Научитесь находить уязвимости связанные с базами данных, и внедрять произвольный SQL-код в уязвимые приложения.

    На последнюю неделю приходится экзамен, где нужно будет показать свои навыки, взломав ряд уязвимых учебных сайтов, и добыть флаги. Успешно сдавшие экзамен получат сертификат.

    Запись на курс до 25 апреля. Получить промодоступ ...

Статья Ворклог реверса одной мобильной игры

Часть 1- логин сервер
Как-то раз в далеком 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
что оно наследует Cocos2dxActivity.

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

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

1588454293032.png

1588515533097.png

Что мы видим:
  • Клиент взаимодействует с двумя серверами. Вначале стучится к логин серверу (порт 9300), у которого получает данные игрового сервера. Потом- собственно к игровому (порт 9339)
  • Приложение общается с сервером через tcp сокет. Причем почему-то без ssl.
  • Трафик клиента вначале идет в открытом виде, а потом начинает шифроваться. Трафик сервера всегда не зашифрован (видно по кускам строк).
  • Используется little endian
Если внимательно посмотреть на данные, то можно угадать структуру пакетов. Вначале идут два short'a: размер пакета, и непонятная константа. Эта константа- идентификатор типа пакета, в зависимости от которого выбирается обработчик.

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

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

За взаимодействие с сервером тут отвечает класс NetMessage. Вот его методы слева направо:
1588440295163.png



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


Он использует метод ELangh класса Langh- набора криптографических утилит, который используется как синглтон. Всё приложение работает с одним его экземпляром
Внутри творится криптографическая магия. Однако прогуглив интересные методы Langh, я нашел библиотеку, которая, похоже, была перепилина в этот класс- .
Вот метод EncryptData из этой библиотеки, и кусок ELangh из игры. Почти одно и то же
1588446074734.png
1588444435111.png


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

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


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

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

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

1588449898472.png


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

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

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.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 будет все ломать. Поэтому я не смог показать полные данные пакета с ним. Вот вывод моего реверс прокси. Как видите, у меня организована авторазметка пакетов
1588453839358.png

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

wooolff

Green Team
19.02.2017
233
36
BIT
0
я немного потерялся, ты пишешь про exe, java, более конкретней можешь.
- исследуем такое то приложение, столько то файлов, такие и такие я исследую тем, а такие темто и т.д....
прикольно, но получилось как фильм от первого лица когда главный герой снимает камерой, а все остальное додумуй сам)))))))))))
а так от меня лайк)))
 
  • Нравится
Реакции: cryptomv3

mrtyrel

Green Team
01.12.2018
80
45
BIT
0
Я 0 в реверсе но статью прочел полностью очень интересно!
 
  • Нравится
Реакции: domebr
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!