English version - https://uvicorn.github.io/ictf2022_rev_en/
Вступление
На прошедшем Imaginaryctf мы заняли 7 место, закрыв весь реверс.
Небольшое описание задач:
The House Always Wins- glibc random predictionJormungandr- python virual machineOne Liner Revenge- python labyrinthxobeert- another python virual machinewired- 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:
- первым декоратором на очереди будет функция, стоящая перед
bytes.decodeна скрине выше:

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

хорошо, это функция random.seed.
- поднимаемся по функции
mainвыше

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

с помощью log выясняем, что это random.randbytes
- идем дальше

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

Последняя функция сравнивает переданное ей значение с неким константным массивом, который мы можем вытащить при помощи обычного принта ([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, из которого мы забираем себе
- модуль процессора
ATmega328для иды - плагин
diaphora, с помощью которого мы можем облегчить себе жизнь, найдя дифф между скомпиленным нами бинарем, содержащим встроенные функции, и нужным бинарем. Это нужно, чтобы восстановить имена функций, в нашем случае это восстановятся только 3 функции:

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

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

Алгоритм шифрования:
- Генерируем
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);
}
}
- ксорим флаг по адресу
0x100сrng stream - выводим его на светодиоды, некоторые из которых инвертированы

Если говорить совсем просто, то при попадании на ветку с 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