/Разборы задач
31 октября 2026
Райтап на задачу «Почувствуй радугу»
Разбор задачи «Почувствуй радугу»: изучаем ELF-файл в Ghidra и восстанавливаем флаг по MD5-хешам.36
57

Rev
Задача «Вернуть 2007й»
В целом необязательно запускать приложение, можно решить чисто статически.
Для начала необходимо установить приложение для реверса 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 метод 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.
Для каждого кандидата приложение:
salt, PIN-ключа и предыдущего состояния цепочки.slowCharMix. Эта функция выполняет более 1500 раундов перемешивания.Формула сравнения находится в конце обработки 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.
Для каждого символа приложение:
salt, PIN-ключа и предыдущего состояния цепочки.slowCharMix. Эта функция выполняет более 1500 раундов перемешивания.Расшифровки ENC_STAGE уже известны:
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))Запускаем код, немного ждём и получаем заслуженный флаг!