Как писать райтапы, если решений не осталось
Braindead
Реверс брейнфака. Находим райтап и следуя ему с помощью этого скрипта транслируем брейнфак в си, который позднее компилируем с -O3.
Закидываем бинарь в иду и по вызовам putchar находим нечто похожее на strcmp по своей логике:
v320 = *(_DWORD *)&v350[16];
v322 = 0;
v319 = 1;
do
{
while ( 1 )
{
v323 = v357;
if ( v357 )
{
v357 = 0;
v321 += v323;
}
if ( v321 )
v357 += v321;
v324 = v320;
v325 = v320 + v323; // v325 = v323 // v323 - кол-во сиволов, которые совпали
if ( !v323 )
v325 = v324;
if ( v325 == 32 ) // сравниваются только первые 32 символа -> длина флага = 32
break;
++v398;
v321 = 0;
v320 = 0;
if ( !--v319 )
goto LABEL_989;
}
++v322;
v321 = 0;
v320 = 0;
--v319;
}
while ( v319 );
LABEL_989:
*(_QWORD *)&v350[12] = 0LL;
v3 = 0;
v356 = 0;
v358 = 32;
*(_DWORD *)&v350[4] = v322;
if ( !v322 ) // if v322 == 0
goto lose;
printf("Congratulations! The flag you have entered is correct");
lose:
printf("No, that's wrong");
В дебаггере обнаруживаем, что в v323 (rdx) хранится кол-во введенных символов, которые совпадают с символами флага.
Я, как существо ленивое, не захотел копаться в бинаре и решил написать нейросеть-брутфорсер:
import time
gdb.execute('b *0x55555555888f') # адрес v325 = v320 + v323;
dlt = 32*"6"
gdb.execute('r <<<"' + dlt +'"')
m_val = int(str(gdb.parse_and_eval("$rdx")),16)+1 # rdx - v323
print(m_val, len(dlt))
time.sleep(2)
gdb.execute('c')
st = ''
for i in range(len(st),32):
for ch in '\'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345789{|}^_ !#$%&()*+,-./:;<=>?@':
print(st+ch)
gdb.execute('r <<<"'+ "6"*len(st) + ch + "6"*(31-len(st)) +'"') # r <<< "6666666666666_66666666666666666"
val = int(str(gdb.parse_and_eval("$rdx")),16)
if val >= m_val:
st += ch
print("SUCCESS ", st)
time.sleep(3)
gdb.execute('c')
break
gdb.execute('c')
Идея состоит в том, чтобы проверять каждый символ отдельно, заполняя 31 оставшийся шестерками (можно заполнять любыми символами), после чего переходить к следующему символу.
Стоит сказать, что нейросеть не идеальна, так как при переборе некоторых символов приходилось убавлять m_val на 1, а иногда и вовсе оставлять символ неразгаданным. Тем не менее, минут через 15 получаем что-то похожее на флаг:spbctf{h4vE_%_B%%n_br41NWa5%3d?}.
Находим возможные оставшиеся неизвестные символы (%):
dct = dict()
gdb.execute('b *0x55555555888f')
for ch in range(32,127):
if ch in (34,):continue # двойная кавычка демотивировала гдб продолжать работу
st = "spbctf{h4vE_%_B%%n_br41NWa5%3d?}".replace('%',chr(ch))
gdb.execute(f'r <<<"{st}"')
val = int(str(gdb.parse_and_eval("$rdx")),16)
dct[st] = val
gdb.execute('c')
for key, val in dct.items():
if val != 29:
print(key, val)
По завершению работы скрипта получаем
spbctf{h4vE_$_B$$n_br41NWa5$3d?} 23
spbctf{h4vE_1_B11n_br41NWa513d?} 30
spbctf{h4vE_3_B33n_br41NWa533d?} 30
spbctf{h4vE_\_B\\n_br41NWa5\3d?} 14
spbctf{h4vE_`_B``n_br41NWa5`3d?} 12
spbctf{h4vE_h_Bhhn_br41NWa5h3d?} 30
Соответственно, возможные оставшиеся символы - 1, 3, h.
Ручками подгоняем флаг до spbctf{h4vE_1_B33n_br41NWa5h3d?}.
CatGirl Industrials Shell
Реверс виртуальной машины на питоне.
def avogado6(*args):
exit(1)
def beca(st):
padding = "=" * ((4 - len(st)) % 4)
mpl = list(b64decode(st + padding))
return mpl
def do_magic(st): return "".join(chr(ord(i) ^ 0x13 ^ 0x37) for i in st)
def pet_the_cat(st): return sum(ord(i) for i in st)
def love(a0, a1): return a0+a1
def peace(a0, a1): return a0 - a1
def hopes_and_dreams(st): return st
class _outer_:pass
class _naruto_:pass
class _conti_:pass
class _petergeize_:pass
class _non_:pass
class _romane_:pass
class _rerere_:pass
class _outer_pointer_:pass
class _ghost_out_of_the_shell_:pass
class _ghost_in_the_shell_:pass
def dmagic(xval, st):
ans = ""
st = bytes(st.encode())
for i in range(len(st)):
ans += chr(st[i] ^ xval)
xval ^= (ord(ans[i]) | st[i])
return ans
memory = [ # bytecode
print,
do_magic,
0,
...
]
data = [ # stack
[do_magic("Welcome to our CatGirl Industrials shell! There is an offline auth")],
...
]
def execute():
pointer = 0
while 1:
if memory[pointer] == 0:
print("STOP VM")
break
elif memory[pointer] is _petergeize_:
pointer += 1
jt = memory[pointer]
pointer += jt-1
elif memory[pointer] is _romane_:
pointer += 1
pointer = memory[pointer] - 1
elif memory[pointer] is _conti_:
p0, p1 = data.pop(), data.pop()
data.append([*p0, *p1])
pointer += 1
elif memory[pointer] is _non_:
p0, p1 = data.pop(), data.pop()
pointer += 1
jp = memory[pointer]
if p0 == p1:
pointer += 1
else:
pointer += jp-1
elif memory[pointer] is _rerere_:
f0, o0 = data.pop()[0], data.pop()[0]
memory[pointer+o0] = f0
pointer += 1
elif memory[pointer] is _ghost_in_the_shell_:
f0 = data.pop()
data.append([f0])
pointer += 1
elif memory[pointer] is _ghost_out_of_the_shell_:
f0 = data.pop()
f0 = f0[0]
data.append(f0)
pointer += 1
elif memory[pointer] is _naruto_:
p0 = data.pop()
data.append(p0)
data.append(p0)
pointer += 1
elif memory[pointer] is _outer_:
pointer += 1
p0 = memory[pointer]
data.append([p0])
pointer += 1
elif memory[pointer] is _outer_pointer_:
pointer += 1
data.append([pointer])
else:
fcall = []
while memory[pointer] != 0:
fcall.append(memory[pointer])
pointer += 1
param = data.pop()
for fun in fcall[::-1]:
param = [fun(*param)]
if param[0] is not None:
data.append(param)
pointer += 1
execute()
Переименуем функции:
avogado6 -> ext
do_magic -> xor_36
pet_the_cat -> sum_chars
love -> add
peace -> sub
hopes_and_dreams -> nop
_outer_ -> PUSH
_naruto_ -> COPY
_conti_ -> DOUBLE_PACK
_petergeize_ -> JMP_PLUS
_non_ -> JNE
_romane_ -> JMP
_rerere_ -> _rerere_ # инструкция не использовалась в байткоде, поэтому я забил на ее переименование
_outer_pointer_ -> PUSH_PTR
_ghost_out_of_the_shell_ -> UNPACK
_ghost_in_the_shell_ -> PACK
И встроим в обработчик инструкций дисассемблер:
def ext(*args):
p('EXIT')
exit(1)
DEBUG = True
def p(*args): # debug print
global pointer
if DEBUG:
print(f"{pointer}>", *args)
pointer = 0
while 1:
if memory[pointer] == 0: # EXIT
p('EXIT')
print("STOP VM")
break
elif memory[pointer] is JMP_PLUS: # JMP pointer+arg
addr = memory[pointer+1]
p(f'JMP_PLUS {addr}')
pointer += addr
elif memory[pointer] is JMP: # JMP arg
pointer = memory[pointer+1] - 1
p(f'JMP {pointer}')
elif memory[pointer] is DOUBLE_PACK: # PUSH [POP, POP]
p('POP; POP; PACK')
p0, p1 = data.pop(), data.pop()
data.append([*p0, *p1])
pointer += 1
elif memory[pointer] is JNE: # JNE arg
p0, p1 = data.pop(), data.pop()
pointer += 1
jp = memory[pointer]
p(f'CMP {p0} == {p1}; JNE {jp-1}')
if p0 == p1:
pointer += 1
else:
pointer += jp-1
elif memory[pointer] is _rerere_: # я хз что это вообще
f0, o0 = data.pop()[0], data.pop()[0]
memory[pointer+o0] = f0
pointer += 1
elif memory[pointer] is PACK: # PUSH [POP] # [3] -> [[3]]
f0 = data.pop()
p(f'PACK # {f0} => [{f0}]')
data.append([f0])
pointer += 1
elif memory[pointer] is UNPACK: # [[3]] -> [3]
f0 = data.pop()
f0 = f0[0]
p(f'UNPACK # [{f0}] => {f0}')
data.append(f0)
pointer += 1
elif memory[pointer] is COPY: # pop ; push; push # копирует элемент на стеке
p0 = data.pop()
p(f"POP {p0}; PUSH ; PUSH")
data.append(p0)
data.append(p0)
pointer += 1
elif memory[pointer] is PUSH: # PUSH arg
pointer += 1
p0 = memory[pointer]
data.append([p0])
p(f'PUSH {[p0]}')
pointer += 1
elif memory[pointer] is PUSH_PTR: # PUSH pointer
pointer += 1
p(f'PUSH PTR {[pointer]}')
data.append([pointer])
else:
fcall = []
while memory[pointer] != 0:
fcall.append(memory[pointer])
pointer += 1
param = data.pop()
st = str(param)[1:-1]
for fun in fcall[::-1]:
st = f"{fun.__name__}({st})"
param = [fun(*param)]
p("PUSH "+st)
if param[0] is not None:
data.append(param)
pointer += 1
Перед тем, как приступим к реверсу байткода, скажу, что вы можете скачать код с дисассемблером тут.
Находим первые пять символов флага в CMP:

Ради следующих шести придется развернуть строку moc.eb

Ксор xor_36('\x0bSEPGL\x1bR\x19') == '/watch?v='

Опять сравнение с CMP

На последнем шаге нужно будет дисассемблировать байткод чуть дальше текущего указателя и получить нужный массив (последняя часть флага - b64encode(bytes([27, 97, 79, 25, 132])))

Соберем флаг воедино: youtube.com/watch?v=H3w1G2FPGYQ
ЗЫ: Автор таска сказал, что по данному адресу должны были находиться бурятские песенки, но правообладатель их заблочил
Movfuscator
Решение этого таска я объяснить не могу, потому что решил его при помощи своих экстрасенсорных навыков.
Итак, нам дают бинарник, состоящий из инструкций вида mov rcx, 0x1234.
Да, можно было заосинтить деобфускатор, но это путь слабых.
Я же после пары запусков подопытного угадал алгоритм шифрования
st = input()
res = ''
for i in range(len(st)):
res += chr(ord(st[i]) - i)
print(res)

Что здесь происходит догадаться нетрудно
Затем закинул бинарь в гдб и начал прожимать nexti, просматривая память в поисках шифротекста, с которым сравнивается наш ввод.
Где-то на адресе 0x4006f0 обнаруживаю заветную строку:
t2f\iDAAlPUb$bA!f"3_>
И расшифровываю ее
st = "t2f\\iDAAlPUb$bA!f\"3_>"
res = ''
for i in range(len(st)):
res += chr(ord(st[i]) + i)
print(res) # t3h_mIGHtY_m0oO0v3ErR
А кто-то говорил, что стега от менада ничему не учит…
Check Summer
К бинарю на паскале прилагается трасса инструкций вида
004216CF Main CALL check_su.0040D760
0040D760 Main PUSH EBX
0040D761 Main MOV EDX,DWORD PTR DS:[5CC020]
0040D767 Main TEST EDX,EDX
0040D769 Main JE SHORT check_su.0040D776
0040D76B Main MOV EAX,DWORD PTR DS:[5CB320]
0040D770 Main CALL EDX
Логику работы бинарника за меня опишет эта гифка:

По xref-ам находим алгоритм создания хеша от флага

И сам алгоритм хеширования:
int __usercall UNIT1____CRC32_ANSISTRING__ANSISTRING@<eax>(unsigned __int8 *input@<eax>)
{
unsigned int state, length; // ebx
unsigned __int8 *inp; // [esp+38h] [ebp-4h]
inp = input; // адрес - 0x04244B0
// ...
v7 = FPC_SETJMP();
if ( !v7 )
{
state = 0xFFFFFFFF;
length = (int)inp;
if ( inp )
length = *((_DWORD *)inp - 1);
if ( length >= 1 )
{
inp_i = 0;
do
{
state ^= inp[inp_i++]; // 0x0424507
for ( i = 8; i > 0; --i )
{
if ( (state & 1) != 0 ) // если state нечетное
state = (state >> 1) ^ 0xEDB88320; // адрес - 0x0424519
else // четное
state >>= 1; // адрес - 0x0424527
}
}
while ( length > inp_i );
}
SYSUTILS____INTTOHEX_QWORD_LONGINT__ANSISTRING(~state, 0);
}
// ...
return state; // адрес - 0x0424568
}
Идея моего решения состоит в том, чтобы по четности стейта и его значению на предыдущей итерации цикла while, определить символ, с которым стейт был поксорен.
Финальный код решения:
data = open('check_summer_rtrace.txt').read().split('\n')[:-1]
addrs = [i.split()[0] for i in data]
start_crc_func = '004244B0' # начало функции
end_crc_func = '00424568' # конец функции
xor_state_addr = '00424507' # state ^= inp[inp_i++]
is_state_odd_addr = '00424519' # state = (state >> 1) ^ 0xEDB88320
is_state_even_addr = '00424527' # state >>= 1
addrs = addrs[addrs.index(end_crc_func)+1:] # на гифке видно, что при снятии трассы хеш берется 2 раза (от слова SPbCTF и от флага), нам нужен именно последний , поэтому отбрасываем часть трассы с хешированием SPbCTF
func_block = addrs[addrs.index(start_crc_func):addrs.index(end_crc_func)]
check_arr = []
for addr in func_block:
if addr == is_state_odd_addr:
check_arr.append(1)
elif addr == is_state_even_addr:
check_arr.append(0)
state_prev = 0xFFFFFFFF
flag = ''
# длину флага находим по кол-ву совершенных state ^= inp[inp_i++]
flag_len = func_block.count(xor_state_addr)
for inp_i in range(flag_len):
for char in range(32,127):
state = state_prev ^ char
new_state_check_arr = []
for i in range(8):
if (state & 1) != 0:
new_state_check_arr.append(1)
state = (state >> 1) ^ 0xEDB88320
else:
new_state_check_arr.append(0)
state >>= 1
if new_state_check_arr == check_arr[inp_i*8:inp_i*8+8]:
state_prev = state
flag += chr(char)
break
print(flag)
# spbctf{June_1st_it5_r34lly_sUmm3R}
Last modified on 2022-02-12