LoginSignup
1
0

picoCTF2024 Reverse Engineeringのwriteup

Posted at

picoCTF初参加

 picoCTF2024に参加し、483位/4625pointsでした。
スクリーンショット (47).png
 CTFを始めて約一年半となりますが、picoCTFに初参加してreversingを完走できたので、備忘録としてreversingだけでもwriteupを記しておきます。
 初心者ゆえ泥臭い解き方ですがご容赦ください。
 使用ツール:基本ghidra、あとexeはIDA free、elfはgdb

1.packer

 ELFファイルが渡される。起動すると
   Enter the password to unlock this file:
と表示される。
 タイトルがもろpackerなのでとりあえずstringsするとUPXの文字あり。

UPX.exe -d out

で解凍。もう一回stringsすると

Enter the password to unlock this file:
You entered: %s
Password correct, please see flag: 7069636f4354467b5539585f556e5034636b314e365f42316e34526933535f39343130343638327d

の表記がありdecodeしてフラグゲット。

2.FactCheck

 またELFファイル。ghidraで中身を見てみると、

tsIcESaIcEEC1EPKcRKS3_(auStack_248,"picoCTF{wELF_d0N3_mate_ ",&uStack_249);

と途中までフラグが書いてある。
 その先はif文で細かく分岐させていて、条件に合致すればDAT部の中身をフラグに追記していく流れだと分かる。
 分岐を計算して追うのがめんどいのでghidraのデバッガでgdbを動かす。
スクリーンショット (47).png
  通ったところだけDAT部から値引っ張ってきて加えていったらフラグになった。

3.WinAntiDbg0x100

 ヒントに確か「debuggerで開いてね!」みたいなことが書いてあったのでIDA freeで開いてみる。そのまま走らせるとOutputのウィンドウに

      (_)          / ____|__   __|  ____| 
 _ __  _  ___ ___ | |       | |  | |__    
| '_ \| |/ __/ _ \| |       | |  |  __|   
| |_) | | (_| (_) | |____   | |  | |      
| .__/|_|\___\___/ \_____|  |_|  |_|      
| |                                       
|_|                                       
 Welcome to the Anti-Debug challenge!

と出てきた。かわいい。続いて

### Oops! The debugger was detected. Try to bypass this check to get the flag!

と表示されて終わったので、この直前の分岐ポイントにbreakpointを張り、jzからjnzに書き換えて走らせるとフラグが表示された。
スクリーンショット (49).png

4.Classic Crackme 0x100

 これだよ!これ!一番大変だったやつ!
 まずghidraで逆コンパイルしてもらう。

ghidra
undefined8 main(void)

{
  int iVar1;
  size_t sVar2;
  char local_a8 [64];
  undefined8 local_68;
  undefined8 local_60;
  undefined8 local_58;
  undefined8 local_50;
  undefined8 local_48;
  undefined7 local_40;
  undefined4 uStack_39;
  uint local_2c;
  uint local_28;
  char local_21;
  uint local_20;
  uint local_1c;
  uint local_18;
  int local_14;
  int local_10;
  int local_c;
  
  local_68 = 0x7570626770636871;
  local_60 = 0x7570656767616277;
  local_58 = 0x6f7762787473686c;
  local_50 = 0x7a6b726766776177;
  local_48 = 0x626e62636374736a;
  local_40 = 0x6c67706b656873;
  uStack_39 = 0x657a6c;
  setvbuf(stdout,(char *)0x0,2,0);
  printf("Enter the secret password: ");
  __isoc99_scanf(&DAT_00402024,local_a8);
  local_c = 0;
  sVar2 = strlen((char *)&local_68);
  local_14 = (int)sVar2;
  local_18 = 0x55;
  local_1c = 0x33;
  local_20 = 0xf;
  local_21 = 'a';
  for (; local_c < 3; local_c = local_c + 1) {
    for (local_10 = 0; local_10 < local_14; local_10 = local_10 + 1) {
      local_28 = (local_10 % 0xff >> 1 & local_18) + (local_10 % 0xff & local_18);
      local_2c = ((int)local_28 >> 2 & local_1c) + (local_1c & local_28);
      iVar1 = ((int)local_2c >> 4 & local_20) +
              ((int)local_a8[local_10] - (int)local_21) + (local_20 & local_2c);
      local_a8[local_10] = local_21 + (char)iVar1 + (char)(iVar1 / 0x1a) * -0x1a;
    }
  }
  iVar1 = memcmp(local_a8,&local_68,(long)local_14);
  if (iVar1 == 0) {
    printf("SUCCESS! Here is your flag: %s\n","picoCTF{sample_flag}");
  }
  else {
    puts("FAILED!");
  }
  return 0;
}

 入力された値を計算して、演算結果をlocal_68と比べているっぽい。他はダミーか?
 とりあえず最終的にlocal_68の値になればいいのか?と考えてpythonに書き直し、sympyでsolveしてみるがちゃんとした値が出ない。よく見ると計算の最後の行で+iVar1と-iVar1が行われていてこのままやっても定数になりそう。しかもcharを足している。分かんね~C言語(に似た疑似言語らしいけど)分かんね~
 埒が明かないのでgdbで起動して変数の中身を確認することにする。

$ gdb crackme100
(gdb) b main
(gdb) run
(gdb) info local

 これで

output = "@\000@\000\000\000\000\000<(\376\367\377\177\000\000\360\006\000\000\000\000\000\000\351\340\377\377\377\177\000\000\000\020\374\367\377\177\000\000\000\000\000\001\001\001\000\000\002\000"
input = '\000' <repeats 48 times>, "\001\000"
i = 0
len = 0
secret1 = 4096
secret2 = 0
secret3 = 100
fix = 0 '\000'

8個のlocal変数が確保されていることが分かった。最終的にmemcmpしているのはおそらくoutputとinputなので、これを

(gdb) watch output
(gdb) watch input

でwatchに追加してcontinueし続ける。gdbのwatch、値が変わったところで止めてくれるのでとても便利。途中でEnter the secret password:が表示されるので適当にaaaaaaaaとか入れてみる。
 このままcontinueし続けるとinputの文字が処理されていって、最終的に比較のための値に置き換わる……と思ったが、なんか雲行きが怪しい。

(gdb) info local
i = 49
output = "qhcpgbpuwbaggepulhstxbwowawfgrkzjstccbnbshekpgllze"
input = "addgdggjQTTWTWWZQTTWTWWZTWWZWZZ]QTTWTWWZTWWZWZZ]UW"
i = 2
len = 50
secret1 = 85
secret2 = 51
secret3 = 15
fix = 97 'a'

 両方50文字ある。なんで?
 ここで思い違いに気が付く。outputの先に、上記のghidraの逆コンパイルのlocal_60以降の変数の中身が入っている。local_68からuStack_39まですべて比較用の値だったわけである。C言語分かんね~(2回目)
 とはいえやることは変わらない。Enter the secret password:に今度はaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaと入力して処理結果を見てみる。

(gdb) info local
i = 49
output = "qhcpgbpuwbaggepulhstxbwowawfgrkzjstccbnbshekpgllze"
input = "addgdggjdggjgjjmdggjgjjmgjjmjmmpdggjgjjmgjjmjmmpgj"
i = 2
len = 50
secret1 = 85
secret2 = 51
secret3 = 15
fix = 97 'a'

 ghidraの逆コンパイルを見てもaから一文字ずつindexに準拠して処理を行っており、中身から定数分ずらしたように見える。
 よって、inputと同じだけoutputを戻していく。

crackme.py
cr_output = list("qhcpgbpuwbaggepulhstxbwowawfgrkzjstccbnbshekpgllze")
cr_input = list("addgdggjdggjgjjmdggjgjjmgjjmjmmpdggjgjjmgjjmjmmpgj")
difflist = []
passphrase = []
for s in cr_input:
    diff = ord(s)-ord("a")
    difflist.append(diff)
for i,s in zip(difflist,cr_output):
    res = ord(s)-i
    if res < 97:
        res = res + 26
    passphrase.append(chr(res))
print("".join(passphrase))

 でこれで出てきた値をgdbでまた入力。処理してもらう。

(gdb) info local
i = 49
output = "qhcpgbpuwbaggepulhstxbwowawfgrkzjstccbnbshekpgllze"
input = "qhcpgbpuwbaggepulhstxbwowawfgrkzjstccbnbshekpgllze"
i = 2
len = 50
secret1 = 85
secret2 = 51
secret3 = 15
fix = 97 'a'

 というわけで、改めてncで繋いでフラグがもらえた。
 実はこれを解いたのが最終日の昼休みで、スクリプト書いてたら間に合わないと思って上司に白い目で見られながら50文字手で戻した思い出がある。スクリプトを書いてください。そっちのが全然早いから。

5.weirdSnake

 pythonバイトコードが渡される。TCP1Pでも出たので二度目まして。こちらのページを参考にしました。

  1          0 LOAD_CONST               0 (4)
             2 LOAD_CONST               1 (54)
             4 LOAD_CONST               2 (41)
             6 LOAD_CONST               3 (0)
             8 LOAD_CONST               4 (112)
            10 LOAD_CONST               5 (32)
            12 LOAD_CONST               6 (25)
            14 LOAD_CONST               7 (49)
            16 LOAD_CONST               8 (33)
            18 LOAD_CONST               9 (3)
            20 LOAD_CONST               3 (0)
            22 LOAD_CONST               3 (0)
            24 LOAD_CONST              10 (57)
            26 LOAD_CONST               5 (32)
            28 LOAD_CONST              11 (108)
            30 LOAD_CONST              12 (23)
            32 LOAD_CONST              13 (48)
            34 LOAD_CONST               0 (4)
            36 LOAD_CONST              14 (9)
            38 LOAD_CONST              15 (70)
            40 LOAD_CONST              16 (7)
            42 LOAD_CONST              17 (110)
            44 LOAD_CONST              18 (36)
            46 LOAD_CONST              19 (8)
            48 LOAD_CONST              11 (108)
            50 LOAD_CONST              16 (7)
            52 LOAD_CONST               7 (49)
            54 LOAD_CONST              20 (10)
            56 LOAD_CONST               0 (4)
            58 LOAD_CONST              21 (86)
            60 LOAD_CONST              22 (43)
            62 LOAD_CONST              23 (104)
            64 LOAD_CONST              24 (44)
            66 LOAD_CONST              25 (91)
            68 LOAD_CONST              16 (7)
            70 LOAD_CONST              26 (18)
            72 LOAD_CONST              27 (106)
            74 LOAD_CONST              28 (124)
            76 LOAD_CONST              29 (89)
            78 LOAD_CONST              30 (78)
            80 BUILD_LIST              40
            82 STORE_NAME               0 (input_list)

 ここまではlistに数列を入れているっぽいので、順番に加えてリストを作る。

input_list = [4, 54, 41, 0, 112, 32, 25, 49, 33, 3, 0, 0, 57, 32, 108, 23, 48, 4, 9, 70, 7, 110, 36, 8, 108, 7, 49, 10, 4, 86, 43, 104, 44, 91, 7, 18, 106, 124, 89, 78]

 そのあと、

  2          84 LOAD_CONST              31 ('J')
            86 STORE_NAME               1 (key_str)

 3          88 LOAD_CONST              32 ('_')
            90 LOAD_NAME                1 (key_str)
            92 BINARY_ADD
            94 STORE_NAME               1 (key_str)

 4          96 LOAD_NAME                1 (key_str)
            98 LOAD_CONST              33 ('o')
           100 BINARY_ADD
           102 STORE_NAME               1 (key_str)

 5         104 LOAD_NAME                1 (key_str)
           106 LOAD_CONST              34 ('3')
           108 BINARY_ADD
           110 STORE_NAME               1 (key_str)

 6         112 LOAD_CONST              35 ('t')
           114 LOAD_NAME                1 (key_str)
           116 BINARY_ADD
           118 STORE_NAME               1 (key_str)

ではkey_strという変数に文字を一文字ずつ追加しているのが分かる。よって

key_str = 'J_o3t'

とする。(間違っているが後述)

  9         120 LOAD_CONST              36 (<code object <listcomp> at 0x7ff3b9776d40, file "snake.py", line 9>)
            122 LOAD_CONST              37 ('<listcomp>')
            124 MAKE_FUNCTION            0
            126 LOAD_NAME                1 (key_str)
            128 GET_ITER
            130 CALL_FUNCTION            1
            132 STORE_NAME               2 (key_list)

 "snake.py"のline 9を呼び出してkey_strからkey_listをつくるらしい。

Disassembly of at 0x7ff3b9776d40, file "snake.py", line 9>
 9           0 BUILD_LIST               0
             2 LOAD_FAST                0 (.0)
       >>    4 FOR_ITER                12 (to 18)
             6 STORE_FAST               1 (char)
             8 LOAD_GLOBAL              0 (ord)
            10 LOAD_FAST                1 (char)
            12 CALL_FUNCTION            1
            14 LIST_APPEND              2
            16 JUMP_ABSOLUTE            4
       >>   18 RETURN_VALUE

charをordにするだけか。

key_list = []
for key in list(key_str):
    key_list.append(ord(key))

元に戻って

 11     >>  134 LOAD_NAME                3 (len)
            136 LOAD_NAME                2 (key_list)
            138 CALL_FUNCTION            1
            140 LOAD_NAME                3 (len)
            142 LOAD_NAME                0 (input_list)
            144 CALL_FUNCTION            1
            146 COMPARE_OP               0 (<)
            148 POP_JUMP_IF_FALSE      162

 12         150 LOAD_NAME                2 (key_list)
            152 LOAD_METHOD              4 (extend)
            154 LOAD_NAME                2 (key_list)
            156 CALL_METHOD              1
            158 POP_TOP
            160 JUMP_ABSOLUTE          134

 key_listのlenとinput_listのlenを比べて、key_listの方が短ければkey_listをkey_listでextendしている。つまりkey_listをinput_listより長くなるまでループさせて伸ばす。

while True:
   if len(key_list) < len(input_list):
       key_list.extend(key_list)
   else:
       break
 15     >>  162 LOAD_CONST              38 (<code object <listcomp> at 0x7ff3b9776df0, file "snake.py", line 15>)
            164 LOAD_CONST              37 ('<listcomp>')
            166 MAKE_FUNCTION            0
            168 LOAD_NAME                5 (zip)
            170 LOAD_NAME                0 (input_list)
            172 LOAD_NAME                2 (key_list)
            174 CALL_FUNCTION            2
            176 GET_ITER
            178 CALL_FUNCTION            1
            180 STORE_NAME               6 (result)

input_listとkey_listをzipして"snake.py", line 15を読み込み、resultを作っている。

Disassembly of at 0x7ff3b9776df0, file "snake.py", line 15>
 15           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                16 (to 22)
              6 UNPACK_SEQUENCE          2
              8 STORE_FAST               1 (a)
             10 STORE_FAST               2 (b)
             12 LOAD_FAST                1 (a)
             14 LOAD_FAST                2 (b)
             16 BINARY_XOR
             18 LIST_APPEND              2
             20 JUMP_ABSOLUTE            4
        >>   22 RETURN_VALUE

listを用意。for文に引数(.0)を入れて、aとbという変数を用意し、これをxorしてlistにappendするもの。

result = []
for a,b in zip(input_list,key_list):
    result.append(a^b)

最後に、

 18         182 LOAD_CONST              39 ('')
            184 LOAD_METHOD              7 (join)
            186 LOAD_NAME                8 (map)
            188 LOAD_NAME                9 (chr)
            190 LOAD_NAME                6 (result)
            192 CALL_FUNCTION            2
            194 CALL_METHOD              1
            196 STORE_NAME              10 (result_text)
            198 LOAD_CONST              40 (None)
            200 RETURN_VALUE

 これを順番に処理して

result_text = ''.join(map(chr,result))
print(result_text)

こう。

> NiF3jF^wJ_V]ok:2M1K;Mne7"a7ChsX5j:

 ……?フラグにならないなあ。
 "picoCTF{"で始まることが期待されるので、input_listとpicoCTF{でxorしてみる(ズル)
スクリーンショット (51).png
 なんかkey_strの順番が違う。
 最初にkey_strに文字を追加していったとき、「前に追加するか後ろに追加するか」を完全に無視していたことに気がつく。
というわけで改めて、

snake.py
input_list = [4, 54, 41, 0, 112, 32, 25, 49, 33, 3, 0, 0, 57, 32, 108, 23, 48, 4, 9, 70, 7, 110, 36, 8, 108, 7, 49, 10, 4, 86, 43, 104, 44, 91, 7, 18, 106, 124, 89, 78]
key_str = 't_Jo3'
key_list = []
for key in list(key_str):
  key_list.append(ord(key))
print(ord,input_list)
while True:
  if len(key_list) < len(input_list):
      key_list.extend(key_list)
  else:
      break
result = []
for a,b in zip(input_list,key_list):
  result.append(a^b)
result_text = ''.join(map(chr,result))
print(result_text)

これでフラグゲット。

6.WinAntiDbg0x200

 え!これで300点もらえるんですか!?みたいな問題。
 0x100と同様にIDA freeで起動。今度は管理者権限つき。
グラフを見ていくと、「.text:009B1832 push offset aOopsTheDebugge ; "### Oops! The debugger was detected. Tr"」とdebugが失敗したことを表す文が挿入されているため、その手前のjnzにbreakpointを張ってjzに書き換え。
スクリーンショット (52).png
線が点滅していたのでprint screenのタイミングで消えました。
 IDAのいいところは、分岐で止めた時に次にどっちに行くよーというのを視覚的に教えてくれることです。これによって、書き換えるべきかそのままにしておくべきかが一目で分かります。
 今回は2段階あって、下のbreakpointを張ったjzもjnzに書き換える必要があります。
 というわけで、このまま進めてフラグが出てきました。

7.WinAntiDbg0x300

 ラスボスにふさわしい難問。
 例によってIDA freeで見ていく。今までと違い、exeがGUIになっている。見ていると下の方に「UPX」の3文字を見つけたため、とりあえず解凍してもう一回IDA freeへ。関数をcallしているところが多すぎてページが飛び飛びでやりづらい。関数の中まで見ていくが、どこにもflagにつながる分岐が見当たらない。
 仕方なくflagで検索したところ、StartAddress_0の中にflagの出力場所を発見。
スクリーンショット (53).png
 この関数自体はStartAddressなるED123Fから始まる関数に呼び出されているが、今度はStartAddressがどこにも呼び出されていない。この関数のみで実行できると信じて適当な場所からStartAddressを呼び出すこととする。
スクリーンショット (54).png
 起動してすぐのcall文が値が近かったから使った。適当だなあ。
 というわけで適当に分岐をいじくりながら進めてみる。
スクリーンショット (55).png
ダメだった。
すぐ下に、
  Debugged application message: ### (Note: The flag could become corrupted if the process state is tampered with in any way.)
と出てくる。適当にプロセスをいじくったせいでflagが壊れたらしい。
 つまり適切なところで呼び出さないとflagにならない。
 ここで、しばらく悩み、exeファイルと一緒にPDBファイルが渡されたことに気が付く。File>Load file>PDB file...で読み込んでみる。
情報量がめちゃめちゃ増えた。もう一度mainの画面をよく見ると、途中でStartAddressのアドレスが読み込まれており、その先に「__imp__free」という関数を発見。中まで追うが、何もなし。そのまま返ってくるだけ。怪しい。
 というわけで、ここでアセンブラ職人してここの値を改ざん、StartAddressにつなげます。
スクリーンショット (59).png
 本当は読み込んだアドレスを使うべきなんだろうけど、技術がないので直接StartAddressをcallしてみる。
スクリーンショット (34).png
 変なダイアログも出てきたけど、もう一個のダイアログでフラグゲット。
 writeupを書くのに再現してたらフラグが出なかったりIDAが落ちちゃったりしたこともあったので、想定通りの解ではなさそうです。というわけできれいな解がまだあるんでしょうが一応解けました。

終わり

 こうやって書いているとすんなり解けているように見えますが、実際はbreakpointを張りまくってここかなーここかなーといろんなところを書き換えたりchatGPTにghidraの逆コンパイル結果を解釈してもらったりして頑張っています。デバッグ楽しい。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0