Try   HackMD

Write-up Simple Protocol

User1

Здесь могла быть ваша реклама, но пока тут реклама @ch4nnel1

Original write-up: https://hackmd.io/@osogi/Simple_Protocol

Разбор + история

Сам таск(https://game.ctfcup.ru/tasks/948157b9-8d44-477d-afac-4b46488d89d0)

И еще гитхаб таска -> https://github.com/acisoru/ctfcup-21-quals/blob/main/tasks/reverse/simple-protocol/

Пролог (поиск бирника)

Можем попробовать запустить бинарник, но нам это мало что даст, так как он просто читает что-то с ввода и потом отдает несколько байт.

Давайте посмотрим, что находится у него внутри

[0x000012d2]> pdf
            ; DATA XREF from entry0 @ 0x10a127: int main (int argc, char **argv, char **envp);
│           0x000012d2      f30f1efa       endbr64
│           0x000012d6      55             push rbp
│           0x000012d7      4889e5         mov rbp, rsp
│           0x000012da      488d3d230d00.  lea rdi, [0x00002004]       ; "Hello world!"0x000012e1      e87afdffff     call fcn.000010600x000012e6      b800000000     mov eax, 00x000012eb      5d             pop rbp
└           0x000012ec      c3             ret
[0x000012d2]> s fcn.00001060
[0x00001060]> pdf
            ; CALL XREF from main @ 0x12e111: fcn.00001060 ();
│           0x00001060      f30f1efa       endbr64
└           0x00001064      f2ff255d2f00.  bnd jmp qword [reloc.puts]  ; [0x3fc8:8]=0x1030 fcn.00001030 ; "0\x10"
[0x00001060]> 

Странно, судя по main'у бинарь должен просто вывести hello word, однако ничего подобного не происходит и близко. Попробуем поставить бряку на entry point и запустить. Иииии прога не дойдет до нашей бряки, но будет выполнять какой-то код, а известных мне инитов или чего-то что должно запускаться перед entry нет (на самом деле есть читайте Эпилог), вот так мистика. Ладно допустим, что путем странных манипуляций еще до захода в entry исполняется какой-то другой код. Попробуем запустить приложуху под strace, чтобы хотя бы примерно понять что и как.

По логам последнего видим, что бинарь выполняет как-то слишком много работы с памятью вначале (использует syscall'ы mmap и mprotect), что может подвести нас к идее, что он запакован. Но проверив с помощью die, ничего интересного не получаем.

Хммм Окей, попробуем продебажить радаром до тех пор, пока бинарь не начнет читать наш инпут. Я использовал для этого команду dcs read, которая запускает бинарь до вызова сискола read и ждал пока первый операнд будет равен 0, что означает что он читает из stdin.

После окажемся где-то в районе 0x7f адресов, давайте проверим в каком "мапе" мы очутились

[0x7fb2b1ac72ee]> dm
0x00007fb2b1846000 - 0x00007fb2b1886000 - usr   256K s rw- unk0 unk0
0x00007fb2b1886000 - 0x00007fb2b18ab000 - usr   148K s r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fb2b18ab000 - 0x00007fb2b1a23000 - usr   1.5M s r-x /usr/lib/x86_64-linux-gnu/libc-2.31.so /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fb2b1a23000 - 0x00007fb2b1a6d000 - usr   296K s r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fb2b1a6d000 - 0x00007fb2b1a6e000 - usr     4K s --- /usr/lib/x86_64-linux-gnu/libc-2.31.so /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fb2b1a6e000 - 0x00007fb2b1a71000 - usr    12K s r-- /usr/lib/x86_64-linux-gnu/libc-2.31.so /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fb2b1a71000 - 0x00007fb2b1a74000 - usr    12K s rw- /usr/lib/x86_64-linux-gnu/libc-2.31.so /usr/lib/x86_64-linux-gnu/libc-2.31.so
0x00007fb2b1a74000 - 0x00007fb2b1a78000 - usr    16K s rw- unk1 unk1
0x00007fb2b1a92000 - 0x00007fb2b1a93000 - usr     4K s r-- /memfd: (deleted) /memfd: (deleted)
0x00007fb2b1a93000 - 0x00007fb2b1a94000 - usr     4K s r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fb2b1a94000 - 0x00007fb2b1ab7000 - usr   140K s r-x /usr/lib/x86_64-linux-gnu/ld-2.31.so /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fb2b1ab7000 - 0x00007fb2b1abf000 - usr    32K s r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fb2b1abf000 - 0x00007fb2b1ac0000 - usr     4K s --- unk2 unk2
0x00007fb2b1ac0000 - 0x00007fb2b1ac1000 - usr     4K s r-- /usr/lib/x86_64-linux-gnu/ld-2.31.so /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fb2b1ac1000 - 0x00007fb2b1ac2000 - usr     4K s rw- /usr/lib/x86_64-linux-gnu/ld-2.31.so /usr/lib/x86_64-linux-gnu/ld-2.31.so
0x00007fb2b1ac2000 - 0x00007fb2b1ac3000 - usr     4K s rw- unk3 unk3
0x00007fb2b1ac4000 - 0x00007fb2b1ac6000 - usr     8K s rw- unk4 unk4
0x00007fb2b1ac6000 - 0x00007fb2b1ac7000 - usr     4K s r-- unk5 unk5
0x00007fb2b1ac7000 - 0x00007fb2b1aca000 * usr    12K s r-x unk6 unk6
0x00007fb2b1aca000 - 0x00007fb2b1acc000 - usr     8K s r-- unk7 unk7
0x00007fb2b1acc000 - 0x00007fb2b1ad0000 - usr    16K s rw- unk8 unk8
0x00007ffc88eda000 - 0x00007ffc88efb000 - usr   132K s rw- [stack] [stack]
0x00007ffc88f49000 - 0x00007ffc88f4d000 - usr    16K s r-- [vvar] [vvar]
0x00007ffc88f4d000 - 0x00007ffc88f4f000 - usr     8K s r-x [vdso] [vdso]
0xffffffffff600000 - 0xffffffffff601000 - usr     4K s --x [vsyscall] [vsyscall] ; map.vsyscall_.__x

Вау, сколько тут всего намапилось интересного еще до выполнения основного бинаря. Пока дебажил, еще случайно нашел пару строчек, которые содержали "upx", что приводит к мысле что в какой-то момент тут распаковывался upx файл и все эти подозрительные сегменты/мапы вызваны частично и им. Ну что же, поищем elf header

[0x7fb2b1ac72ee]> / \x7fELF
0x7fb2b1886000 hit11_0 .\u007fELF>.
0x7fb2b1a92000 hit11_1 .\u007fELF>.
0x7fb2b1a92110 hit11_2 .!\u007fELF>`wv.
0x7fb2b1a93000 hit11_3 .v%4!15\u007fELF>.
0x7fb2b1a9b41e hit11_4 .?vH?TH\u007fELFI9FpA.
0x7fb2b1a9b451 hit11_5 .A9~A~\u007fELFA~r.
0x7fb2b1a9b7a2 hit11_6 .jDfH\u007fELFI9F|.
0x7fb2b1a9c1c9 hit11_7 .?vH?H\u007fELFI9FA.
0x7fb2b1a9c1fc hit11_8 .A9~A~\u007fELFgA~.
0x7fb2b1a9c57a hit11_9 .DDH\u007fELFI9FOg.
0x7fb2b1ab74c0 hit11_10 .0-00\u007fELF.
0x7fb2b1ac6000 hit11_11 .\u007fELF>.
0x7ffc88ef9678 hit11_12 .@\u007fELF>.
0x7ffc88f4d000 hit11_13 .\u007fELF>.

Сопоставив эти данные с известными мапами, придем к выводу, что нам скорее всего нужны мапы с unk5 по unk8, так как в них и находится бинарник, который выполняет всю "полезную" работу. Сдампим их

[0x7fb2b1acf6c0]> pr 0x00007fb2b1ad0000-0x00007fb2b1ac6000 @ 0x00007fb2b1ac6000 > unpack_server.elf

Попытка запуска распакованного бинаря ничего не даст,

Однако, закинув его в тот же радар, получим интересный результат, что это "живой", настоящий бинарь.

[0x000013c8]> afl
0x00001060    1 47           entry0
0x000013c8   41 1152         main
0x00001040    1 11           fcn.00001040
0x00001090    4 41   -> 34   fcn.00001090
0x00001209    6 122          fcn.00001209
0x00001185   11 132          fcn.00001185
0x000012a9    1 43           fcn.000012a9
0x000012d4    1 35           fcn.000012d4
0x00001316    1 44           fcn.00001316
0x000012f7    1 31           fcn.000012f7
0x00001149    1 60           fcn.00001149
0x00001342    4 134          fcn.00001342
0x00003894   15 1264         fcn.00003894
0x000019a4    1 22           fcn.000019a4
0x000019ba    4 75           fcn.000019ba
0x000018c0    1 32           fcn.000018c0
0x000018e0    1 24           fcn.000018e0
0x00001961    4 67           fcn.00001961
0x00001848    4 120          fcn.00001848
0x00002e6f   15 1241         fcn.00002e6f
0x000018f8    4 105          fcn.000018f8
0x00003d84    1 22           fcn.00003d84
0x00001a05    1 28           fcn.00001a05
0x00001a21    4 286          fcn.00001a21
0x00001050    1 11           fcn.00001050
0x00001fc1   37 1250         fcn.00001fc1
0x00001f60    1 97           fcn.00001f60
0x00001e99    4 199          fcn.00001e99
0x00001b3f    4 288          fcn.00001b3f
0x000024a3   34 1152         fcn.000024a3
0x00001d7b    4 286          fcn.00001d7b
0x00001c5f    4 284          fcn.00001c5f
0x00001000    3 27           segment.LOAD1
0x00001287    1 34           fcn.00001287
0x00002927   21 1352         fcn.00002927
0x0000334c   21 1352         fcn.0000334c
0x00001030    2 31   -> 28   fcn.00001030
0x00001100    5 57   -> 54   fcn.00001100
0x00001140    5 137  -> 60   fcn.00001140
0x00001283    1 4            fcn.00001283
0x00002923    1 4            fcn.00002923
0x00003348    1 4            fcn.00003348
0x00003da0    4 101          fcn.00003da0
0x00003e10    1 5            fcn.00003e10
0x00003e18    1 13           fcn.00003e18

Анпакнутый elf, если кому-то нужно

Завязка (реверсинг бинарника)

Я конечно мазохист, но не настолько чтобы реверсить что-то в радаре, если это можно сделать в иде, так что погнали закинем распакованный бин в иду, и чуть пореверсим. Тут я быстро расскажу о реверсинге некоторых функций и как это делалось

sub_1185 (imul)

unsigned __int64 __fastcall sub_1185(unsigned __int64 a1, unsigned __int64 a2)
{
  unsigned __int64 v5; // [rsp+10h] [rbp-10h]

  v5 = 0LL;
  while ( a2 )
  {
    if ( (a2 & 1) != 0 )
    {
      if ( v5 < -59LL - a1 )
        v5 += a1;
      else
        v5 -= -59LL - a1;
    }
    if ( a1 < -59LL - a1 )
      a1 *= 2LL;
    else
      a1 = 2 * a1 + 59;
    a2 >>= 1;
  }
  return v5;
}

Функция небольшая, но с наскоку трудно понять, что она делает. Но если же подправить и засунуть этот код в какую-нибудь gcc, чтобы потыкать его, то можно быстро определить, что это простое умножение двух чисел. Обзовем ее imul.

sub_1209 (pow)

__int64 __fastcall pow(__int64 a1, unsigned __int64 a2)
{
  __int64 v3; // rax
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  if ( a2 == 1 )
    return a1;
  v3 = pow(a1, a2 >> 1);
  v4 = imul(v3, v3);
  if ( (a2 & 1) != 0 )
    v4 = imul(v4, a1);
  return v4;
}

Просто видно, что это бинарное возведение в степень.

Syscall funs

__int64 __fastcall read(int fd, void *buf, size_t count)
{
  __asm { syscall; LINUX - }
  return 1LL;
}

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

sub_1149 (malloc)

void *__fastcall _malloc(int a1)
{
  void *v2; // [rsp+10h] [rbp-8h]

  v2 = (void *)((unsigned int)offset_for_empty_data + some_adr_for_malloc);
  offset_for_empty_data += a1;
  return v2;
}

По тому где вызывается функция и что делает, можно легко провести параллели с сишным маллоком.

sub_3894 и sub_2E6F(decode и encdode)

unsigned __int64 __fastcall decode(unsigned int *a1, __int64 a2, _BYTE *a3, unsigned int a4)
{
  __int64 v4; // rdx
  unsigned __int64 result; // rax
  int i; // [rsp+28h] [rbp-258h]
  int j; // [rsp+2Ch] [rbp-254h]
  int k; // [rsp+30h] [rbp-250h]
  unsigned int v10; // [rsp+34h] [rbp-24Ch]
  __int64 v11; // [rsp+40h] [rbp-240h]
  __int64 v12; // [rsp+48h] [rbp-238h]
  int v13[134]; // [rsp+50h] [rbp-230h] BYREF
  unsigned __int64 v14; // [rsp+268h] [rbp-18h]

  v14 = __readfsqword(0x28u);
  memset(v13, 0, 0x210uLL);
  crypt_keygen((__int64)v13, a2, (__int64)a3, a4);
  v11 = 0LL;
  v12 = 0LL;
  for ( i = 31; i >= 0; --i )
  {
    if ( i > 30 )
    {
      *a1 ^= v13[128];
      a1[1] ^= v13[129];
      a1[2] ^= v13[130];
      a1[3] ^= v13[131];
    }
    else
    {
      a1[2] = a1[3] ^ rol4(a1[2], 10) ^ (a1[1] << 7);
      *a1 = a1[3] ^ rol4(*a1, 27) ^ a1[1];
      a1[3] = rol4(a1[3], 25);
      a1[1] = rol4(a1[1], 31);
      a1[3] ^= a1[2] ^ (8 * *a1);
      a1[1] ^= *a1 ^ a1[2];
      a1[2] = rol4(a1[2], 29);
      *a1 = rol4(*a1, 19);
    }
    for ( j = 0; j <= 31; ++j )
    {
      v10 = dword_6820[16 * (i % 8)
                     + ((4 * (unsigned __int8)(a1[2] >> j)) & 4 | (2 * (unsigned __int8)(a1[1] >> j)) & 2 | (*a1 >> j) & 1 | (8 * (unsigned __int8)(a1[3] >> j)) & 8)];
      LODWORD(v11) = v11 | ((v10 & 1) << j);
      HIDWORD(v11) |= ((v10 >> 1) & 1) << j;
      LODWORD(v12) = v12 | (((v10 >> 2) & 1) << j);
      HIDWORD(v12) |= ((v10 >> 3) & 1) << j;
    }
    for ( k = 0; k <= 3; ++k )
    {
      a1[k] = *((_DWORD *)&v11 + k) ^ v13[4 * i + k];
      *((_DWORD *)&v11 + k) = 0;
    }
  }
  v4 = *((_QWORD *)a1 + 1);
  *(_QWORD *)a3 = *(_QWORD *)a1;
  *((_QWORD *)a3 + 1) = v4;
  result = __readfsqword(0x28u) ^ v14;
  if ( result )
    result = stack_fail();
  return result;
}
unsigned __int64 __fastcall encdode(unsigned int *mes, __int64 key, _BYTE *dest, unsigned int len)
{
  unsigned int v4; // ebx
  int v5; // ebx
  int v6; // eax
  unsigned int v7; // ebx
  int v8; // ebx
  int v9; // eax
  int v10; // eax
  int v11; // eax
  __int64 v12; // rdx
  unsigned __int64 result; // rax
  int i; // [rsp+28h] [rbp-258h]
  int j; // [rsp+2Ch] [rbp-254h]
  int k; // [rsp+30h] [rbp-250h]
  unsigned int v18; // [rsp+34h] [rbp-24Ch]
  __int64 v19; // [rsp+40h] [rbp-240h]
  __int64 v20; // [rsp+48h] [rbp-238h]
  int main_key[134]; // [rsp+50h] [rbp-230h] BYREF
  unsigned __int64 v22; // [rsp+268h] [rbp-18h]

  v22 = __readfsqword(0x28u);
  memset(main_key, 0, 0x210uLL);
  crypt_keygen((__int64)main_key, key, (__int64)dest, len);
  v19 = 0LL;
  v20 = 0LL;
  for ( i = 0; i <= 31; ++i )
  {
    for ( j = 0; j <= 3; ++j )
    {
      *((_DWORD *)&v19 + j) = main_key[4 * i + j] ^ mes[j];
      mes[j] = 0;
    }
    for ( k = 0; k <= 31; ++k )
    {
      v18 = dword_6020[16 * (i % 8)
                     + ((4 * (unsigned __int8)((unsigned int)v20 >> k)) & 4 | (2 * (unsigned __int8)(HIDWORD(v19) >> k)) & 2 | ((unsigned int)v19 >> k) & 1 | (8 * (unsigned __int8)(HIDWORD(v20) >> k)) & 8)];
      *mes |= (v18 & 1) << k;
      mes[1] |= ((v18 >> 1) & 1) << k;
      mes[2] |= ((v18 >> 2) & 1) << k;
      mes[3] |= ((v18 >> 3) & 1) << k;
    }
    if ( i > 30 )
    {
      *mes ^= main_key[128];
      mes[1] ^= main_key[129];
      mes[2] ^= main_key[130];
      mes[3] ^= main_key[131];
    }
    else
    {
      v4 = mes[1];
      v5 = rol4(*mes, 13) ^ v4;
      v6 = v5 ^ rol4(mes[2], 3);
      mes[1] = rol4(v6, 1);
      v7 = mes[3];
      v8 = rol4(mes[2], 3) ^ v7;
      v9 = v8 ^ (8 * rol4(*mes, 13));
      mes[3] = rol4(v9, 7);
      v10 = rol4(*mes, 13);
      *mes = rol4(v10 ^ mes[1] ^ mes[3], 5);
      v11 = rol4(mes[2], 3);
      mes[2] = rol4(v11 ^ mes[3] ^ (mes[1] << 7), 22);
    }
  }
  v12 = *((_QWORD *)mes + 1);
  *(_QWORD *)dest = *(_QWORD *)mes;
  *((_QWORD *)dest + 1) = v12;
  result = __readfsqword(0x28u) ^ v22;
  if ( result )
    result = stack_fail();
  return result;
}

Эти две функции существуют, выполняют какую-то крипту. И так как одна похожа на перевернутую другую, то я решил проверить с помощью дебага радаром, обратные ли они, и это оказалось так. Так что было решено что одна криптит, а другая декриптит.

Main

После реверса всех "дочерних" функций получаем примерно такой main

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // edx
  int v4; // eax
  int v5; // esi
  _BYTE dest[2]; // [rsp+6h] [rbp-5Ah] BYREF
  _BYTE count_1[4]; // [rsp+8h] [rbp-58h] BYREF
  unsigned int count; // [rsp+Ch] [rbp-54h] BYREF
  __int64 first_inp; // [rsp+10h] [rbp-50h] BYREF
  unsigned __int64 rand; // [rsp+18h] [rbp-48h] BYREF
  __int64 mes; // [rsp+20h] [rbp-40h] BYREF
  unsigned __int64 a2; // [rsp+28h] [rbp-38h]
  _BYTE *buf; // [rsp+30h] [rbp-30h]
  _BYTE *important_data; // [rsp+38h] [rbp-28h]
  _BYTE *user_secret; // [rsp+40h] [rbp-20h]
  void *out; // [rsp+48h] [rbp-18h]
  char *pathname; // [rsp+50h] [rbp-10h]
  unsigned __int64 v18; // [rsp+58h] [rbp-8h]

  v18 = __readfsqword(0x28u);
  first_inp = 0LL;
  read(0, &first_inp, 8uLL);
  rand = 0LL;
  get_random(&rand, 8uLL, v3);
  mes = pow(0xF549E9B5207189BCLL, rand);
  write(1, &mes, 8uLL);
  a2 = 0LL;
  a2 = pow(first_inp, rand);
  key_ = _malloc(16);
  crypt_xor(key_, a2);
  while ( 1 )
  {
    while ( 1 )
    {
      buf = _malloc(32);
      important_data = _malloc(32);
      read(0, buf, 0x20uLL);
      decode((unsigned int *)buf, (__int64)key_, important_data, 0x10u);
      decode((unsigned int *)buf + 4, (__int64)key_, important_data + 16, 0x10u);
      if ( *important_data != 0x22 || important_data[1] != 0x33 )
        exit();
      v4 = (unsigned __int8)important_data[2];
      if ( v4 != 4 )
        break;
      if ( fd )
        exit();
      if ( !flag_ok )
        exit();
      count = 0;
      _memcpy(&count, important_data + 3, sizeof(count));
      if ( count > 0x18 )
        exit();
      user_secret = _malloc(count);
      _memcpy(user_secret, important_data + 7, count);
      if ( !check_secret(user_secret, count) )
        exit();
      out = encode_flag(glob_for_flag, leng_of_flag);
      write(1, out, (leng_of_flag & 0xFFFFFFF0) + 16);
    }
    if ( (unsigned __int8)important_data[2] > 4u )
      break;
    switch ( v4 )
    {
      case 3:
        if ( !fd || fd == -1 )
          exit();
        close(fd);
        fd = 0;
        break;
      case 1:
        count = 0;
        _memcpy(&count, important_data + 3, sizeof(count));
        if ( count > 0x18 )
          exit();
        pathname = (char *)_malloc(count);
        v5 = (_DWORD)important_data + 7;
        _memcpy(pathname, important_data + 7, count);
        if ( fd )
          exit();
        fd = fopen(pathname, v5);
        if ( fd == -1 )
          exit();
        break;
      case 2:
        *(_WORD *)dest = 0;
        *(_DWORD *)count_1 = 0;
        count = 0;
        _memcpy(count_1, important_data + 3, sizeof(count_1));
        if ( *(_DWORD *)count_1 > 0x18u )
          exit();
        _memcpy(dest, important_data + 7, sizeof(dest));
        _memcpy(&count, important_data + 9, sizeof(count));
        if ( count > 0x80 )
          exit();
        glob_for_flag = (unsigned int *)_malloc(count);
        if ( !fd || fd == -1 )
          exit();
        read(fd, glob_for_flag, count);
        flag_ok = 1;
        leng_of_flag = count;
        break;
      default:
        goto LABEL_38;
    }
  }
LABEL_38:
  exit();
}

Из него мы можем понять, что сначала мы подаем число, которое используется для генерации "ключа". После отсылаем зашифрованные блоки, которые при расшифровке складываются в примерно такой протокол связи, по нулевому смещению находится хедер в виде 2 байт \x22\x33.

Operation\Offset +2 (operation code) +3 +7 +9
fopen 1 len of path to file (4 bytes) filepath (len bytes) -
read file 2 some padding <=0x18 (4 bytes) another padding (2 bytes) how much read (4 bytes)
close
(simply close file, need for last)
3 - - -
write data from file 4 len of secret (4 bytes) secret (len bytes)

ДБ иды с этим файлом

Кульминация (кодинг "клиента")

Вообще зная протокол, написать клиент не должно составить труда. Могут возникнуть только проблемы с тем, что мы не знаем степень, в которую возводится число, используемое для генерации ключа. Но ее можно либо попробовать высчитать, на основе известных данных, либо же заслать 1 или 0, так как что первое, что последнее в любой степени дают сами себя -> мы всегда будем знать, число которое использовалось для генерации ключа.

И так же возникают трудности с автоматическим декриптом и инкриптом сообщений. Что бы решить эту проблему у меня в голове было 3 способа:

  • воспользоваться фридой
  • заинжектить so
  • воспользоваться r2pipe rzpipe

Первый вариант я не выбрал так как, до сих пор не могу в фриду. Второй вариант мне не понравился, так как нужно писать сокеты внутри so, либо шаманить с обменом инфой между so и сторонним приложением. Так что я выбрал самый безболезненный, но костыльный вариант - rzpipe. И конечно буду кодить на питоне так как это быстрее всего. Разберем как же это все было реализовано. Для начала разберем конструктор и какие переменные вводятся

class Crypter:
# заводим класс криптор
    def __init__(self, file_name):
        rflags = ["-dw"]
        r = rzpipe.open(file_name, flags=rflags) 
        # запускаем ризин в дебаг и райт моде 
        for _ in range(5):
            r.cmd("dcs read")
        # ждем пятого рида, так как пятый рид всегда наш
        # тот который находится внутри распакованого бинарника
		
        a=r.cmd("dr rip")
        a = int(a, 16)
        # получаем текущий рип, чтобы посчитать смещения
        # # + основные адреса 
		
        self.off = a-0x12EE
        # в бинаре рид назодится по адресу 0x12EE
        # путем несложной математики находим смещение
        self.r=r
		
		
        self.encode_addr=0x2E6F+self.off
        self.encode_addr_end = self.encode_addr+0x4d8
		
        self.decode_addr=0x3894+self.off
        self.decode_addr_end=self.decode_addr+0x4ef

        self.keyfun_addr=0x1342+self.off
        self.keyfun_addr_end=0x13C7+self.off
        # высчитываем адреса нужных функций и их конца
		
        m =0x9038+self.off
        self.mem_addr=int(r.cmdj(f"?j [{m}]")["uint64"])
        # находим адрес используемый кастомным malloc'ом
        # будем использовать его, чтобы знать куда можно писать

Дальше разберу реализацию одной из функций, остальные работают схожим образом

def encrypt(self, message, key):
	r=self.r
	# получаем туже сессию ризина

	r.cmd(f"dr rip={self.encode_addr}")
	# прыгаем на адрес функции

	r.cmd(f"dr rcx=0x10") #len
	# передаем в аргументы длину декодируемого сообщения

	dest_addr = self.mem_addr+0x1000
	r.cmd(f"dr rdx={dest_addr}")
	# передаем адрес куда сохранить расшифрованное сообщение

	key_addr = self.mem_addr+0x1100
	r.cmd(f"dr rsi={key_addr}")
	# передаем адрес по которому будет находится ключ
	
	hexkey=key.to_bytes(16, byteorder=bo).hex()
	r.cmd(f"wx {hexkey} @ {key_addr}")
	# записываем ключ по этому адресу

	mes_addr = self.mem_addr+0x1200
	r.cmd(f"dr rdi={mes_addr}")
	# передаем адрес зашифрованного сообщения

	hexmes=message.hex()
	r.cmd(f"wx {hexmes} @ {mes_addr}")
	# записывем зашифрованное сообщение по этому адресу
	
	r.cmd(f"dcu {self.encode_addr_end}")
	# продолжаем выполнение до конца функции
	
	b = r.cmd(f"p8 16 @ {dest_addr}")
	# достаем значение по адресу куда попадает расшифрованное сообщение
	return bytes.fromhex(b)

Полный код клиента можно найти тут P.S. Чейнд не бей за использрвание pwntools, я хотел заfb'шить таск, а это единственное, что я помнил

Развязка (получение "секрета")

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

Вот вроде бы мы написали функции декрипта и инкрипта, все работает. Начинаем писать код для получения флага, как вдруг видим, что чтобы получить байты файла, в протоколе есть еще проверка какого-то секрета. Она реализована следующим образом.

__int64 __fastcall secret_sub(_BYTE *secr, int len, unsigned int result)
{
  while ( len-- )
    result = (result << 8) ^ some_array[(unsigned __int8)(HIBYTE(result) ^ *secr++)];
	//some_array - массив из 256 dword'ов 
  return result;
}

_BOOL8 __fastcall check_secret(_BYTE *secr, unsigned int len)
{
  return (unsigned int)secret_sub(secr, len, 0xFFFFFFFF) == 0xDF705098;
}

В принципе тут почти сразу видно, что это можно взломать, так как нам известен желаемый результат (0xDF705098) и при result << 8 последний байт 0, то можно найти среди some_array элемент, который тоже оканчивается на тот же байт, что и желаемый результат. Дальше сделать так 4 раза (так как 4 байта) + сделать поправочку на ксор, оформить это все в скрипт и вуаля мы подобрали нужный секрет, и им оказался \xca\xdb\x88\x2d

Добавляем наш секрет в клиент, запускаем его. На локалке все робит, отлично, запускаем на сервер и получаем флаг.

Решение

  • понять, что по умолчанию в данном нам бинаре, почти нет ничего интересного
  • найти, расшифровать, сдампить или еще каким-то образом получить "настоящий" бинарь
  • разреверсить его (файл для иды с ± красивыми именами)
    • разобраться в протоколе
    • понять, что крипто-функции взаимно обратные
  • написать клиент для получения флага
    • каким-то образом реализовать крипто-функции сервера
    • реализовать протокол
    • найти секрет для получения флага (скрипт)
  • получить флаг

Авторское решение туть -> https://github.com/acisoru/ctfcup-21-quals/blob/main/tasks/reverse/simple-protocol/README.md

Эпилог

Я посмотрел, потыкал почему у нас запускается какой-то другой код в начале бинаря, и прога даже не доходит до entry point'а. Как оказалось это вызвано тем что в коде был преинит участок (подробнее о всяких штуках происходящих перед загрузкой main'а можно почитать тут https://habr.com/ru/post/339698/), и радар хоть его и обнаружил, но справился плоховато: никак не обозвал эту функцию + плохо ее проанализировал - сильно ее обрезал (обрезал прям всю), в отличие от ризина, который справился с этим делом лучше

В очередной раз доказано, что ризин>радара. На самом деле у меня иногда тупит и тот и другой, так что я пользуюсь ими посменно, на одном таске могу по приколу юзать то одно, то другое.


Кого-то еще могло заинтересовать почему на картинке, превьюшке, какие-то аниме чувачки и как это связано с таском. Ну вот, это все из-за того, что я загуглил "protocol" нашел такую пикчу к какой-то игре, и мне понравилось как это выглядит.