Forkbomb reverse ch2

Как писать райтапы, если решений не осталось

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