Recently in CTF Category

We got a file called PPC300 and told to get 10000 points. File is a "Snake" video game written in Python and packet to exe using py2exe. Ofcourse you can play a game for fun and get a flag(or, oops - you can't, read ahead), but that's a good case for imporoving your Python bytecode reversing skills.
First, we extract bytecode from exe using py2exe extract.
Then try to disassemble it using dis, not so readable, but we can see the constants:
'Snake   Score:%s'    #caption template
100                   #points, incremented per eaten food
2000                  #speed tick increment
First idea was to patch a points incrementor constant and get a flag, it didn't work.
I decided to decompile bytecode to a human readable form, tried Uncompyle, then Decompyle with my patch, both didn't work, but decompilation errors were in different places.
I fixed the decompyle using some parser code pieces from Uncompyle and my patch (decompyle.patch) and got a readable source code (snake.p_).
As we can see - flag is generated from "fps" value, not from points. But the original source code has a mistake and you can't get a flag even if you get enough points: "f" is called with 1 parameter, but requires 2:
def f(fps, control):
    x = ((dumps(color) + dumps(control)) + dumps(fontsize))
    t = ''
    for i in x:
        t += hex((ord(i) ^ fps))[2:]

    return t

while mainloop:
    tickFPS = Clock.tick(fps)
    if (fps >= 101):
        screen.fill((0, 0, 0))
        tickFPS = Clock.tick(2000)

I made a script for flag generation and reported the mistake to authors.
from cPickle import dumps

color = (32, 32, 32)
fontsize = 35
fps = 1
control = 251

def f(fps, control):
    x = ((dumps(color) + dumps(control)) + dumps(fontsize))
    t = ''
    for i in x:
        t += hex((ord(i) ^ fps))[2:]

    return t

if __name__ == '__main__':
    while True:
        fps += 1
        control -= 1
        if (fps >= 101):
            print f(fps, control)
Flag: 4d2c56576f2c56576f2c56576f1115546f4b2c5450546f4b2c56506f4b
It was real "structured programming" task. Code contains lots of function, but there no direct xrefs. Control is passed from function to function via pointers in structs.

Typical struct has the following format:
    +0: function pointer
    +4: function arguments
Each structure has a helper procedure that allow to "execute" struct, i.e. call function with specified parameters. For example,
int exec_struct6_runtime_if_2(struc_6 *pStruct)
    return pStruct->proc(pStruct->bWhatChar,
There was 6 different structs that are correspond to input sequence, brainfuck sequence, and control flow management.

File has simple anti-debug (ptrace) trick, and two brainfuck-like sequences (for debug and non-debug sessions). A program interprets brainfuck sequence and modifies only one memory cell: + increments it, > multiplies it by 2, < divides it by 2. Each non-control (+<>) symbol forces interpreter to check user input sequence: current symbol from user input is XOR'ed with values from memory cell. If the result is equal to 'a', then loop is continued and memory cell is cleared. Else, interpreter stops and reports error.

The following code snapshot decodes the flag:
int result = 0;
int counter = 0;

for (int i = 1; i < bf_len; i++)
    char c = bf[i];
    if ((c & 0x80) != 0)
        c = ~c;

    if (c == '+')
    else if (c == '>')
        result >>= 1;
    else if (c == '<')
        result *= 2;
        pass[counter] = 'a' ^ result;
        result = 0;
Without a debugger we get the following: ar3n't_funct10n_p01nt3rs_fun?

Under debugger output is different: d3bugg3rs_ar3_just_t00_us3ful
Obfuscated x64 binary. Very small amount of code is split into chunks that are linked together via jmp | push + jmp and "high-level" constructions like this:
mov rdx, offset InputData
push offset trim_password
mov rcx, offset _strlen
jmp @jmp_via_ecx
mov rdi, rdx
push ecx
jmp do_ret

Bring things together:
mov rdx, offset InputData
push offset trim_password ; retaddr
mov rcx, offset _strlen   ; destination
mov rdi, rdx
jmp ecx
Or, more clear
mov rdi, offset InputData
push offset trim_password ; retaddr
jmp qword ptr [_strlen]   ; destination
After de-obfuscation, code becomes more readable.

First, seed value is calculated: seed = 940903 * 659 * -3;

Then srand(seed) is called, and console is read using fgets(InputData, 80, STDIN). Each symbol from user input (InputData array) is compared with an one symbol of the Key sequence:
Position is determined by rand() calling. In other words,
InputData[i] == Key[rand() % 64] ? continue : exit()
After full comparison, length of InputData is considered, and successful result is only possible if length of InputData is 64.

The following code snapshot allow to encode the flag:
char input[] = "Rh|hx6VPJXwt7b%YUh3|ku9!Nnl#p-B*fjKUrA:r83Alg3KMjoiWA%P-8Xo5R%^:";
char result[sizeof(input)];

int seed = 940903 * 659 * -3;

for (int i = 0; i < sizeof(input) - 1; i++)
    result[i] = input[rand() % 64];
result[sizeof(input) - 1] = '\0';
printf("%s\n", result);
Output: Bg8Ph#xnr||l*YjV|9K#RRfh6XhnhK8*%f:h5AAUgg%t5K3%xRnR%Xh|iU#W6h3k

From the name of the task and the description we guess that it is the data from a card reader similar to the "Square".

The technical details about encoding are desribed here:

In short: Magnetic stripe + Aiken Biphase encoding + ANSI BCD encoding = key

The game is very simple. 2 long strings are shown, you have to select which one is bigger. Short analysis of the strings didn't show any anomalities, so we just decided to collect all the string and remember their positions against each other. So we write an automated player. There are only 500 strings. A naive sorting approach will give us 500*499 comparisons, so we just left it as is for a couple of hours. You can definitely afford it during the 48hrs competition.
I've lost my source code!
Fortunately, I have a test program.
But, Test program is not perfect.
Please, I need your help.

Дан архив 49F69C55C47B4AA87059F3EEF391F5A7 с запакованным ASProtect исполняемым файлом и запароленным ZIP-архивом внутри. Распакуем ASProtect.

(russian version)

No, that's not about Quake. This article will be useful for young teams, who've already tried to participate in CTFs.
If you don't know what CTF (Capture The Flag) means, CTF is a competition in information security field. The goal is getting so called "flags" which score you points.

The competition is usually held in the following formats or their variations:

  • Classic: teams search for vulnerabilities in rival's infrastructure, attack them and retrieve flags, while defending their own infrastructure at the same time.
  • Jeopardy: teams solve tasks of various complexity and
    retrieve flags from them.

A competition lasts for 24-48hrs, non-stop, which requires participants to have strong skills and experience. One of the most crucial factors is the ability to share information in real-time. Thus CTF can be considered a model of a time-condensed process, implying data analysis, brainstorming, vulnerabilities search and exploitation, as well as software development.

У нас есть HTTP сервис, отвечающий на запросы следующим образом:
GET / HTTP/1.0

HTTP/1.0 501 Unsupported method ('GET')
Server: BaseHTTP/0.3 Python/2.7.2+
Date: Sun, 18 Mar 2012 18:50:57 GMT
Content-Type: text/html
Connection: close

<title>Error response</title>
<h1>Error response</h1>
<p>Error code 501.
<p>Message: Unsupported method ('GET').
<p>Error code explanation: 501 = Server does not support this operation.
В глаза бросается Python и BaseHTTP, опыт подсказывает, что это XMLRPC сервер. Пробуем:
>>> import xmlrpclib
>>> s = xmlrpclib.ServerProxy('')
>>> print s.system.listMethods()
['get_answer', 'get_server_var', 'system.listMethods', 'system.methodHelp', 'system.methodSignature']
отлично, вызовем get_answer.
>>> print s.get_answer()
xmlrpclib.Fault: <Fault 1: "<type 'exceptions.TypeError'>:get_answer() takes exactly 4 arguments (0 giv
а с параметрами?
>>> print s.get_answer(1,2,3,4)
xmlrpclib.Fault: <Fault 1: "<type 'exceptions.ValueError'>:Wrong parameters">
попробуем узнать, что же за параметры нам нужны:
>>> print s.system.methodHelp('get_answer')
Returns an answer if params are correct
посмотрим вторую функцию.
>>> print s.system.methodHelp('get_server_var')
Gets a var from the server
попробуем достать ENV?
>>> print s.get_server_var('ENV')
xmlrpclib.Fault: <Fault 1: "<type 'exceptions.NameError'>:name 'ENV' is not defined">
может быть ответ?
>>> print s.get_server_var('get_answer')
<function get_answer at 0x7f94aff416e0>
да ведь это eval! используя встроенные атрибуты, смотрим, что у нас есть интересного:
>>> print s.get_server_var('get_answer.func_globals')
{'__builtins__': <module '__builtin__' (built-in)>, '__file__': '/home/bay/', 'xmlrpclib': <modu
le 'xmlrpclib' from '/usr/lib/python2.7/xmlrpclib.pyc'>, 'SimpleXMLRPCServer': <class SimpleXMLRPCServe
r.SimpleXMLRPCServer at 0x7f94aff376d0>, '__package__': None, 're': <module 're' from '/usr/lib/python2
.7/re.pyc'>, 'get_answer': <function get_answer at 0x7f94aff416e0>, 'get_server_var': <function get_ser
ver_var at 0x7f94aff3f9b0>, '__name__': '__main__', 'server': <SimpleXMLRPCServer.SimpleXMLRPCServer in
stance at 0x7f94b2110f38>, '__doc__': None}
>>> print s.get_server_var('get_answer.func_code.co_varnames')
('a', 'b', 'c', 'd', 'key')
>>> print s.get_server_var('get_answer.func_code.co_consts')
('Returns an answer if params are correct', 79011, 'Wrong parameters', 1702257177, 'The answer is %s')
>>> print s.get_server_var('get_answer.func_code.co_filename')
В константах ключа нет, вероятно он считается динамически, придется реверсить код функции get_answer
>>> import dis
>>> dis.dis(s.get_server_var('get_answer.func_code.co_code'))
дизассемблированный код функции. используя имена переменных и константы полученные ранее, восстановим ее псевдокод:
def get_answer(a, b, c, d):
    if( a + b != 79011): raise "Wrong parameters"
    if( a != b + c ): raise "Wrong parameters"
    if( c*d != 1702257177 ): raise "Wrong parameters"
    key = str(a) + str(b) + str(c) + str(d)
    return 'The answer is %s' % key
Найдем подходящие параметры.
>>> print s.get_answer(39506,39505,1,1702257177)
The answer is 395063950511702257177
Но данный ответ не корректен с точки зрения организаторов. Т.к. система уравнений имеет множество решений, попробуем поискать параметры, которые будут чем-то выделяться.
>>> def get_vals(a):
       b = 79011 - a
       c = a - b
       d = 1702257177/c
       if( c < 0 or 1702257177 % c != 0 ): return None
       return (a,b,c,d)
>>> for i in range(1,79011):
...  if( get_vals(i) != None): print i
>>> get_vals(66666)
(66666, 12345, 54321, 31337)
Флаг: 66666123455432131337 thanks to @snk.
В трех словах:
виртуальная машина, которая выполняет код производящий операции над матрицей, матрица - это представление графа, а алгоритм это поиск кратчайшего пути.
Ответ: Dijkstra

В много слов:
Программа представляет из себя виртуальную машину, с основной функцей:
.text:08048C00 ; char __cdecl vm_exec(struct_vm *vm, int *vm_data)
.text:08048C00 vm_exec         proc near               ; CODE XREF: main+27p
Алгоритм интерпретатора довольно простой, в цикле читается однобайтовый опкод команды, после switch и вызывается функция обработчик нужной команды.
    switch ( *code )
      case 1:
        v2 = code + 1;
        opcode_create_class_1(vm, (char *)vm_data + *(_DWORD *)v2);
        code = v2 + 4;
      case 6:
        v7 = code + 1;
        opcode_add(vm, (int *)vm + *v7, (int *)vm + v7[1]);
        code = v7 + 2;
Команд всего 19, после непродолжительного анализа мы можем написать свой дизассемблер.
def GetMem(start_ea,size):
  r = ""
  for ea in range(start_ea,start_ea+size):
    r += chr(Byte(ea))
  return r

def sign(a):
 if( a >> 15 ):
   return -(65536-a)
 return a

vm_area = 0x0804C080
opcode = [
 {'s':1, 'name':'exit',                 'u':'',         'd':lambda pc,a: "exit" },
 {'s':5, 'name':'class_matrix',         'u':'I',        'd':lambda pc,a: "class_matrix(0x%x) # %d" % (vm_area + a[0],a[0]) },  
 {'s':5, 'name':'class_list',           'u':'I',        'd':lambda pc,a: "class_list(0x%x)   # %d" % (vm_area + a[0],a[0]) },
 {'s':3, 'name':'call_vf1 (get)',       'u':'BB',       'd':lambda pc,a: "matrix%d->vf1_get_val(r%d)" % (a[0],a[1])      }, 
 {'s':3, 'name':'call_vf2 (set)',       'u':'BB',       'd':lambda pc,a: "matrix%d->vf2_set_val(r%d)" % (a[0],a[1])      },
 {'s':3, 'name':'cmp',                  'u':'BB',       'd':lambda pc,a: "cmp r%d,r%d" % (a[0],a[1]) },
 {'s':3, 'name':'add',                  'u':'BB',       'd':lambda pc,a: "add r%d,r%d" % (a[0],a[1]) },
 {'s':3, 'name':'sub',                  'u':'BB',       'd':lambda pc,a: "sub r%d,r%d" % (a[0],a[1]) },
 {'s':3, 'name':'call_vf4 (set col) ',  'u':'BB',       'd':lambda pc,a: "matrix%d->vf4_set_col(r%d)" % (a[0],a[1])    },
 {'s':3, 'name':'call_vf3 (set row)',   'u':'BB',       'd':lambda pc,a: "matrix%d->vf3_set_row(r%d)" % (a[0],a[1])    },
 {'s':2, 'name':'print',                'u':'B',        'd':lambda pc,a: "print r%d" % (a[0]) },
 {'s':3, 'name':'jmp',                  'u':'H',        'd':lambda pc,a: "jmp 0x%x (%d)" % (pc+1+sign(a[0]),a[0]) },    
 {'s':3, 'name':'jz',                   'u':'H',        'd':lambda pc,a: "jz 0x%x (%d)" % (pc+1+sign(a[0]),a[0]) },     
 {'s':3, 'name':'mov reg, #Imm',        'u':'BB',       'd':lambda pc,a: "mov r%d, %d" % (a[0],a[1]) },
 {'s':2, 'name':'inc reg',              'u':'B',        'd':lambda pc,a: "inc r%d" % (a[0]) }, 
 {'s':2, 'name':'dec reg',              'u':'B',        'd':lambda pc,a: "dec r%d" % (a[0]) }, 
 {'s':3, 'name':'list_has_key',         'u':'BB',       'd':lambda pc,a: "list%d->has_key(r%d)" % (a[0],a[1])}, 
 {'s':3, 'name':'list_add_item',        'u':'BB',       'd':lambda pc,a: "list%d->add_item(r%d)" % (a[0],a[1])}, 
 {'s':3, 'name':'mov reg,reg',          'u':'BB',       'd':lambda pc,a: "mov r%d, r%d" % (a[0],a[1]) },

def dis(pc = 0x0804C11C, end_pc = 0x0804C1EF):
  while(pc <= end_pc):
    op = Byte(pc)
    add = struct.unpack( opcode[op]['u'], GetMem(pc+1,opcode[op]['s']-1) )
    print "%x: %s" % (pc,opcode[op]['d'](pc,add) )
    pc += opcode[op]['s']
    if( op == 0 ): break
Опкоды 1 и 2 создают по новому экземпляру классов MatrixGraph и PointerToGraph, у меня они называются для простоты matrix и list. Так же есть несколько опкодов, которые вызывают виртуальные функции этих классов.
Запустим наш дизассемблер получим следующий листинг:
804c11c: class_matrix(0x804c084) # 4
804c121: class_list(0x804c118) # 152
804c126: mov r0, 0
804c129: matrix0->vf2_set_val(r0)
804c12c: mov r0, 6
804c12f: mov r3, 0
804c132: dec r3
804c134: mov r1, 0
804c137: mov r2, 5
804c13a: cmp r1,r2
804c13d: jz 0x804c174 (54)
804c140: list1->has_key(r1)
804c143: jz 0x804c16f (43)
804c146: mov r5, 0
804c149: dec r5
804c14b: cmp r5,r3
804c14e: jz 0x804c154 (5)
804c151: jmp 0x804c16c (26)
804c154: matrix0->vf3_set_row(r1)
804c157: matrix0->vf4_set_col(r1)
804c15a: matrix0->vf1_get_val(r4)
804c15d: matrix0->vf4_set_col(r3)
804c160: matrix0->vf3_set_row(r3)
804c163: matrix0->vf1_get_val(r5)
804c166: cmp r4,r5
804c169: jz 0x804c16f (5)
804c16c: mov r3, r1
804c16f: inc r1
804c171: jmp 0x804c13a (65480)
804c174: matrix0->vf3_set_row(r3)
804c177: matrix0->vf4_set_col(r3)
804c17a: matrix0->vf1_get_val(r4)
804c17d: mov r5, 0
804c180: dec r5
804c182: cmp r5,r4
804c185: jz 0x804c191 (11)
804c188: cmp r4,r5
804c18b: jz 0x804c191 (5)
804c18e: jmp 0x804c1db (76)
804c191: list1->add_item(r3)
804c194: mov r1, 0
804c197: mov r2, 5
804c19a: cmp r1,r2
804c19d: jz 0x804c1d2 (52)
804c1a0: matrix0->vf4_set_col(r3)
804c1a3: matrix0->vf3_set_row(r1)
804c1a6: matrix0->vf1_get_val(r2)
804c1a9: matrix0->vf3_set_row(r3)
804c1ac: matrix0->vf1_get_val(r5)
804c1af: matrix0->vf4_set_col(r1)
804c1b2: matrix0->vf3_set_row(r1)
804c1b5: matrix0->vf1_get_val(r4)
804c1b8: add r2,r5
804c1bb: cmp r2,r4
804c1be: jz 0x804c1ca (11)
804c1c1: matrix0->vf4_set_col(r1)
804c1c4: matrix0->vf3_set_row(r1)
804c1c7: matrix0->vf2_set_val(r2)
804c1ca: inc r1
804c1cc: mov r2, 5
804c1cf: jmp 0x804c19a (65482)
804c1d2: dec r0
804c1d4: mov r2, 0
804c1d7: cmp r0,r2
804c1da: jz 0x804c12f (65364)
804c1dd: mov r2, 0
804c1e0: mov r2, 5
804c1e3: matrix0->vf4_set_col(r2)
804c1e6: matrix0->vf3_set_row(r2)
804c1e9: matrix0->vf1_get_val(r0)
804c1ec: print r0
804c1ee: exit
После анализа приблизительный псевдокод будет следующим:
m = class_matrix(0x804c084);
list = class_list(0x804c118)
    r3 = 0xFFFFFFFF;
    for (r1 = 0; r1 <= 5; r1++) {
        if( list1->has_key(r1) ) continue
        if(0xFFFFFFFF <= r3){
            r3 = r1
        }else if(m[r1,r1] > m[r3,r3]){
            r3 = r1

    if( 0xFFFFFFFF > m[r3,r3] && m[r3,r3]>0xFFFFFFFF){
        while (1); //error !?


    for (r1; r1<=5; r1++) {
        if( m[r3,r1] + m[r3,r3] <= m[r1,r1] ){
            m[r1,r1] = r2

print m[0,0]
Имея подсказку, в виде названия класса MatrixGraph, понимаем что матрица представляет из себя форму хранения графа (см, а сам алгоритм это поиск кратчайшего пути или алгоритм Дейкстры. Ответ к задаче будет: Dijkstra.
В трех словах:
com файл, подмена прерываний, самораспаковка, считываем с клавиатуры пароль, расшифровываем строку
Ответ: Bi11_i5_pr0|_|d_0|=_y0|_|

В много слов:
Выполнение начинается с вызова функции
seg000:0245                 call    sub_1028D
которая является своеобразным шлюзом для вызова других функций из таблицы, которая расположена по адресу 27Fh
seg000:02A8                 mov     ax, [bx+27Fh]
seg000:02AC                 push    ax
сама таблица содержит 7 функций, но часть кода зашифровано
seg000:027F off_1027F       dw offset f1_save_int1c
seg000:0281                 dw offset f2_save_int9
seg000:0283                 dw offset f3_save_int20
seg000:0285                 dw offset f4_restore_int1c
seg000:0287                 dw offset f5_restore_int20
seg000:0289                 dw offset f6_restore_int9
seg000:028B                 dw offset f7_print_string
Вначале управление передается на функцию:
seg000:0179 proc            f1_save_int1c near
Действия её сводится к следующему:
  1. вызов sub_1028D, которая в свою очередь вызовет уже вторую функцию f2_save_int9 (сохранит вектор 9го прерывания)
  2. сохранит вектор 1C прерывания
  3. вызов sub_1028D -> f3_save_int20
  4. установка нового вектора прерывания таймера

Давайте посмотрим новую функцию таймера
seg000:0104 proc            int_1C near 
Первым делом она устанавливает новые обработчики прерываний 0x9 и 0x20, далее она считает контрольную сумму самого себя с адреса 0x248 по 0x27B.
seg000:0115                 mov     bx, offset sub_10248
seg000:0118                 mov     [check_sum], 0
seg000:011D loc_1011D:                              ; CODE XREF: int_1C+26j
seg000:011D                 cmp     bx, offset unk_1027B
seg000:0121                 jz      short loc_1012C
seg000:0123                 mov     al, [bx]
seg000:0125                 add     [check_sum], al
seg000:0129                 inc     bx
seg000:012A                 jmp     short loc_1011D
seg000:012C                 mov     bl, [check_sum]
seg000:0130                 and     bl, 0Fh         ; 8
seg000:0133                 mov     [check_sum], bl
Полученной контрольной суммой расшифровываются (0x248,0x27B), (0x1AA,0x1E1) (0x205,0x214) ниже скрипт для расшифровки:
sum = 0
for ea in range(0x10248,0x1027B):
   sum += Byte(ea)
sum &= 0xF

for ea in range(0x10248,0x1027B):
for ea in range(0x101AA,,0x101E1):
for ea in range(0x10205,0x10214):
Дальше выполнение программы вернется на 0x248, где в цикле с клавиатуры считывается 16 символьный пароль, считается сумма всех символов и этим ключем расшифровывается строка расположенная по адресу 0x02C0. Если сумма расшифрованной программы равно 0ADh, то мы выводим её.
seg000:0260 loc_10260:                              ; CODE XREF: sub_10248+23j
seg000:0260                 mov     bx, offset unk_102BE
seg000:0263                 add     bx, cx
seg000:0265                 xor     [bx+1], dh
seg000:0268                 add     dl, [bx+1]
seg000:026B                 loop    loc_10260
seg000:026D                 cmp     dl, 0ADh
seg000:0270                 jnz     short locret_10279
seg000:0272                 mov     ah, 9
seg000:0274                 mov     dx, 2BEh
seg000:0277                 int     21h             ; DOS - PRINT STRING
seg000:0277                                         ; DS:DX -> string terminated by "$"
Поскольку размер ключа у нам всего 1байт, а последний символ должен быть '$' мы можем перебрать все значения (на самом деле его можем просто вычислить) и найти верный ключ, которым расшифровав строку получим '90 |)33P3r$'

Идем дальше, смотрим прерывания int9, прерывание от клавиатуры. Что оно делает? Читает с контроллера прерываний сканкод нажатой клавишы, после этого суммирует значение первых 16 нажатых клавиш и вызывает функцию расшифровки второй для второй строки, расположеной по адресу 0x02CD.
seg000:01AA proc            decrypt_str2 near       ; CODE XREF: seg000:01DEp
seg000:01AA                                         ; DATA XREF: int_1C:loc_1014Bo
seg000:01AA                 mov     cx, 19h
seg000:01AD loc_101AD:                              ; CODE XREF: decrypt_str2+16j
seg000:01AD                 mov     bx, 2CDh
seg000:01B0                 add     bx, cx
seg000:01B2                 mov     al, [bx]
seg000:01B4                 xor     al, [key1]
seg000:01B8                 rol     al, cl          ; rol(b[i]^key,i)
seg000:01BA                 mov     [bx], al
seg000:01BC                 add     [key2], al
seg000:01C0                 loop    loc_101AD
seg000:01C2                 retn
seg000:01C2 endp            decrypt_str2
def decrypt(s,key):
   r = []
   for i in range(0x19,-1,-1):
      r.append( rol(s[i]^key,i%8) )
   return r

s = []
for ea in range(0x102CD,0x102CD+26):

for i in range(256):
  a = map(chr,decrypt(s,i))
  if( a[0] == '$'):
    print "".join(a)
На выходе получаем строку: '\x19i11_i5_pr0|_|d_0|=_y0|_|$'
Однако, asm код не расшифровает первый символ строки, поэтому оставив его без изменений получим правильный ответ: Bi11_i5_pr0|_|d_0|=_y0|_|

About this Archive

This page is an archive of recent entries in the CTF category.

Fun is the next category.

Find recent content on the main index or look in the archives to find all content.