🛡️

setodaNote CTF Exhibition Writeup (Rev)

2024/10/18に公開

Rev - Helloworld

気が付くと椅子に座っていた。簡単なテストから始めよう。ガラスを隔てて真正面に白衣の女が立っている。君が優秀であることを示してくれ。声は天井のスピーカーから聞こえてくるようだ。心配はいらない。そばにある端末が起動する。どちらにしてもすぐに済む。

添付されたファイルを解析してフラグを得てください。
ファイルは「infected」というパスワード付き ZIP になっています。

問題にはhelloworld.exeが添付されています。
stringsコマンドで意味のある文字列が含まれていないか確認します。

$ strings helloworld.exe 
!This program cannot be run in DOS mode.
C\Rich
.text
`.rdata
@.data
.rsrc
@.reloc
h8!@
ht!@
h(%@
Y_^[
=)0@
u"h,0@
h80@
hH%@
Y_^[
=(0@
=,0@
h,0@
hH0@
>csm
%X0@
%`0@
SVW3
ntel
5ineI
5Genu
t#=`
=d0@
=d0@
=d0@
=d0@
5`0@
_^[3
5X1@
=T1@
%H1@
-D1@
%8 @
%4 @
%< @
%@ @
%d @
%| @
%X @
%l @
%` @
%P @
%H @
%h @
%p @
%t @
%x @
=`0@
Nice try, please set some word when you run me.
flag
Good job, but please set 'flag' when you run me.
hint
Nice try ;)
("/)5(<++
(/'<
=+-;<+
-7,+<=>/-+3
GCTL
.text$mn
.idata$5
.00cfg
.CRT$XCA
.CRT$XCAA
.CRT$XCZ
.CRT$XIA
.CRT$XIAA
.CRT$XIAC
.CRT$XIZ
.CRT$XPA
.CRT$XPZ
.CRT$XTA
.CRT$XTZ
.rdata
.rdata$sxdata
.rdata$zzzdbg
.rtc$IAA
.rtc$IZZ
.rtc$TAA
.rtc$TZZ
.xdata$x
.idata$2
.idata$3
.idata$4
.idata$6
.data
.bss
.rsrc$01
.rsrc$02
__current_exception
__current_exception_context
memset
_except_handler4_common
VCRUNTIME140.dll
__acrt_iob_func
__stdio_common_vfprintf
_seh_filter_exe
_set_app_type
__setusermatherr
_configure_narrow_argv
_initialize_narrow_environment
_get_initial_narrow_environment
_initterm
_initterm_e
exit
_exit
_set_fmode
__p___argc
__p___argv
_cexit
_c_exit
_register_thread_local_exe_atexit_callback
_configthreadlocale
_set_new_mode
__p__commode
_initialize_onexit_table
_register_onexit_function
_crt_atexit
_controlfp_s
terminate
api-ms-win-crt-stdio-l1-1-0.dll
api-ms-win-crt-runtime-l1-1-0.dll
api-ms-win-crt-math-l1-1-0.dll
api-ms-win-crt-locale-l1-1-0.dll
api-ms-win-crt-heap-l1-1-0.dll
QueryPerformanceCounter
GetCurrentProcessId
GetCurrentThreadId
GetSystemTimeAsFileTime
InitializeSListHead
IsDebuggerPresent
UnhandledExceptionFilter
SetUnhandledExceptionFilter
IsProcessorFeaturePresent
GetModuleHandleW
GetCurrentProcess
TerminateProcess
KERNEL32.dll
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level='asInvoker' uiAccess='false' />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>
060M0c0
1*1{1
262K2P2U2v2{2
4H4n4}4
646n6w6
8\8b8
9!9>9
9S:\:d:
;(;1;S;Z;i;s;
<!<)<5<><C<I<S<]<m<}<
=!='=-=3=9=?=E=O=
0$2(202
2<5@5\5`5
$

Nice try, please set some word when you run me.
flag

という文字列から、helloworld.exeを実行する際に引数としてflagを指定すればよいのかもしれません。

$ helloworld.exe flag
flag{free_fair_and_secure_cyberspace}
$

正答:flag{free_fair_and_secure_cyberspace}

Rev - ELF

監獄というより研究室のような施設だった。見る角度が大切なんだ。ガラスで隔てたられた部屋を白衣の男が歩いている。すべてを疑ってみることから始める。そばにある端末の電源が入る。手を動かして検証するというのは実に大事なことだ。

添付されたファイルを解析してフラグを入手してください。

問題にはelfというファイルが添付されています。
問題タイトルからこのファイルの形式はELF、つまりLinux上で実行可能なファイルと予想できます。
…が、このままでは上手く実行できません。

$ ./elf
zsh: exec format error: ./elf
$

そこで、fileコマンドでこのファイルの種類を調べてみましたがよく分かりません。

$ file elf    
elf: data
$

次に、xxdコマンドでバイナリデータを見てみます。

$ xxd elf | head -n 1
00000000: 5858 5858 0201 0100 0000 0000 0000 0000  XXXX............
$

ELF形式なら先頭4バイトは7f45 4c46となるはずなので修正して実行します。

$ xxd elf | head -n 1
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
$ ./elf
flag{run_makiba}
$

正答:flag{run_makiba}

Rev - Passcode

その部屋はまぶしいほどの明かりで照らされていた。ここからが本番だ。白衣の人物が書類に目を落としながらつぶやくように話している。結果がすべてという訳ではないが。そばにある端末が起動する。いい結果を期待している。

添付されたファイルを解析してフラグを得てください。

問題にはpasscodeというファイルが添付されています。
まず、fileコマンドでファイルの種類を調べます。

$ file passcode
passcode: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8be572b7a0563868ee29af143b2df0c7d6b1636d, for GNU/Linux 3.2.0, stripped
$

Linux上で実行可能なファイルのようです。
次に、stringsコマンドで意味のある文字列が含まれているか確認します。

$ strings passcode
/lib64/ld-linux-x86-64.so.2
__isoc99_scanf
puts
putchar
printf
strlen
__cxa_finalize
strcmp
__libc_start_main
libc.so.6
GLIBC_2.7
GLIBC_2.2.5
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
u/UH
[]A\A]A^A_
Enter the passcode: 
%255[^
]%*[^
Invalid passcode.
Invalid passcode. Too short.
Invalid passcode. Too long.
20150109
The passcode has been verified.
Flag is : flag{%s}
Invalid passcode. Nice try.
;*3$"
GCC: (Debian 10.2.1-6) 10.2.1 20210110
.shstrtab
.interp
.note.gnu.build-id
.note.ABI-tag
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.plt.got
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic
.got.plt
.data
.bss
.comment
$

Enter the passcode:
20150109

という文字列から、パスコードとして20150109を入力すればよいのかもしれません。

$ ./passcode         
Enter the passcode: 20150109
The passcode has been verified.

Flag is : flag{20150109}
$

正答:flag{20150109}

Rev - Passcode2

予想以上の結果だった。今日もガラス越しに対象が目を覚ます。ここまでうまくいったことはかつてない。端末に今日のデータを送信する。今度こそうまくいくかもしれない。

添付されたファイルを解析してフラグを得てください。

問題にはpasscode2というファイルが添付されています。
まず、fileコマンドでファイルの種類を調べます。

$ file passcode2
passcode2: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a396332a87a60f8e353e93a001a1a9521673f19d, for GNU/Linux 3.2.0, stripped
$

Linux上で実行可能なファイルのようです。
次に、stringsコマンドで意味のある文字列が含まれているか確認します。

$ strings passcode2
/lib64/ld-linux-x86-64.so.2
__isoc99_scanf
puts
putchar
printf
strlen
__cxa_finalize
__libc_start_main
libc.so.6
GLIBC_2.7
GLIBC_2.2.5
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
u/UH
[]A\A]A^A_
Enter the passcode: 
%255[^
]%*[^
Invalid passcode.
Invalid passcode. Too short.
Invalid passcode. Too long.
The passcode has been verified.
Flag is : flag{%s}
Invalid passcode. Nice try.
;*3$"
GCC: (Debian 10.2.1-6) 10.2.1 20210110
.shstrtab
.interp
.note.gnu.build-id
.note.ABI-tag
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.plt.got
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.dynamic
.got.plt
.data
.bss
.comment
$

今回はパスコードらしき文字列が見当たりません。
仕方がないのでGhidraで分析していきます。
まず、Defined Stringsで「The passcode has been verified.」を検索します。
すると、関数FUN_00101175にてパスコードの正誤判定を行っていることが分かります。

次に、関数FUN_00101175のデコンパイル結果を見てみます。

undefined8 FUN_00101175(void)

{
  int iVar1;
  undefined8 uVar2;
  size_t sVar3;
  byte local_124 [12];
  undefined8 local_118;
  undefined8 local_110;
  undefined8 local_108;
  undefined8 local_100;
  undefined8 local_f8;
  undefined8 local_f0;
  undefined8 local_e8;
  undefined8 local_e0;
  undefined8 local_d8;
  undefined8 local_d0;
  undefined8 local_c8;
  undefined8 local_c0;
  undefined8 local_b8;
  undefined8 local_b0;
  undefined8 local_a8;
  undefined8 local_a0;
  undefined8 local_98;
  undefined8 local_90;
  undefined8 local_88;
  undefined8 local_80;
  undefined8 local_78;
  undefined8 local_70;
  undefined8 local_68;
  undefined8 local_60;
  undefined8 local_58;
  undefined8 local_50;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  ulong local_10;
  
  local_118 = 0;
  local_110 = 0;
  local_108 = 0;
  local_100 = 0;
  local_f8 = 0;
  local_f0 = 0;
  local_e8 = 0;
  local_e0 = 0;
  local_d8 = 0;
  local_d0 = 0;
  local_c8 = 0;
  local_c0 = 0;
  local_b8 = 0;
  local_b0 = 0;
  local_a8 = 0;
  local_a0 = 0;
  local_98 = 0;
  local_90 = 0;
  local_88 = 0;
  local_80 = 0;
  local_78 = 0;
  local_70 = 0;
  local_68 = 0;
  local_60 = 0;
  local_58 = 0;
  local_50 = 0;
  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_124[0] = 0x18;
  local_124[1] = 0x1f;
  local_124[2] = 4;
  local_124[3] = 0x79;
  local_124[4] = 0x4f;
  local_124[5] = 0x5a;
  local_124[6] = 4;
  local_124[7] = 0x18;
  local_124[8] = 0x1a;
  local_124[9] = 0x1b;
  local_124[10] = 0x1e;
  local_124[0xb] = 0;
  printf("Enter the passcode: ");
  iVar1 = __isoc99_scanf("%255[^\n]%*[^\n]",&local_118);
  if (iVar1 == -1) {
    uVar2 = 1;
  }
  else {
    __isoc99_scanf(&DAT_0010202c);
    if ((char)local_118 == '\0') {
      printf("Invalid passcode.");
    }
    else {
      sVar3 = strlen((char *)&local_118);
      if (sVar3 < 0xb) {
        printf("Invalid passcode. Too short.");
      }
      else {
        sVar3 = strlen((char *)&local_118);
        if (sVar3 < 0xc) {
          sVar3 = strlen((char *)&local_118);
          if (sVar3 == 0xb) {
            local_10 = 0;
            while ((sVar3 = strlen((char *)local_124), local_10 < sVar3 &&
                   (*(byte *)((long)&local_118 + local_10) == (local_124[local_10] ^ 0x2a)))) {
              local_10 = local_10 + 1;
            }
            sVar3 = strlen((char *)local_124);
            if (local_10 == sVar3) {
              puts("The passcode has been verified. ");
              printf("Flag is : flag{%s}",&local_118);
            }
            else {
              printf("Invalid passcode. Nice try.");
            }
          }
          else {
            printf("Invalid passcode.");
          }
        }
        else {
          printf("Invalid passcode. Too long.");
        }
      }
    }
    putchar(10);
    uVar2 = 0;
  }
  return uVar2;
}

どうやらlocal_124の各バイトと0x2aのXOR演算の結果を入力値と比較しているようです。
CyberChefで計算してみます。

パスコードが25.Sep.2014であると分かったので、あとはpasscode2を実行するだけです。

$ ./passcode2
Enter the passcode: 25.Sep.2014
The passcode has been verified.

Flag is : flag{25.Sep.2014}
$ 

正答:flag{25.Sep.2014}

Rev - to_analyze

あの施設はなんだったのだろう。ふとした瞬間に思い出す。「秘密情報が含まれているファイルを入手した。特定の環境で実行した場合のみ情報が表示される仕組みのようだが条件が特定できない。解析してみてくれないか。」同僚から連絡が入る。端末を開き受信データを確認する。今日の解析対象が画面に表示される。

添付されたファイルを解析してフラグを入手してください。
ファイルは「infected」というパスワード付き ZIP になっています。

問題にはto_analyze.exeが添付されています。
とりあえず、Windows上で実行してみます。

$ to_analyze.exe
nice try!
$

次に、fileコマンドでファイルの種類を調べます。

$ file to_analyze.exe 
to_analyze.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections
$

.NETで作成されたプログラムのようなので、ILSpyでデコンパイルします。

https://github.com/icsharpcode/ILSpy

デコンパイル結果は以下の通りです。

// dreamcheck, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// a
using System;
using System.IO;
using System.Text;

internal class a
{
  private static void a(string[] A_0)
  {
    byte[] array = new byte[15]
    {
      65, 127, 89, 80, 182, 160, 183, 182, 89, 118,
      119, 116, 177, 189, 177
    };
    for (int i = 0; i < array.Length; i++)
    {
      array[i] ^= 35;
      if (a(array[i], 119))
      {
        array[i] += 3;
      }
      array[i] ^= 21;
      array[i] -= 32;
      array[i] = b(array[i], 39);
    }
    a(Encoding.ASCII.GetString(array), array);
  }

  private static byte b(byte A_0, int A_1)
  {
    switch (A_1)
    {
    case 114:
      A_0 = (byte)(A_0 ^ 0x28u);
      break;
    case 39:
      A_0 = (byte)(A_0 ^ 0x13u);
      break;
    }
    return A_0;
  }

  private static bool a(byte A_0, int A_1)
  {
    if (A_1 == 119)
    {
      if (A_0 == 107 || A_0 == 117 || A_0 == 108 || A_0 == 102 || A_0 == 98)
      {
        return true;
      }
    }
    else if (A_0 == 110 || A_0 == 119 || A_0 == 99 || A_0 == 111 || A_0 == 97 || A_0 == 101 || A_0 == 112 || A_0 == 103 || A_0 == 108 || A_0 == 107 || A_0 == 112 || A_0 == 113)
    {
      return true;
    }
    return false;
  }

  private static void a(string A_0, byte[] A_1)
  {
    if (Directory.Exists(A_0))
    {
      Console.WriteLine("Yes, that's the right answer.");
      byte[] array = new byte[27]
      {
        9, 37, 48, 34, 41, 61, 199, 49, 220, 63,
        115, 59, 220, 200, 46, 115, 57, 220, 214, 50,
        53, 46, 47, 37, 124, 62, 9
      };
      for (int i = 0; i < array.Length; i++)
      {
        array[i] ^= A_1[12];
        array[i] ^= A_1[8];
        array[i] ^= A_1[3];
        array[i] ^= 35;
        if (a(array[i], 113))
        {
          array[i] += 3;
        }
        array[i] ^= 21;
        array[i] -= 32;
        array[i] = b(array[i], 114);
      }
      Console.WriteLine(Encoding.ASCII.GetString(array));
    }
    else
    {
      Console.WriteLine("nice try!");
    }
  }
}

まず、1番上の関数a(string[] A_0)の内容を確認します。

// 引数は文字列A_0
private static void a(string[] A_0)
{
  // バイト列arrayを定義
  byte[] array = new byte[15]
  {
    65, 127, 89, 80, 182, 160, 183, 182, 89, 118,
    119, 116, 177, 189, 177
  };
  // arrayの各要素に以下の処理を実施
  // 1. 35とXOR演算
  // 2. a(array[i], 119)がTrueならば3を加算
  // 3. 21とXOR演算、32を減算
  // 4. b(array[i], 39)を代入
  for (int i = 0; i < array.Length; i++)
  {
    array[i] ^= 35;
    if (a(array[i], 119))
    {
      array[i] += 3;
    }
    array[i] ^= 21;
    array[i] -= 32;
    array[i] = b(array[i], 39);
  }
  // a(Encoding.ASCII.GetString(array), array)を実行
  a(Encoding.ASCII.GetString(array), array);
}

次に、関数a(byte A_0, int A_1)の内容を確認します。

// 引数はバイトA_0、整数A_1
private static bool a(byte A_0, int A_1)
{
  // A_1が119の場合
  if (A_1 == 119)
  {
    // A_0が107、117、108、102、98の場合
    if (A_0 == 107 || A_0 == 117 || A_0 == 108 || A_0 == 102 || A_0 == 98)
    {
      // trueを返す
      return true;
    }
  }
  // A_0が110、119、99、111、97、101、112、103、108、107、112、113の場合
  else if (A_0 == 110 || A_0 == 119 || A_0 == 99 || A_0 == 111 || A_0 == 97 || A_0 == 101 || A_0 == 112 || A_0 == 103 || A_0 == 108 || A_0 == 107 || A_0 == 112 || A_0 == 113)
  {
    // trueを返す
    return true;
  }
  // 上記いずれでもない場合falseを返す
  return false;
}

また、関数b(byte A_0, int A_1)の内容も確認します。

// 引数はバイトA_0、整数A_1
private static byte b(byte A_0, int A_1)
{
  switch (A_1)
  {
  // A_1が114の場合
  case 114:
    // 0x28とXOR演算
    A_0 = (byte)(A_0 ^ 0x28u);
    break;
  // A_1が39の場合
  case 39:
    // 0x13とXOR演算
    A_0 = (byte)(A_0 ^ 0x13u);
    break;
  }
  // A_0を返す
  return A_0;
}

最後に、関数a(string A_0, byte[] A_1)の内容を確認します。

// 引数は文字列A_0とバイト列A_1
private static void a(string A_0, byte[] A_1)
{
  // A_0というフォルダが存在する場合
  if (Directory.Exists(A_0))
  {
    // 「Yes, that's the right answer.」と表示
    Console.WriteLine("Yes, that's the right answer.");
    // バイト列arrayを定義
    byte[] array = new byte[27]
    {
      9, 37, 48, 34, 41, 61, 199, 49, 220, 63,
      115, 59, 220, 200, 46, 115, 57, 220, 214, 50,
      53, 46, 47, 37, 124, 62, 9
    };
    // arrayの各要素に以下の処理を実施
    // 1. A_1[12]、A_1[8]、A_1[3]、35とXOR演算
    // 2. a(array[i], 113)がTrueならば3を加算
    // 3. 21とXOR演算、32を減算
    // 4. b(array[i], 114)を代入
    for (int i = 0; i < array.Length; i++)
    {
      array[i] ^= A_1[12];
      array[i] ^= A_1[8];
      array[i] ^= A_1[3];
      array[i] ^= 35;
      if (a(array[i], 113))
      {
        array[i] += 3;
      }
      array[i] ^= 21;
      array[i] -= 32;
      array[i] = b(array[i], 114);
    }
    //arrayを文字列に変換して表示(おそらくflagを表示)
    Console.WriteLine(Encoding.ASCII.GetString(array));
  }
  // A_0というフォルダが存在しない場合
  else
  {
    // 「nice try!」と表示
    Console.WriteLine("nice try!");
  }
}

上記の流れをまとめると

  1. 関数a(string A_0, byte[] A_1)の内容より、A_0というフォルダが存在するとおそらくflagが表示されます。
  2. 関数a(string A_0, byte[] A_1)は関数a(string[] A_0)内のa(string[] A_0)内のa(Encoding.ASCII.GetString(array), array);で呼び出されます。
  3. つまり、A_0a(string[] A_0)の実行によって生成されるarrayを文字列に変換したものです。

要はa(string[] A_0)で生成されるarrayを知ることができればよいので、C#で実装していきます。

Program.cs
using System;
using System.Text;

class Program
{
  static void Main(string[] args)
  {
    a(new string[] { });
  }

  private static void a(string[] A_0)
  {
    byte[] array = new byte[15]
    {
      65, 127, 89, 80, 182, 160, 183, 182, 89, 118,
      119, 116, 177, 189, 177
    };
    for (int i = 0; i < array.Length; i++)
    {
      array[i] ^= 35;
      if (a(array[i], 119))
      {
        array[i] += 3;
      }
      array[i] ^= 21;
      array[i] -= 32;
      array[i] = b(array[i], 39);
    }
    string result = Encoding.ASCII.GetString(array);
    Console.WriteLine(result);
  }

  private static bool a(byte A_0, int A_1)
  {
    if (A_1 == 119)
    {
      if (A_0 == 107 || A_0 == 117 || A_0 == 108 || A_0 == 102 || A_0 == 98)
      {
        return true;
      }
    }
    else if (A_0 == 110 || A_0 == 119 || A_0 == 99 || A_0 == 111 || A_0 == 97 || A_0 == 101 || A_0 == 112 || A_0 == 103 || A_0 == 108 || A_0 == 107 || A_0 == 112 || A_0 == 113)
    {
      return true;
    }
    return false;
  }

  private static byte b(byte A_0, int A_1)
  {
    switch (A_1)
    {
    case 114:
      A_0 = (byte)(A_0 ^ 0x28u);
      break;
    case 39:
      A_0 = (byte)(A_0 ^ 0x13u);
      break;
    }
    return A_0;
  }
}
$ dotnet run
C:\Users\321txt
$

この実行結果から、「C:\Users\321txt」というフォルダを作成してからto_analyze.exeを実行すればよさそうです。

$ dir C:\Users\ | findstr 321txt
2024/10/17  08:52    <DIR>          321txt
$ to_analyze.exe
Yes, that's the right answer.

flag{Do_y0u_Kn0w_Ursnif?}
$

正答:flag{Do_y0u_Kn0w_Ursnif?}

Discussion