ImaginaryCTF 2022 reverse writeups

English version - https://uvicorn.github.io/ictf2022_rev_en/

Вступление

На прошедшем Imaginaryctf мы заняли 7 место, закрыв весь реверс.

Небольшое описание задач:

  1. The House Always Wins - glibc random prediction
  2. Jormungandr - python virual machine
  3. One Liner Revenge - python labyrinth
  4. xobeert - another python virual machine
  5. wired - avr rng

The House Always Wins

Дан бинарь следующего содержания

unsigned int get_rand()
{
  FILE *stream; // [rsp+8h] [rbp-18h]
  int j; // [rsp+14h] [rbp-Ch]
  int i; // [rsp+18h] [rbp-8h]
  unsigned int seed; // [rsp+1Ch] [rbp-4h]

  rand();
  if ( !init )
  {
    init = 1;
    seed = 0;
    stream = fopen("/dev/urandom", "r");
    for ( i = 0; i <= 7; ++i )
    {
      seed += fgetc(stream);
      if ( i != 7 )
        seed <<= 8;
    }
    fclose(stream);
    srand(seed);
    for ( j = 0; j < rand() >> 15; ++j )
      rand();
  }
  return rand() >> 15;                          // 17 bit
}

int main(int argc, const char **argv, const char **envp)
{
  char v3[59]; // [rsp+0h] [rbp-70h] BYREF
  char aswer; // [rsp+3Bh] [rbp-35h] BYREF
  unsigned int stavka; // [rsp+3Ch] [rbp-34h] BYREF
  FILE *v6; // [rsp+40h] [rbp-30h]
  int rand2; // [rsp+4Ch] [rbp-24h]
  unsigned int v8; // [rsp+50h] [rbp-20h]
  unsigned int v9; // [rsp+54h] [rbp-1Ch]
  float v10; // [rsp+58h] [rbp-18h]
  float v11; // [rsp+5Ch] [rbp-14h]
  int rand; // [rsp+60h] [rbp-10h]
  unsigned int prize; // [rsp+64h] [rbp-Ch]
  int is_win; // [rsp+68h] [rbp-8h]
  unsigned int dollar; // [rsp+6Ch] [rbp-4h]

  puts("You start with $100. Get 1 billion dollars, and we'll give you the flag.");
  puts("Run out of money, and we kick you out of the casino.\n");
  puts("Are you feeling lucky?\n");
  dollar = 100;
  while ( 1 )
  {
    rand = get_rand();
    printf("Current money: %u\n", dollar);
    if ( dollar > 1000000000 )
      break;
    puts("How much are you betting? (minimum bet $5)");
    printf(">>> ");
    __isoc99_scanf("%u%c", &stavka, &dead);
    if ( stavka <= 4 || dollar < stavka )
    {
      puts("You can't bet that!");
      puts("Get out of here, and come back with some real money!");
      exit(1);
    }
    dollar -= stavka;
    printf("The first number is %d.\n\n", (unsigned int)rand);
    v11 = (float)((float)rand + 1.0) / 65536.0; / (r+1)/65536
    v10 = 1.0 - v11;                            
    v9 = (int)((double)(int)stavka * 0.99 / v11);
    v8 = (int)((double)(int)stavka * 0.99 / (float)(1.0 - v11));
    printf(
      "Odds of higher: %.2f\tPayout of higher: %u\n",
      (float)(100.0 * v10),
      (unsigned int)(int)((double)(int)stavka * 0.99 / v10));
    printf("Odds of lower:  %.2f\tPayout of lower:  %u\n\n", (float)(100.0 * v11), v9);
    puts("Do you think the next number will be:");
    puts("1) Higher");
    puts("2) Lower\n");
    puts("Remember, the house wins ties!");
    printf(">>> ");
    __isoc99_scanf("%c%c", &aswer, &dead);
    rand2 = get_rand();
    is_win = 0;
    prize = 0;
    printf("The second number is %d!\n", (unsigned int)rand2);
    if ( aswer == '1' && rand < rand2 )
    {
      is_win = 1;
      prize = v8;
    }
    else if ( aswer == '2' && rand > rand2 )
    {
      is_win = 1;
      prize = v9;
    }
    if ( is_win )
    {
      printf("Congrats! You won %u dollars!\n\n", prize);
      dollar += prize;
    }
    else
    {
      puts("You lost... Better luck next time!\n");
    }
  }
  v6 = fopen("./flag.txt", "r");
  __isoc99_fscanf(v6, "%s", v3);
  printf("How'd you beat the house? %s\n", v3);
  exit(0);
}

Нам дают результат get_rand и просят по нему угадать будет ли больше или меньше результат вызова следующего get_rand. Так как srand вызывается один раз при первом запуске get_rand , то функция приобретает вид

unsigned int get_rand()
{
  rand();
  return rand() >> 15;                          // 17 bit
}

В этой статье хорошо описывается рандом в glibc и как его предиктить:

Так как за один ход rand вызывается 4 раза - [0,n1,0,n2] (где n1 нам известен, 0 - нет, n2 нам нужно угадать), то выражение принимает вид o[i] = o[i-31-3] + o[i-31-31] + o[i-3-3] + o[i-3-31] = o[i-62] + o[i-9] + 2*o[i-34], следовательно i>=62, то есть нам нужно 62//4=16 раундов чтобы начать выигрывать. Нам понадобится примерно log(1000000000, 2)~=30 запросов чтобы достать флаг, то eсть можно не заморачиваться над оптимизацией всего этого.

Для начала затестим алгоритм предсказания рандома на локалке:

from os import urandom
from ctypes import CDLL


libc = CDLL("libc.so.6")


def init():
    seed = int.from_bytes(urandom(8), "big")
    libc.srand(seed)
    for _ in range(libc.rand() >> 15):
        libc.rand()


def get_random():
    libc.rand()
    return libc.rand() >> 15

init()

o = []
for i in range(50):
    o.append(-1)
    libc.rand()
    o.append(libc.rand() >> 15)

get_mod = lambda x: ((x << 15) % 2147483647) >> 15
for i in range(63,100,4):
    print(o[i] - get_mod(o[i - 62] + o[i - 6] + o[i - 34] * 2))

Работает! Разница между предсказанным и реальным значением меньше 4, что нам подходит, так как настолько точного результата и нам не надо. Финальный код решения:

from pwn import remote, context

context.log_level = "debug"

r = remote("the-house-always-wins.chal.imaginaryctf.org", 1337)
r.recvlines(6)
o = []


def bet():
    current_money = int(r.recvline().split()[-1])
    r.recvline()
    r.recv(4)
    if len(o) >= 31:
        r.sendline(str(current_money).encode())
        o.append(int(r.recvline().split()[-1][:-1]))
        predict = (((o[-17] * 2 + o[-31] + o[-3]) << 15) % 2147483647) >> 15
        if predict > o[-1]:
            status = b"1"
        else:
            status = b"2"
    else:
        r.sendline(b"5")
        o.append(int(r.recvline().split()[-1][:-1]))
        status = b"1"
    r.recvlines(9)
    r.recv(4)
    r.sendline(status)
    o.append(int(r.recvline().split()[-1][:-1]))
    r.recvlines(2)


for i in range(200):
    bet()
# ictf{if_the_house_isn't_using_cryptographically_secure_PRNG_the_house_deserves_to_lose}

Здесь диллер дурак,
Ёр буллшит фак!
Порядок другоооооой!

Ты где их береееееешь?
Ты дегенераааааааат!
Порядок у карт
В киосках был взяят,
Ты че, долбоеб?

Jormungandr

Нам дана следующая стековая виртуальная машина на питоне:

find=lambda v:(i:=0,len([(i:=i+1)for(c)in(iter(lambda:text[i].startswith(v), True))]))[1]

def p(N):
    Enter =1
    prime=2# flag
    while Enter<N:
        prime+=(1+prime%2)
        s =prime%N
        for( hile)in range(3, prime,int( hex (2),16)):# salt
            if( prime%hile)==0 : break
            j__f = prime
        else:
            Enter+=1
    return(prime)

text=open( __file__).read().split()

try:
    while False:0
    while 1:
        {**{chr(i):lambda:0for(i)in range(32,127)},**{
            'l':lambda:text.insert(find(text[0][1:])+1,input()),
            's':lambda:text.append(text.pop(0)),
            'd':lambda:(text.pop(),text.pop()),
            'w':lambda:(text.pop(find(text[0][1:])+1),text.insert(find(text[0][1:])+1,text[1])),
            'i':lambda:(b:=text[1],text.pop(1),text.insert(1,('{:0%dx}'%(len(b))).format((int(b[:len(b)],16)*(3**p(len(text)))+p(len(text)))%16**(len(b)))),text.append(text.pop(0))),# lit
            'q':lambda:[text.pop()for( d)in iter(int,1)],
            'j':lambda:( chile :=text[0][1:],[text.append(text.pop(0))for(i)in(iter(lambda:text[0].startswith(chile),True))]),'p':lambda:print(text[1],end=' j__f '*0),# kite ce10e59f40c8d954d9dad1ea81811a834d26580107149d16c3a769198fb158f0cb0e33dbd98f8dc8bb874105974b71719790b23c971736e8fe8ec88e8695 p
            'not' :lambda: print(' bad... '),
            'c':lambda:text.append(text.pop(0))if text[find(text[0][1:])+1][1]==text[1][0]else[text.append(text.pop(0))for(i)in' q'],
            'k':lambda:text.append(text.pop(0))if text[find(text[0][1:])+1]==text[1]else[text.append(text.pop(0))for(i)in' q'],
        }
        }[text[0][0]]()
        text.append(text.pop(0))
except:
    pass

Файл читает сам себя и, видимо, на этом основывается байткод для вм:

text=open(__file__).read().split()
...
text.insert(find(text[0][1:])

Эта проблема легко решается созданием нового файла и смены __file__ на original.py

text явно является стеком нашей машины, а словарь {'l':lambda:..., 's': lambda: ...,} похож на обработчики инструкций. Напишем для них дисассемблер:

def disp(opc): # disp(text[0][0])
    if opc == 'l':
        print(f"text[{find(text[0][1:])+1}] = input()")
    elif opc == 's':
        print('POPINS # {text[0]}')
    elif opc == 'd':
        print('POP 2')
    elif opc == 'w':
        print(f'POP text[{find(text[0][1:])+1}] && text[{find(text[0][1:])+1}] = text[1] # {text[1]}')
    elif opc == 'i':
        b = text[1]
        pp = p(len(text)-1) # I spent 6 hours searching for an error in this opcode logger.
        res = (int(b,16)*(3**pp)+pp) % (16**(len(b))) # b*3**p + p mod 16**len(b)
        print(f'text[1] = ({int(b,16)} * {(3**pp)} + {pp}) % ({16**(len(b))}) = {res} && POPINS # pp = {pp}')
    elif opc == 'q':
        print(f'text = [] # QUIT')
    elif opc == 'j':
        print(f"POPINS while (text[0] not start '{text[0][1:]}' )")
    elif opc == 'p':
        print(f"Print {text[1]}")
    elif opc == 'c':
        print(f'if text[{find(text[0][1:])+1}][1] ({text[find(text[0][1:])+1][1]}) == text[1][0] ({text[1][0]}) then POPINS else POPINS 2')
    elif opc == 'k':
        print(f'if text[{find(text[0][1:])+1}] ({text[find(text[0][1:])+1]}) == text[1] ({text[1]}) then POPINS else POPINS 2
    print(f"POPINS")

Небольшой комментарий к инструкциям:

l - считывает из ввода и кладет в стек
s - POPINS (POP & INSERT), то есть снимает нижний элемент стека и пушит его на верх( проще говоря, сдвигает стек на 1 элемент влево)
d - удаляет 2 верних элементов стека
w - удаляет элемент по индексу find(text[0][1:])+1, а затем кладет по этому же индексу text[1] # если find(text[0][1:])+1 == 0, то это будет find(...) вернет уже новый индекс
q - удаляет все элементы из стека, что вызывает exception, из-за чего работа машины прерывается
j - сдвигает стек влево пока text[i] не начинается с text[0][1:]
p - выводит text[1] в консоль
c, k - какие-то сравнения
i - пушит на верх стека b*3**p + p mod 16**len(b) (где b=text[1], pp = p(len(text)-1), функция p выдает n-ное простое число)

После этих и любых других опкодов вм сделает POPINS

Итак, запускаем наш дисассемблер, вводя a:

Как видим, в обработчик i передается int(input(), 16), а затем результат функции идет дальше, пока не доходит до обработчика k (сравнения), в котором сравнивает результаты с некой константой и в зависимости от этого выдает bad... или not bad...:

В итоге имеем уравнение вида ((b * 3^p1 + p2) * 3^p3 + p4) * ... == 0xce10e... mod m Решаем его при помощи sage и забираем флаг

m = 16**124 # легко угадать, смотря на длину сравниваемой константы ~= log(0xce10..., 16)
n = Mod(0xce10e59f40c8d954d9dad1ea81811a834d26580107149d16c3a769198fb158f0cb0e33dbd98f8dc8bb874105974b71719790b23c971736e8fe8ec88e8695,m)
n -= 277
n /= 1454077510067338869372316944847370699315973030897976908309312512336980481738317971337352174999857054574561953999845406588476984323763
n -= 283
n /= 1060022504839090035772419052793733239801344339524625166157488821493658771187233801104929735574895792784855664465887301402999721572023227
n -= 307
n /= 299381664701132778293030021884627147018656102332612973563127175701392996827190480510641050862491714816103809471587812971633152346558576658975844187
n -= 313
n /= 218249233567125795375618885953893190176600298600474857727519711086315494687021860292257326078756460100939677104787515656320568060641202384393390412323
n -= 331
n /= 84554224792451089968936207473892531191988484041341985013001115226199070159922911068116016183264280284215039063438883556666870418811396281309653281910088285947
n -= 347
n /= 3639782124011924976038695589926766574205325707740600894440840379831043263633626934257002144272462342660405490567954921015326460861727327341974159433117917530528729787
print(long_to_bytes(n)) # ictf{welcome_to_the_flag_at_the_end_of_the_universe!_8762a9ab}

xobeert

Нам дан дамп ast (абстрактное синтаксическое дерево) проги на питоне. Пытаемся распарсить его при помощи ast.unparse (эту чудесную функцию ввели в 3.9), но получаем AttributeError: 'Assign' object has no attribute 'lineno', что фиксится при помощи ast.fix_missing_locations:

from ast import *
tree = eval(open('boxast.txt').read()[1:-3])
tree = ast.fix_missing_locations(tree)
decompiled = unparse(tree)
open('out.py', 'w').write(decompiled)

Получаем еще один адовый обфусцированный код (привет one liner):

Как видим, все завязано на декораторах. Легче всего понять что это на примере функцию main из этой задачи, которую можно переписать следующим образом:

def main():
    pass
main = print(fffffffffffffffffffffffffffffffffff(main))

Так вот, начнем деобфускацию декораторов функции main:

  1. первым декоратором на очереди будет функция, стоящая перед bytes.decode на скрине выше:

Как видим, эта функцию вызывает какой-то метод из random, заведем функцию log, чтобы посмотреть что находится в bytes.decode перед вызовом random.__dict__.get:

def log(string):
    print(string)
    return string

хорошо, это функция random.seed.

  1. поднимаемся по функции main выше

Функция берет длину нашего ввода, обзовем ее get_input_len , а функцию с получением ввода get_input

  1. поднимаемся еще выше

с помощью log выясняем, что это random.randbytes

  1. идем дальше

Эта функция ксорит наш ввод с массивом y, который передается в нее, пока обзовем ее xor_with_input

  1. финал

Последняя функция сравнивает переданное ей значение с неким константным массивом, который мы можем вытащить при помощи обычного принта ([123, 250, 94, 95, 121, 195, 249, 70, 71, 59, 137, 59, 5, 67, 65, 226, 17, 160, 205, 100, 251, 169, 50, 118, 184, 177, 1, 175, 133]), а затем пишет wrong или correct. Обзовем функцию cmp_with_some_array.

Итак, у нас есть деобфусцированный main:

С помощью log вытаскиваем изначальный сид для рандома - debdbeef_or_sth , а дальше все довольно просто:

import random
cmp_arr = [123, 250, 94, 95, 121, 195, 249, 70, 71, 59, 137, 59, 5, 67, 65, 226, 17, 160, 205, 100, 251, 169, 50, 118, 184, 177, 1, 175, 133]
random.seed('debdbeef_or_sth')
seed2 = random.randbytes(len(cmp_arr)) # так как ввод должен сравниваться с массивом равной длины 
random.seed(seed2)
print(xor(random.randbytes(len(cmp_arr)), cmp_arr)) # ictf{wh0_n33d5_c4ll5_4nyw4y?}

P.S. этот таск был сломан и оргам удалось починить его только со второй попытки

One Liner Revenge

Нам дают файл следующего содержания (дальше идут еще лямбды):

Я немного опешил. После jormungandr вообще не хотелось это решать, но все таки удалось себя убедить, что это сон и нужно лишь немного деобфусцировать код и все будет хорошо. Итак, первые строчки кода превращаются в

# globals().__setitem__(chr(0x67),globals())
g = globals()
# g.__setitem__(chr(0x74),lambda*a:bytes.fromhex('{:x}'.format(a[0])).decode())
t = lambda*a: bytes.fromhex('{:x}'.format(a[0])).decode()

Дальше по коду мы видим, что t расшифровывает строчки. Заменяем ее вызовы на расшифрованные строчки, пытаясь привести код к человеческому виду:

Это все таки лучше изначального ада, но все еще ужасно. На 7 строчке видим проверку на то, что файл не патчили (как ее обходить, думаю, и без меня понятно). Можно было бы и дальше деобфусцировать этот кринж, но мне в голову пришла идея получше. Что если протрейсить исполнение огромного массива лямбд и уже ориентироваться по ним что делать дальше? Итак, заводим функцию aboba , которая будет нам с этим помогать, и пробуем ее вставить в лямбду:

def aboba(a, *equations):
    print(a, equations)
    return equations
...
# *(lambda*a:(51*a[10]+56*a[0]+...), ...) -> *(lambda*a:aboba(a, 51*a[10]+56*a[0]+...)

Как видим, в лямбды передается наш ввод, а на выходе получаем результат исполнения 4 уравнений. Я сразу попытался сделать так, чтобы все уравнения выдали мне True, но это не сработало. Тогда пробуем посмотреть в каком порядке лямбды исполняются:

def aboba(a, num, *equations):
    print(a, num, equations)
    return equations
...
# *(lambda*a:(51*a[10]+56*a[0]+...), ...) -> *(lambda*a:aboba(a,1, 51*a[10]+56*a[0]+...)

То есть прога выходит после двух лямбд. Стоп, а что если подменить (False,False,False,False), допустим, на (2,2,2,2) ? Прога выкидывает ошибку ValueError: invalid literal for int() with base 2: '2222', по которой мы находим ее виновника:

Я предположил, что для перехода к следующей паре уравнений мне необходимо найти нужную пару результатов лямбд, то есть перебрать 2^8=256(в результате лямбды может быть 4 значения true или false) значений. Для этого пришлось немного извернуться, добавив в код новый инпут и убрав предыдующий:

i2 = list(map(int, input().split())) # вводим лишний 0, который преобразуется в (0,0,0,0) и (0,0,0,0), чтобы увидеть прошли ли мы на 2 следующих лямбды
print(i2)

def to_bits(i):
    return [int(j) for j in '{0:08b}'.format(int(i))]

bits = []
for i in i2:bits.extend(to_bits(i))
ctr = 4

def aboba(a, num, *equations):
    global ctr
    print(a, num, equations)
    res = bits[ctr-4:ctr]
    ctr+=4
    return res

И запускаем все это при помощи костылей:

for i1 in range(256):
    os.system(f"echo '{i1} {0}' | python3.9 ./ff2.py") # 0 это и есть одно лишнее значение

18 (а точнее 0, 0, 0, 1 и 0, 0, 1, 0) и является нужным результатом первой пары лямбды, чтобы перейти к исполнению второй (лабиринт какой-то). Подобным образом дампим нужные значения оставшихся пар лямбд(я это сделал руками, нужно было всего 8 запросов) - [18, 52, 86, 120, 154, 188, 222, 240]

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

import z3

def to_bits(i):
    return [int(j) for j in '{0:08b}'.format(int(i))]

ll = (lambda *a: 51*a[10]+56*a[0]+... , lambda *a: ...)

sp = [16, 1, 15, 2, 14, 3, 13, 4, 12, 5, 11, 6, 10, 7, 9, 8]
lambd_results = [18, 52, 86, 120, 154, 188, 222, 240]
a = [z3.Int(f"flag[{i}]") for i in range(24)]

cons = []
for lambd_res in lambd_results:
    t = to_bits(lambd_res)
    cons.append(t[:4])
    cons.append(t[4:])

s = z3.Solver()

for i in range(16):
    l_result = ll[sp[i]-1](*a)
    for j in range(4):
        if cons[i][j] == 1:
            s.add(l_result[j])
        else:
            s.add(not l_result[j])

for k in range(24):
    s.add(a[k] <= 126)
    s.add(a[k] >= 32)

print(s.check())
res = s.model()
flag = ""
for letter in a:
    flag += chr(res.eval(letter).as_long())

print(flag)

# sat
# ictf{0n3l1n3is5uperior!}

wired

Нам дают прошивку под ардуино и видосик с тем, как плата выдает нам флаг, зашифрованный при помощи hardware rng module. Сам видосик.

Находим хороший райтап с подробным гайдом по реверсу avr, из которого мы забираем себе

  1. модуль процессора ATmega328 для иды
  2. плагин diaphora, с помощью которого мы можем облегчить себе жизнь, найдя дифф между скомпиленным нами бинарем, содержащим встроенные функции, и нужным бинарем. Это нужно, чтобы восстановить имена функций, в нашем случае это восстановятся только 3 функции:

Смотря на видосик и замеряя задержку, которая создается при помощи delay, между порядком загорания светодиодов понимаем, что она примерно равна 200 миллисекундам, что соответствует следующему циклу:

Отсюда извлекаем порядок битов (Наскальные рисунки древних шизов в моем исполнении):

Алгоритм шифрования:

  1. Генерируем random stream (на самом деле он сразу ксорит его с флагом, но для простоты повествования опущу это)

Видим цикл, в котором прога ждет ввода аналогового сигнал (вольтажа) и преобразует его в десятибитное число, которое разбивается на регистры r24 и r25.

Выясняем, что это seed для нашего RNG. Так как он занимает всего 10 бит, то нам понадобится 2^10=1024 попыток чтобы угадать его, то есть его можно сбрутить.

Алгоритм генерации rng stream находится выше, втупую переписываем его на си, добавиви брут seed.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char orig[] = {????}; // вывод программы

int check_rng(unsigned char r24, unsigned char r25) {
    unsigned char work[21];
    memcpy(work, orig, 21);

    for(int i = 0; i < 20; i ++) {
        unsigned char *c = &work[i];
        *c = *c ^ r24;
        unsigned char r18 = r24;
        unsigned char r19 = r25;
        unsigned int r25r24 = (r25 << 8) | r24;
        r25r24 = r25r24 >> 1;
        r25 = (r25r24 & 0xFF00) >> 8;
        r24 = (r25r24 & 0xFF);
        if (r18 & 1) {
            r25 ^= 0xAD;
        }
    }
    write(1, work, 20);

}

int main() {
    for(int seed = 0; seed < 1024; seed++) {
        unsigned char r24 = (seed & 0xFF00) >> 8;
        unsigned char r25 = (seed & 0xFF);
        check_rng(r25, r24);
    }
}
  1. ксорим флаг по адресу 0x100 с rng stream
  2. выводим его на светодиоды, некоторые из которых инвертированы

Если говорить совсем просто, то при попадании на ветку с sbrc у нас инвертируется бит, при попадании на sbrs - нет. Получаем маску, которая обозначает светодиоды, на которых биты инвертированы - 0b10011010

Выводим формулу получения флага: flag[i] ^ rng[i] ^ invert_mask = out[i] => flag[i] = rng[i] ^ 0b10011010 ^ out[i]

По видосику определяем массив out и сразу накладываем на него маску.

a = [0b01010110, 0b10101011, 0b01000111, 0b10101000, 0b11001011, 0b01111000, 0b00110101, 0b00010110, 0b10011010, 0b11000111, 0b01011001, 0b00100110, 0b10010011, 0b01001110, 0b10011100, 0b00001111, 0b11111101, 0b10000011, 0b01101101, 0b00101101]
out = list(map(lambda o: o ^ 0b10011010, a)) # [204, 49, 221, 50, 81, 226, 175, 140, 0, 93, 195, 188, 9, 212, 6, 149, 103, 25, 247, 183]

Итоговый код решения:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned char orig[] = {204, 49, 221, 50, 81, 226, 175, 140, 0, 93, 195, 188, 9, 212, 6, 149, 103, 25, 247, 183, 0};

int check_rng(unsigned char r24, unsigned char r25) {
    unsigned char work[21];
    memcpy(work, orig, 21);

    for(int i = 0; i < 20; i ++) {
        unsigned char *c = &work[i];
        *c = *c ^ r24;
        unsigned char r18 = r24;
        unsigned char r19 = r25;
        unsigned int r25r24 = (r25 << 8) | r24;
        r25r24 = r25r24 >> 1;
        r25 = (r25r24 & 0xFF00) >> 8;
        r24 = (r25r24 & 0xFF);
        if (r18 & 1) {
            r25 ^= 0xAD;
        }
    }
    write(1, work, 20);

}

int main() {
    for(int i = 0; i < 1024; i ++) {
        unsigned char r24 = (i & 0xFF00) >> 8;
        unsigned char r25 = (i & 0xFF);
        check_rng(r25, r24);
    }
}

Запускаем:

./a.out | grep -oaiP 'ictf{.*?}'
ictf{weird_rng_912b}


Last modified on 2022-07-20