R0 CREW

Plaid CTF 2014 :: paris (reverse 300)

PlaidCTF 2014 :: paris (reverse 300)

И так тема второго занятия «Виртуальная машина» (очередные многабукав) =)

Задание звучало следующим образом «This binary was found on some of our Windows machines. It’s got The Plague written all over it. What secrets are contained inside?».

Ну что ж поищем секрет.

В качестве жертвы выступает исполняемый файл Windows размером 9216 байт.
Приложение консольное и работает абсолютно по той же схеме, что и рассмотренное выше:

  1. запрашивается ввод пароля
.text:00401006                 push    0FFFFFFF5h      ; nStdHandle
.text:00401008                 call    ds:GetStdHandle
.text:0040100E                 push    0               ; lpReserved
.text:00401010                 push    offset NumberOfCharsWritten ; lpNumberOfCharsWritten
.text:00401015                 push    0Ah             ; nNumberOfCharsToWrite
.text:00401017                 push    offset rqs      ; "Password? "
.text:0040101C                 push    eax             ; hConsoleOutput
.text:0040101D                 call    ds:WriteConsoleA
  1. получается пользовательский ввод;
.text:00401023                 push    0FFFFFFF6h      ; nStdHandle
.text:00401025                 call    ds:GetStdHandle
.text:0040102B                 push    0               ; pInputControl
.text:0040102D                 push    offset NumberOfCharsWritten ; lpNumberOfCharsRead
.text:00401032                 push    0FFh            ; nNumberOfCharsToRead
.text:00401037                 push    offset DATA_    ; lpBuffer
.text:0040103C                 push    eax             ; hConsoleInput
.text:0040103D                 call    ds:ReadConsoleA
  1. отдается на обработку
.text:00401056                 call    VM
  1. проверяется результат обработки и чеканья
.text:0040106A                 cmp     esi, 0DEADBEEFh
.text:00401070                 jnz     msg_wrong

Как видно обработка выполняется некой VM (Virtual Machine). Поставим на ручник и заглянем под капот этой машинке.

.text:00402066 VM              proc near
.text:00402066                 xor     ecx, ecx
.text:00402068 NEXT:
.text:00402068                 push    offset ENTRY_EXCEPTION
.text:0040206D                 push    large dword ptr fs:0
.text:00402074                 mov     large fs:0, esp
.text:0040207B                 mov     eax, 0
.text:00402080                 mov     [eax], eax      ; RAISE EXCEPTION
.text:00402082                 sub     edi, 1111h
.text:00402088                 pop     large dword ptr fs:0
.text:0040208F                 add     esp, 4
.text:00402092                 cmp     b_Exit_FL, 1
.text:00402099                 jz      EXIT
.text:0040209F                 jmp     short NEXT
.text:0040209F VM              endp

…………………………

.text:00401C90 EXIT:
.text:00401C90                 retn

Видим цикл, в котором ставится обработчик SEH, генерится исключение при попытке записи по нулевому адресу, уменьшение значения EDI, удаляется обработчик. Выход из этого цикла при установке некого флага b_Exit_FL. Значит, вся радость нас ждет внутри обработчика. Отметим первую особенность данной ВМ: основной цикл вынесен из самой ВМ.

С помощью (с) Matt Pietrek вспомним прототип обработчика

EXCEPTION_DISPOSITION
 __cdecl _except_handler(
     struct _EXCEPTION_RECORD *ExceptionRecord,
     void * EstablisherFrame,
     struct _CONTEXT *ContextRecord,
     void * DispatcherContext
     ); 

Эта информация нам еще понадобится. Прежде чем мы посмотрим на обработчик, краткая характеристика ВМ:

  • тип ВМ: регистровая
  • разрядность ВМ: 16
  • собственный сегмент данных ВМ: есть
  • собственный стек ВМ: есть
  • число обработчиков ВМ: 20

А вот и он:

.text:00402376 EXCEPTION_HANDLER:
.text:00402376                 movzx   eax, INDEX
.text:0040237D                 add     eax, offset HANDLER_SWITCH_TABLE
.text:00402382                 mov     eax, [eax]
.text:00402384                 add     INDEX, 4
.text:0040238B                 cmp     INDEX, 80
.text:00402392                 jl      short GOGOGO
.text:00402394                 mov     INDEX, 0
.text:0040239B
.text:0040239B GOGOGO:
.text:0040239B                 jmp     eax

Тем кто знаком с виртуальными машинами сразу в глаза бросится отсутствие выборки пикода и селектора обработчиков в привычном виде. Так вот второй особенностью данной реализации ВМ есть постселекция опкода, т.е. о том наш ли это хэндлер опкода мы будем знать только на выходе по результатам исполнения его тела. Поэтому представленный выше код точки входа ВМ методично нас отсылает во все хэндлеры присутствующие в таблице.

.text:0040220E HANDLER_SWITCH_TABLE dd offset HNDL_00_NOP
.text:00402212                 dd offset HNDL_01_MOV_REG_REG
.text:00402216                 dd offset HNDL_02_MOV_MEM16_REG
.text:0040221A                 dd offset HNDL_03_MOV_REG_MEM16
.text:0040221E                 dd offset HNDL_04_MOV_REG_IMM16
.text:00402222                 dd offset HNDL_05_ADD
.text:00402226                 dd offset HNDL_06_SUB
.text:0040222A                 dd offset HNDL_07_XOR
.text:0040222E                 dd offset HNDL_08_AND
.text:00402232                 dd offset HNDL_09_ROR8
.text:00402236                 dd offset HNDL_10_NOT
.text:0040223A                 dd offset HNDL_11_INC
.text:0040223E                 dd offset HNDL_12_CMP
.text:00402242                 dd offset HNDL_13_JMP
.text:00402246                 dd offset HNDL_14_CJMP
.text:0040224A                 dd offset HNDL_15_PUSH
.text:0040224E                 dd offset HNDL_16_POP
.text:00402252                 dd offset HNDL_17_SWAP
.text:00402256                 dd offset HNDL_18_XOR_BUF
.text:0040225A                 dd offset HNDL_19_EXIT

Таким образом, количество исполняемого, а следовательно и трассируемого кода увеличивается пропорционально количеству обработчиков. Как реализована постселекция посмотрим на примере самого короткого хэндлера (для команды NOP).

.text:0040247C HNDL_00_NOP:
.text:0040247C                 call    GET_TYPE1_1_EBX
.text:00402481                 sub     eax, 0
.text:00402484                 setz    al
.text:00402487                 jmp     END_

Сразу остановлюсь на именовании функций

  • GET - выбрать (получить) команду
  • TYPE1 – тип команды 1
  • 1 - длина команды 1
  • EBX - результат выборки и распарсивания полей команды в EBX

Кроме этого в ЕАХ возвращается опкод команды при попытке его распарсивания как TYPE1. Следующие две команды элемент постселекции – AL бедет 1 если мы действительно исполняли команду NOP. В остальных хэндлерах между выборкой команды и селекцией выполняются действия связанные с самой операцией. И они понятное дело вносят изменения в контекст ВМ: в регистры, в собственные данные и стек.
Поэтому в самом начале перед выборкой команды

.text:00401CF0 GET_TYPE1_1_EBX:
.text:00401CF0                 call    SAVE_VM_CONTEXT

у нас весь контекст ВМ сохраняется

.text:00401C91 SAVE_VM_CONTEXT:
.text:00401C91                 mov     esi, [esp+14h]
.text:00401C95                 mov     edi, offset saveArea_context
.text:00401C9A                 mov     ecx, 34h
.text:00401C9F                 rep movsd
.text:00401CA1                 mov     esi, offset DATA_ ; text:00401490...401890
.text:00401CA6                 mov     edi, offset saveArea_data
.text:00401CAB                 mov     ecx, 100h
.text:00401CB0                 rep movsd
.text:00401CB2                 mov     esi, offset STACK_
.text:00401CB7                 mov     edi, offset saveArea_stack
.text:00401CBC                 mov     ecx, 80h
.text:00401CC1                 rep movsd
.text:00401CC3                 mov     al, b_ZFLAG
.text:00401CC8                 mov     save_b_ZFLAG, al
.text:00401CCD                 mov     al, b_Exit_FL
.text:00401CD2                 mov     save_b_Exit_FL, al
.text:00401CD7                 mov     ax, w_EIP
.text:00401CDD                 mov     save_w_EIP, ax
.text:00401CE3                 mov     ax, w_ESP
.text:00401CE9                 mov     save_w_ESP, ax
.text:00401CEF                 retn

а в случае если постселекция провалилась (AL не равен 1), то восстанавливается

.text:00401FCA END_:
.text:00401FCA                 mov     edx, [esp+12]
.text:00401FCE                 cmp     al, 1
.text:00401FD0                 jz      exit_from_VM
.text:00401FD6                 mov     esi, offset saveArea_context
.text:00401FDB                 mov     edi, edx
.text:00401FDD                 mov     ecx, 34h
.text:00401FE2                 rep movsd
.text:00401FE4                 mov     esi, offset saveArea_data
.text:00401FE9                 mov     edi, offset DATA_
.text:00401FEE                 mov     ecx, 100h
.text:00401FF3                 rep movsd
.text:00401FF5                 mov     esi, offset saveArea_stack
.text:00401FFA                 mov     edi, offset STACK_
.text:00401FFF                 mov     ecx, 80h
.text:00402004                 rep movsd
.text:00402006                 mov     al, save_b_ZFLAG
.text:0040200B                 mov     b_ZFLAG, al
.text:00402010                 mov     al, save_b_Exit_FL
.text:00402015                 mov     b_Exit_FL, al
.text:0040201A                 mov     ax, save_w_EIP
.text:00402020                 mov     w_EIP, ax
.text:00402026                 mov     ax, save_w_ESP
.text:0040202C                 mov     w_ESP, ax
.text:00402032                 jmp     exit_from_VM

Что тоже увеличивает объем исполняемого кода, да и привязаться к содержимому контеста ВМ совсем непросто, т.к. только одно из 60 (3[сохранение-изменение-восстановление]*20 хэндлеров) действий чтения/записи будет нас интересовать.

Исходя из описанных трудностей с динамическим анализом работы ВМ и учитывая, что хэндлеров всего 20 было решено писать декомпилятор ленты PCODE.

#! /usr/bin/python
# -*- coding: utf-8 -*-
'''
####################################################################
#
#  Plaid CTF 2014
#  Task: paris (reverse 300)
#
#  VM decompilator
#
'''

_TYPES1 = [0x00,0x09,0x15,0x02,0x07,0x18,0x1B,0x0A,0x14]
_TYPES2 = [0x201,0x202,0x203,0x98,0x99,0x9A,0x9B,0x3F]
_TYPES3 = [0x13,0x1F,0x1D]

# get 1-byte command
def PARAM_T1(eip):
   b = ord(PCODE[eip])
   type = (b >> 3) & 0xFFFF
   param = (b & 7) << 1
   return type, param

# get 2-byte command
def PARAM_T2(eip):
   w = (ord(PCODE[eip]) << 8) | ord(PCODE[eip+1])
   type = (w >> 6) & 0xFFFF
   param1 = ((w >> 3) & 7) << 1
   param2 = (w & 7) << 1
   return type, param1, param2

# get 3-byte command
def PARAM_T3(eip):
   d = (ord(PCODE[eip]) << 16) | (ord(PCODE[eip+2]) << 8) | ord(PCODE[eip+1])
   type = (d >> 19) & 0xFFFF
   param1 = ((d >> 16) & 7) << 1
   param2 = (d & 0xFFFF)
   return type, param1, param2

f = open('paris.exe', 'rb')
f.seek(0xF8E)
PCODE = f.read(108+2)
f.close()

# PCODE pointer
eip = 0

# mail loop
while 1:
   # get info as 1-2-3 bytes command 
   type1,param1 = PARAM_T1(eip)
   type2,param2_1,param2_2 = PARAM_T2(eip)
   type3,param3_1,param3_2 = PARAM_T3(eip)

   # command selector
   if type1 in _TYPES1:
      if type1 == 0x00:
         print '%04X: %02X        nop'%(eip,ord(PCODE[eip]))
      elif type1 == 0x09:
         print '%04X: %02X        ror reg-%X,8'%(eip,ord(PCODE[eip]),param1)
      elif type1 == 0x15:
         print '%04X: %02X        not reg-%X'%(eip,ord(PCODE[eip]),param1)
      elif type1 == 0x02:
         print '%04X: %02X        inc reg-%X'%(eip,ord(PCODE[eip]),param1)
      elif type1 == 0x07:
         print '%04X: %02X        push reg-%X'%(eip,ord(PCODE[eip]),param1)
      elif type1 == 0x18:
         print '%04X: %02X        pop reg-%X'%(eip,ord(PCODE[eip]),param1)
      elif type1 == 0x1B:
         print '%04X: %02X        swap reg-%X'%(eip,ord(PCODE[eip]),param1)
      elif type1 == 0x0A:
         print '%04X: %02X        loop_xor buffer,key[reg-%X]'%(eip,ord(PCODE[eip]),param1)
      elif type1 == 0x14:
         print '%04X: %02X        exit'%(eip,ord(PCODE[eip]))
         #break
      eip += 1
   elif type2 in _TYPES2:
      if type2 == 0x201:
         print '%04X: %02X %02X     mov reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
      elif type2 == 0x202:
         print '%04X: %02X %02X     mov [reg-%X],reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
      elif type2 == 0x203:
         print '%04X: %02X %02X     mov reg-%X,[reg-%X]'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
      elif type2 == 0x98:
         print '%04X: %02X %02X     add reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
      elif type2 == 0x99:
         print '%04X: %02X %02X     sub reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
      elif type2 == 0x9A:
         print '%04X: %02X %02X     xor reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
      elif type2 == 0x9B:
         print '%04X: %02X %02X     and reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
      elif type2 == 0x3F:
         print '%04X: %02X %02X     cmp reg-%X,reg-%X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),param2_2,param2_1)
      eip += 2
   elif type3 in _TYPES3:
      if type3 == 0x13:
         print '%04X: %02X %02X %02X  mov reg-%X,0x%04X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),ord(PCODE[eip+2]),param3_1,param3_2)
      elif type3 == 0x1F:
         print '%04X: %02X %02X %02X  jmp %04X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),ord(PCODE[eip+2]),param3_2)
      elif type3 == 0x1D:
         print '%04X: %02X %02X %02X  cjmp %04X'%(eip,ord(PCODE[eip]),ord(PCODE[eip+1]),ord(PCODE[eip+2]),param3_2)
      eip += 3
   # end of PCODE
   if eip >= 108:
      break

Для возможности селекции команд в декомпиляторе также производится распарсивание команды для всех трех типов и их группировка.

Выхлоп декомпилятора (девиртуализатора) оказался коротким, всего 54 команды

0000: 00        nop
0001: 00        nop
0002: 00        nop
0003: 9A 33 31  mov reg-4,0x3133
0006: 9B 00 00  mov reg-6,0x0000
0009: 9C 00 FF  mov reg-8,0xFF00
000C: 9D FF 00  mov reg-A,0x00FF
000F: 80 D8     mov reg-0,[reg-6]
0011: 80 47     mov reg-E,reg-0
0013: DF        swap reg-E
0014: AF        not reg-E
0015: 0F D7     cmp reg-E,reg-4
0017: EF 37 00  cjmp 0037
001A: 80 7E     mov reg-C,reg-E
001C: 26 E6     and reg-C,reg-8
001E: 26 EF     and reg-E,reg-A
0020: 4E        ror reg-C,8
0021: 26 B7     xor reg-E,reg-C
0023: 9E 00 02  mov reg-C,0x0200
0026: 26 3F     add reg-E,reg-E
0028: 26 3E     add reg-C,reg-E
002A: 80 F7     mov reg-E,[reg-C]
002C: DF        swap reg-E
002D: C6        pop reg-C
002E: 26 B7     xor reg-E,reg-C
0030: 3E        push reg-C
0031: 3F        push reg-E
0032: 53        loop_xor buffer,key[reg-6]
0033: 13        inc reg-6
0034: FF 0F 00  jmp 000F
0037: 26 BF     xor reg-E,reg-E
0039: 9A 00 01  mov reg-4,0x0100
003C: 9E 21 AF  mov reg-C,0xAF21
003F: 80 D5     mov reg-A,[reg-4]
0041: DD        swap reg-A
0042: 12        inc reg-4
0043: 12        inc reg-4
0044: C3        pop reg-6
0045: 0F F5     cmp reg-A,reg-C
0047: EF 56 00  cjmp 0056
004A: 0F DD     cmp reg-A,reg-6
004C: EF 3F 00  cjmp 003F
004F: 9B 00 00  mov reg-6,0x0000
0052: 9A 00 00  mov reg-4,0x0000
0055: A7        exit
0056: 9D 4D 5A  mov reg-A,0x5A4D
0059: 0F EB     cmp reg-6,reg-A
005B: EF 65 00  cjmp 0065
005E: 9B 00 00  mov reg-6,0x0000
0061: 9A 00 00  mov reg-4,0x0000
0064: A7        exit
0065: 9B AD DE  mov reg-6,0xDEAD
0068: 9A EF BE  mov reg-4,0xBEEF
006B: A7        exit

Три команды NOP в начале вероятно для того чтобы сразу почувствовать удовольствие от трассирования 60ти хэндлеров без изменений в контексте ВМ.

При просмотре кода можно увидеть желанную сигнатурку 0xDEADBEEF, которая, как мы помним, проверяется после выполнения ВМ и свидетельствует о том, что мы «Ай, молодца!».

Широко в циклах пользуется стек, причем в цикле обработке данные в стек укладываются, а в цикле чеканья выбираются. Тут нам понадобится информация о том, что стек растет от младших адресов к старшим (PUSH - w_ESP += 2 / POP - w_ESP -= 2). И в стеке на вершине уже находится значение 0x5A4D.

Команды работы с памятью ВМ (mov reg16,[reg16]) имеют доступ только к собственному сегменту данных. И при внимательном рассмотрении девиртуализированого кода можно увидеть три характерных адреса (0000h, 0100h, 0200h):

0006: 9B 00 00  mov reg-6,0x0000
000F: 80 D8     mov reg-0,[reg-6]

0023: 9E 00 02  mov reg-C,0x0200
002A: 80 F7     mov reg-E,[reg-C]

0039: 9A 00 01  mov reg-4,0x0100
003F: 80 D5     mov reg-A,[reg-4]

При этом команда

0032: 53        loop_xor buffer,key[reg-6]

тоже работает с собственным сегментом данных ВМ начиная с адреса 0200h. Т.е. на каждом цикле обработки ввода наш сегмент данных начиная с адреса 0200h из которого мы выбираем значения полностью перексоривается.

С началом сегмента данных ВМ мы уже встречались выше, именно туда мы получили пользовательский ввод после запроса пароля.

Таким образом, мы имеем цикл обработки пользовательского ввода, без контроля длины значения находящегося в буфере, с выходом из цикла при получении 0x3133. Обратить также стоит внимание на то, что ВМ у нас 16-разрядная и на обработку извлекается одновременно два символа пользовательского ввода, однако указатель в цикле сдвигается на единицу.

Следующий цикл сравнивает полученные значения с константным массивом. Цикл заканчивается, когда из константного массива будет извлечено значение 0xAF21, что и даст нам необходимую длину пароля. Последним извлеченным значением из стека ВМ должно быть значение 0x5A4D, т.е. стек после проверки должен быть пустым.

Вроде все ясно и можно потирая руки реализовать генерацию входного пароля. Вот на этой мысли вероятно и обломились все те кому решить этот таск не удалось.

«Почему же так?», спросит пытливый читатель. Дело в том, что мы не рассмотрели еще одну составляющую ВМ, а именно регистры, хотя машина у нас регистровая. Вот тут нам и понадобится вспомнить, что находимся мы в обработчике исключения, снова посмотреть на прототип обработчика и код хэндлера работающего с регистрами

.text:00402323 HNDL_01_MOV_REG_REG:
.text:00402323                 call    GET_TYPE2_2_EBX_ECX
.text:00402328                 add     ebx, 9Ch
.text:0040232E                 add     ebx, [esp+0Ch]
.text:00402332                 movzx   ebx, word ptr [ebx]
.text:00402335                 add     ecx, 9Ch
.text:0040233B                 add     ecx, [esp+0Ch]
.text:0040233F                 mov     [ecx], bx
.text:00402342                 sub     eax, 201h
.text:00402347                 setz    al
.text:0040234A                 jmp     END_

и задуматься о том, что же это за волшебная константа 9Ch.

Оказывается регистры ВМ проецируются на контекст прерванного потока (struct _CONTEXT *ContextRecord) и если посмотреть в эту структуру по смещению 9Ch, то увидим куда именно.

00000000 CONTEXT         struc ; (sizeof=0x2CC, standard type)
00000000 ContextFlags    dd ?
00000004 Dr0             dd ?
00000008 Dr1             dd ?
0000000C Dr2             dd ?
00000010 Dr3             dd ?
00000014 Dr6             dd ?
00000018 Dr7             dd ?
0000001C FloatSave       FLOATING_SAVE_AREA ?
0000008C SegGs           dd ?
00000090 SegFs           dd ?
00000094 SegEs           dd ?
00000098 SegDs           dd ?
0000009C _Edi            dd ?
000000A0 _Esi            dd ?
000000A4 _Ebx            dd ?
000000A8 _Edx            dd ?
000000AC _Ecx            dd ?
000000B0 _Eax            dd ?
000000B4 _Ebp            dd ?
000000B8 _Eip            dd ?
000000BC SegCs           dd ?
000000C0 EFlags          dd ?
000000C4 _Esp            dd ?
000000C8 SegSs           dd ?
000000CC ExtendedRegisters db 512 dup(?)
000002CC CONTEXT         ends

После осознания этого уже и команда

.text:00402082                 sub     edi, 1111h

не станет лишней.

И станет понятна связь между

0065: 9B AD DE  mov reg-6,0xDEAD
0068: 9A EF BE  mov reg-4,0xBEEF

и

.text:0040106A                 cmp     esi, 0DEADBEEFh

Еще часть решавших, вероятно, отсеялась на том, что добавила в цикл обработки ввода вычитание 0х1111.

Для тех кто не понял почему это не верно, последнее замечание: хотя извлеченное из буфера значение будет у нас в регистре reg-0 (отображаемом на EDI) всего в течении одной команды

000F: 80 D8     mov reg-0,[reg-6]
0011: 80 47     mov reg-E,reg-0

цикл в начале обработчика прерывания прогонит нас по кругу от HNDL_03_MOV_REG_MEM16 до HNDL_01_MOV_REG_REG и каждый раз будет вычитаться значение 0х1111, т.е. в цикл обработки ввода нужно добавить вычитание значения 18*0х1111.

Вот теперь, пожалуй, точно все и можно писать восстановление пароля.

 #! /usr/bin/python
# -*- coding: utf-8 -*-
'''
####################################################################
#
#  Plaid CTF 2014
#  Task: paris (reverse 300)
#
#  Password generator
#
'''
import struct

def swap(r0):
    return ((r0 & 0xFF) << 8) | (r0 & 0xFF00) >> 8

def modify(ii):
   global values_
   k = ord(xor_keys[ii])
   k = (k << 8) | k
   for j in range(256):
      values_[j] ^= k

f = open('paris.exe', 'rb')
f.seek(0x790)
master = [0]*29
for ii in range(29):
   master[28-ii] = struct.unpack('H', f.read(2))[0]
f.seek(0x890)
values_ = []
for ii in range(256):
   values_.append(struct.unpack('H', f.read(2))[0])
f.seek(0xFFA)
xor_keys = f.read(32)
f.close()

_stack = [0]*30
_stack[0] = 0x5A4D
_out = 'V'
for i in range(len(master)-1):
   _stack[i+1] = master[i]
   idx = values_.index(master[i]^_stack[i])
   print 'pos %02d: index 0x%02X'%(i,idx)
   # find in range ' '..'z'
   for c1 in range(0x20,0x7A):  
      z = (~swap((((ord(_out[-1])<<8)|c1)-18*0x1111)&0xFFFF))&0xFFFF                          
      zl = 0xFF & z                            
      zh = (0xFF00 & z) >> 8                   
      _idx = (zh ^ zl) & 0xFF                  
      if _idx == idx:
         print 'index 0x%X with first char \'%c\' -- %c%c'%(_idx,_out[-1],_out[-1],chr(c1)) 
         _out += chr(c1)
         break
   modify(i)

print '[+] FLAG:', _out

Отбрасывая протокол вывода, результат будет таким V1rTu4L_M4ch1n3s_4r3_Aw3s0m3!

Содержание архива paris.rar (98.2 KB):

  • paris – сэмпл из задания
  • paris.idb – база для IDA Pro v5.7
  • paris_devm.py – декомпилятор (девиртуализатор)
  • paris_gen.py – генерация флага
  • VM_code – код под ВМ
  • paris_gen.log – протокол генерации флага

А на сьогодні все малятка, любі хлопчики й дівчатка! Дід Панас

Отличная работа!

ximera крут…
https://ctftime.org/writeup/1088
https://ctftime.org/writeup/1089

Идея OKOB’а, народ будем привлекать :slight_smile:

Привлекли.
Я с ВМ сам ещё не встречался, поэтому подумал что это какой то заумленный кеуген, часа 4 реверсил. Разгрёб call table, процедурки куда прыгает, но не понял что это опкоды… кучка других недопониманий. Текст сверху ещё не читал.
Сколько время ушло на парис.ехе?