Райтап на задачу «Вернуть 2007й»

8 ноября 2026
Вернуть 2007й
Категория:

Rev

Задача «Вернуть 2007й»

LastTeamLead

Автор:

LastTeamLead

Первая кровь:
Fire-K
Сложность:
Medium
Дата публикации:
25.04.2026
Решений:
38
Рейтинг:
5/5
Ссылка:
Решать

В целом необязательно запускать приложение, можно решить чисто статически.

Для начала необходимо установить приложение для реверса Java-приложений.

Отлично подойдёт jadx, скачаем и установим следующим образом:

curl -L https://github.com/skylot/jadx/releases/download/v1.5.6/jadx-1.5.6.zip -o /tmp/jadx.zip
mkdir jadx
cd jadx
unzip -q /tmp/jadx.zip -d ~/tools/jadx
echo 'export PATH="$HOME/tools/jadx/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
jadx --version

Если всё установилось хорошо, будет выведена версия — 1.5.6.

Откроем jadx через:

jadx-gui

Теперь можно создать проект и закинуть данный в условии .jar-файл.

Получаем мно-о-о-ого исходного Java-кода.

Примерно разберёмся с логикой работы приложения.

Сначала у нас запрашивают четырёхзначный PIN-код, затем можно будет вводить строку и проверять её.

Сначала скопируем весь код, полученный в jadx, в отдельный файл — flagchecher.java.

Возвращаясь к PIN-коду. В ходе анализа найдём класс CheckerCanvas, где есть метод drawPinHint. Он рисует подсказку по двум массивам координат:

private void drawPinHint(Graphics graphics, int i, int i2) {
    graphics.setColor(PIN_HINT_INACTIVE_COLOR);
    drawHintPoints(graphics, i, i2, this.hintOffPoints);
    graphics.setColor(PIN_HINT_ACTIVE_COLOR);
    drawHintPoints(graphics, i, i2, this.hintOnPoints);
    graphics.setColor(SCREEN_PIN);
}

Таким образом, мы можем спарсить изображение и узнать верный PIN. Вспомним, как работать с регулярными выражениями в Python — напишем парсер:

import re

with open("flagchecher.java") as f:
    source = f.read()

data = re.search(
    r"hintOnPoints = new short\[\]\{(.*?)\};", source, re.S
).group(1)
numbers = list(map(int, re.findall(r"\d+", data)))
points = set(zip(numbers[::2], numbers[1::2]))

for y in range(max(numbers[1::2]) + 1):
    print("".join("#" if (x, y) in points else " " for x in range(91)))

Запускаем и получаем следующее:

PIN-код 2007, восстановленный из координат подсказки

Теперь мы знаем, что PIN — 2007.

После ввода PIN метод finishPinEntry преобразует его в целое число и вычисляет ключ:

this.pinCode = iCharAt;
this.pinKey = this.vm.derivePinKey(this.pinCode);

Для PIN 2007 результат derivePinKey равен 0x3f4be403. Функция заполняет массив из 16384 целых чисел, выполняет 8388608 итераций перемешивания, а затем сворачивает массив в 32-битное значение.

Проверка находится в StageVM.verify. Вместо хранения флага в открытом виде приложение хранит зашифрованный массив ENC_STAGE. Метод decryptStage восстанавливает инструкции побайтовым XOR:

bArr[i] = (byte) ((ENC_STAGE[i] & 255) ^
    ((((STATIC_KEY[(i + 5) % STATIC_KEY.length] & 255)
      + ((i * 29) & 255)) + 55) & 255));

Статический ключ в ASCII — dynaVM42. После расшифровки verify проверяет заголовок BG2\x06 и контрольную сумму, затем интерпретирует инструкции. Основные коды операций:

Код  Операция  Назначение
0x10  OP_REQUIRE_UNLOCK  Проверка вычисленного по PIN ключа
0x11  OP_LEN_EQ  Проверка длины строки
0x12  OP_CONTAINS  Пропуск указанного количества байтов
0x30  OP_CHAR_DYN  Проверка одного символа
0x7f  OP_END  Завершение проверки

Для нашего PIN проверка разблокировки проходит: вычисленное значение и операнд инструкции равны 0x921b. Следующая важная инструкция задаёт длину флага — 30 символов.

После расшифровки массива stage его можно вывести в шестнадцатеричном виде и разобрать по формату инструкций. Для этой задачи начало расшифрованного массива выглядит так:

42 47 32 06       заголовок BG2\x06
10 92 1b          OP_REQUIRE_UNLOCK, expected = 0x921b
11 1e             OP_LEN_EQ, длина = 30
12 00             OP_CONTAINS, пропустить 0 байт
30 0a 87 78 23    OP_CHAR_DYN, index=10, salt=135, target=0x7823
30 05 92 39 7f    OP_CHAR_DYN, index=5,  salt=146, target=0x397f
30 1d b2 50 92    OP_CHAR_DYN, index=29, salt=178, target=0x5092
...
7f                OP_END

Формат OP_CHAR_DYN виден прямо в verify:

int i14 = i9 + 1;
int i15 = bArrDecryptStage[i9] & 255;       // salt
int i16 = i14 + 1;
int i17 = bArrDecryptStage[i14] & 255;      // index
int u17 = readU16(bArrDecryptStage, i16);   // target
i5 = i16 + 2;

Иными словами, после байта 0x30 идут четыре байта: salt, index и 16-битное значение target. Например, последовательность 30 0a 87 78 23 означает: проверить символ в позиции 10, используя salt=135 и значение target=0x7823.

Проверки выполняются в порядке инструкций, а индексы при этом перемешаны. Например, первыми проверяются позиции 10, 5 и 29. Поэтому найденные символы записываются в flag[index], а состояние chain обновляется именно после успешного нахождения очередного символа.

Для каждого символа приложение перебирает кандидатов из алфавита:

this.alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_".toCharArray();

То есть брутится не вся строка и не все возможные 30-символьные флаги. На каждой итерации перебирается только один символ, 65 вариантов, и для каждого варианта запускается та же функция перемешивания, что и в приложении. Когда результат совпадает с target, кандидат считается найденным. После этого переходят к следующей инструкции OP_CHAR_DYN.

Для каждого кандидата приложение:

  1. Обновляет внутреннее состояние на основе индекса, salt, PIN-ключа и предыдущего состояния цепочки.
  2. Передаёт символ и состояния в slowCharMix. Эта функция выполняет более 1500 раундов перемешивания.
  3. Сравнивает 16-битную свёртку результата с целевым значением из инструкции.
  4. При успехе обновляет состояние цепочки, которое понадобится следующей инструкции.

Формула сравнения находится в конце обработки OP_CHAR_DYN:

int iSlowCharMix = slowCharMix(
    iCharAt, i15, i17, i, i2, iXorshift32, iXorshift33
);

if (((iSlowCharMix ^ (iSlowCharMix >>> 16)) & 65535)
        == (rol16(
                u17 ^ (
                    ((i17 * 145) + (i15 * 311)
                    + ((iXorshift33 >>> 11) & 65535)
                    + (i2 & 65535)) & 65535
                ),
                ((i + i17 + i15 + (iXorshift33 & 7)) & 7) + 1
            )
            + ((i * 29) + (i17 * 41) + (i15 * 17)
               + ((iXorshift33 >>> 3) & 65535)) & 65535)) {
}

Левая часть зависит от предполагаемого символа через slowCharMix. Правая часть зависит только от текущего состояния VM и констант инструкции, поэтому для одной инструкции её можно вычислить заранее. В коротком скрипте это выражено так:

target = encoded ^ ((salt * 145 + pos * 311 + (chain >> 11)
                     + key) & 65535)
shift = (PIN + salt + pos + (chain & 7) & 7) + 1
target = ((target << shift) | (target >> (16 - shift))) & 65535
target = (target + PIN * 29 + salt * 41 + pos * 17
          + (chain >> 3)) & 65535

Затем для каждого кандидата из ALPHABET вычисляется левая часть и выполняется сравнение:

for char in ALPHABET:
    value = mix(ord(char), pos, salt, key, state, chain)
    if (value ^ value >> 16) & 65535 == target:
        flag[pos] = char
        chain = xs(chain ^ value ^ ord(char) * 16843009 ^ pos * 40503)
        break

Здесь mix — это прямой перенос Java-метода slowCharMix, а обновление chain взято из следующих строк verify:

iXorshift33 = BugCheckerMidlet.xorshift32(
    ((iXorshift33 ^ iSlowCharMix)
    ^ (iCharAt * R.attr.cacheColorHint))
    ^ (i15 * 40503)
);

Каждая инструкция OP_CHAR_DYN содержит индекс символа, байт salt и двухбайтовое целевое значение. Проверки выполняются в порядке инструкций, а индексы при этом перемешаны. Например, первыми проверяются позиции 10, 5 и 29.

Для каждого символа приложение:

  1. Обновляет внутреннее состояние на основе индекса, salt, PIN-ключа и предыдущего состояния цепочки.
  2. Передаёт символ и состояния в slowCharMix. Эта функция выполняет более 1500 раундов перемешивания.
  3. Сравнивает 16-битную свёртку результата с целевым значением из инструкции.
  4. При успехе обновляет состояние цепочки, которое понадобится следующей инструкции.

Расшифровки ENC_STAGE уже известны:

  • PIN — 2007;
  • длина флага — 30;
  • порядок проверок символов;
  • тройки (позиция, salt, encoded) для всех OP_CHAR_DYN.

Теперь мы разобрались с логикой и можем налить кофе, подготовиться лицезреть прекрасные арифметические операции с не менее прекрасными числами в ещё более прекрасном количестве и написать скрипт на Python, вычисляющий флаг:

import re

with open("flagchecher.java") as f:
    source = f.read()

encrypted = re.search(r"ENC_STAGE = \{(.*?)\};", source).group(1)
constants = {"OP_LEN_EQ": 17, "OP_REQUIRE_UNLOCK": 16, "OP_CHAR_DYN": 48}
values = [
    constants[item.strip()] if item.strip() in constants else int(item)
    for item in encrypted.split(",")
]

static_key = b"dynaVM42"
stage = bytes(
    (value & 255) ^ ((static_key[(i + 5) % 8] + i * 29 + 55) & 255)
    for i, value in enumerate(values)
)

MASK = 0xFFFFFFFF


def xorshift32(value):
    value &= MASK
    value ^= (value << 13) & MASK
    value ^= value >> 17
    value ^= (value << 5) & MASK
    return value & MASK


def ror32(value, count):
    count &= 31
    value &= MASK
    return ((value >> count) | (value << ((-count) & 31))) & MASK


def derive_pin_key(pin):
    memory = [0] * 16384
    value = xorshift32((pin * 40503) ^ 2135587861)

    for i in range(len(memory)):
        value = xorshift32(value + i * 73244475 - 1515870811)
        memory[i] = value ^ ror32(value, i)

    value = xorshift32(value ^ 1831565813)
    for i in range(8388608):
        index = (value ^ (i * 40503)) & 16383
        old = memory[index]
        mixed = xorshift32(value + old + pin * 521288629 + i)
        other_index = ((old >> 9) ^ mixed ^ i) & 16383
        memory[index] = old ^ ror32(mixed, old) ^ memory[other_index]
        value = mixed ^ memory[(index + (mixed & 31)) & 16383]

    for i in range(0, len(memory), 97):
        value = xorshift32(value ^ memory[i] ^ (i * 17881))
    return value


def slow_char_mix(char, index, salt, pin, pin_key, state, chain):
    value = xorshift32(
        state ^ (char * 16843009) ^ (salt * 40503)
        ^ chain ^ (pin_key * 521288629)
    )
    rounds = 1536 + ((pin + salt + index) & 511)
    for i in range(rounds):
        mixed = xorshift32(
            value + (pin_key ^ (i * 73244475))
            + (pin + 17) * 522133279
        )
        value = xorshift32(
            (value ^ ror32(mixed, value)) + chain + i * 40503 + salt * 131
        ) ^ ror32(mixed ^ chain, index + i)
    return xorshift32(value ^ ror32(chain, salt + index))


assert stage[:4] == b"BG2\x06"
assert sum(stage[4:-2]) & 0xFFFF == int.from_bytes(stage[-2:], "big")

pin = 2007
pin_key = derive_pin_key(pin)
print(f"PIN key: {pin_key:#010x}")

state = xorshift32(pin_key ^ 1831565813)
chain = xorshift32((pin_key * 73244475) ^ (pin * 40503) ^ 331804469)
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789{}_"
position = 4
flag = None
checked = 0

while position < len(stage) - 2:
    opcode = stage[position]
    position += 1

    if opcode == 0x10:
        expected = int.from_bytes(stage[position:position + 2], "big")
        position += 2
        actual = ((pin_key ^ ror32(pin * 40503, 7)) + 15706) & 0xFFFF
        assert actual == expected
        print(f"Unlock: {actual:#06x}")

    elif opcode == 0x11:
        length = stage[position]
        position += 1
        flag = ["?"] * length
        print(f"Flag length: {length}")

    elif opcode == 0x12:
        length = stage[position]
        position += 1 + length

    elif opcode == 0x30:
        index, salt = stage[position:position + 2]
        encoded = int.from_bytes(stage[position + 2:position + 4], "big")
        position += 4

        state = xorshift32(
            state + salt * 40503 + index * 73244475
            + (pin_key & 32767) + (chain & 65535)
        )
        mask = (salt * 145 + index * 311 + ((chain >> 11) & 65535)
                + (pin_key & 65535)) & 65535
        rotation = ((pin + salt + index + (chain & 7)) & 7) + 1
        target = encoded ^ mask
        target = ((target << rotation) | (target >> (16 - rotation))) & 65535
        target = (target + pin * 29 + salt * 41 + index * 17
                  + ((chain >> 3) & 65535)) & 65535

        matches = []
        for char in alphabet:
            mixed = slow_char_mix(ord(char), index, salt, pin, pin_key, state, chain)
            if ((mixed ^ (mixed >> 16)) & 65535) == target:
                matches.append((char, mixed))

        assert len(matches) == 1, (index, matches)
        char, mixed = matches[0]
        flag[index] = char
        chain = xorshift32(chain ^ mixed ^ (ord(char) * 16843009) ^ (index * 40503))
        checked += 1
        print(f"index {index:2}: {char}  {''.join(flag)}")

    elif opcode == 0x7F:
        assert flag is not None and checked == len(flag) and "?" not in flag
        break

print("Flag:", "".join(flag))

Запускаем код, немного ждём и получаем заслуженный флаг!

Если вы нашли опечатку или ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter

Также по теме:

Райтап на задачу «Почувствуй радугу»

/Разборы задач

Райтап на задачу «Почувствуй радугу»

Разбор задачи «Почувствуй радугу»: изучаем ELF-файл в Ghidra и восстанавливаем флаг по MD5-хешам.
Прочитать

36

Райтап на задачу «Гесс вот (Guess what)»

/Разборы задач

Райтап на задачу «Гесс вот (Guess what)»

Подбираем строку по позиции ошибки, разбираем её содержимое и обсуждаем ускорение перебора.
Прочитать

57