CTFの過去問を解いてOllyDbgの使い方を覚える。

はじめに

 デバッガの使い方が全然わからなかったので、Hack.lu CTF 2013のRoboAuthという問題で練習しました。
この問題は、パスワードを探すというとてもシンプル内容なので練習にはちょうどいいかなと思います。

問題の概要

問題文は以下の通り

RoboAuth (Category: Reversing) Author(s): cutz
Oh boy, those crazy robots can't catch a break! Now they're even stealing our
liquid gold from one of our beer tents! And on top of that they lock it behind
some authentication system. Quick! Access it before they consume all of our
precious beverage!

Download: https://ctf.fluxfingers.net/static/downloads/roboauth/RoboAuth.exe

Flag: password1_password2

問題ファイルのリンクが切れていた場合は下記のアーカイブにある問題データを利用できる。
http://shell-storm.org/repo/CTF/Hacklu-2013/Reversing/RoboAuth-150/

使ったツール

最も有名なデバッガで日本では"折井さん"の愛称で親しまれている。
日本語化パッチも公開されている。

高機能な逆アセンブラ

手順

 exeファイルなのでDOSから実行してどんな挙動をするのか確かめる。
f:id:kira000:20140408183249p:plain
コンソールからパスワードを入力するタイプの非常にシンプルなプログラムだと分かる。

次にOllyDbgを起動。(右クリックして管理者として実行)
RoboAuth.exeをOllyDbgで開く。
f:id:kira000:20140412173410p:plain

  • OllyDbgの画面構成
左上:逆アセンブルコード
左下:メモリダンプデータ
右上:レジスタ(CPUが内部にもつ記憶領域。変数みたいなもん)
右下:現在のスタック(一時的にデータを格納するメモリ領域。スタック内のデータはESPレジスタに格納される。)
  • よく使うショートカット
F2:ブレークポイント設定/解除
F7:詳細ステップ実行(ステップイン:関数内部まで入り実行)
F8:ステップ実行(ステップオーバー:関数呼び出しを1命令として実行)
F9:デバッギー実行
Ctrl+F2:再スタート
F12:デバッギー実行一時停止
Ctrl+F9:リターンまで実行
Alt+F9:ユーザーコードまで実行(システムDLL内から抜ける際等に使用)
Space:アセンブル
Ctrl+E:バイナリデータの編集
Ctrl+G:アドレスを指定して移動
ESC:詳細自動ステップ実行の停止

同時にIDA Proも起動しRoboAuth.exeを開く。
f:id:kira000:20140412002638p:plain

Strings windowのYou passed level1!というところをダブルクリック
f:id:kira000:20140412003038p:plain

DATA XREF: sub_401627+54Eというところをダブルクリック(DATA XREFは外部参照という意味)
f:id:kira000:20140412003610p:plain

一度目の認証でputsやscanfがcallされているアドレス値がわかったので次はOllyDbgで最初にputsが使われているアドレス値に移動する。

具体的にはCtrl+Gしてアドレスのところで00401B3Eを指定する。
f:id:kira000:20140412004031p:plain

F2キーで00401B3Eをブレークポイントに設定しF9でデバッギー実行する。
その後、F8でステップ実行していきscanfがcallされているところでパスワードに"AAAAAAAAAA"と入力してみる。
f:id:kira000:20140412004509p:plain

さらにF8でステップ実行を進めていくとEAXレジスタに"r0b0RUlez!"という文字列が格納されているのが見つかる。
f:id:kira000:20140412004952p:plain
その後もF8でステップ実行を続けるとプログラムは終了する。

ここで"r0b0RUlez!"という文字列が正しいパスワードかどうかDOSから実行し確かめるてみる。
f:id:kira000:20140411021500p:plain
一つ目のパスワードは"r0b0RUlez!"であると確認できた。

またIDAに戻り、二度目の認証が行われているアドレス値を探す。
scanf関数を目印にして探した。
f:id:kira000:20140412005504p:plain

もう一度OllyDbgに戻りCtrl+F2で再スタートする。
このときOllyDbgの本体があるフォルダ内に"filename.bak"や"filename.udd"のようなファイルがあれば削除する。(今回はRoboAuth.bakやRoboAuth.uddなどがあれば削除する。)
さっきと同じようにCtrl+Gで00401B3Eに移動しF2でブレークポイントを設定。
もう1回Ctrl+Gしてアドレスは004015B2を指定して移動。F2で004015B2にもブレークポイントを設定。
その後004015B2以降の逆アセンブルコードを読んでいくと0040161F番地にCC INT3というコードがある。
f:id:kira000:20140412012356p:plain

これはint 3hを使ったアンチデバッグらしい。(詳しいことは知らない)
これを無視させるためにOllyDbgのオプション>>解析詳細設定>>例外項目へと移動し、
以下の(プログラムに渡す)例外項目を無視する:
というところのINT3ブレークにチェックを入れる。
f:id:kira000:20140412012811p:plain

少し戻って004015B2番地(2度目の認証の部分)の逆アセンブルコードをもう一度読んでみる。

004015B2   E8 D1640000      CALL <JMP.&msvcrt.scanf>
004015B7   A1 98AD4000      MOV EAX,DWORD PTR DS:[40AD98]
004015BC   894424 04        MOV DWORD PTR SS:[ESP+4],EAX
004015C0   8D45 E0          LEA EAX,DWORD PTR SS:[EBP-20]
004015C3   890424           MOV DWORD PTR SS:[ESP],EAX
004015C6   E8 7CFFFFFF      CALL RoboAuth.00401547
004015CB   85C0             TEST EAX,EAX
004015CD   75 0D            JNZ SHORT RoboAuth.004015DC
004015CF   A1 A4AD4000      MOV EAX,DWORD PTR DS:[40ADA4]
004015D4   890424           MOV DWORD PTR SS:[ESP],EAX
004015D7   E8 B4640000      CALL <JMP.&msvcrt.puts>

004015C6番地のCALL RoboAuth.00401547の部分が怪しいので、Ctrl+Gで00401547へ移動しブレークポイントに設定。

00401547   55               PUSH EBP
00401548   89E5             MOV EBP,ESP
0040154A   EB 22            JMP SHORT RoboAuth.0040156E
0040154C   8B45 08          MOV EAX,DWORD PTR SS:[EBP+8]
0040154F   0FB610           MOVZX EDX,BYTE PTR DS:[EAX]
00401552   8B45 0C          MOV EAX,DWORD PTR SS:[EBP+C]
00401555   0FB600           MOVZX EAX,BYTE PTR DS:[EAX]
00401558   83F0 02          XOR EAX,2
0040155B   38C2             CMP DL,AL
0040155D   74 07            JE SHORT RoboAuth.00401566
0040155F   B8 01000000      MOV EAX,1
00401564   EB 17            JMP SHORT RoboAuth.0040157D
00401566   8345 08 01       ADD DWORD PTR SS:[EBP+8],1
0040156A   8345 0C 01       ADD DWORD PTR SS:[EBP+C],1
0040156E   8B45 0C          MOV EAX,DWORD PTR SS:[EBP+C]
00401571   0FB600           MOVZX EAX,BYTE PTR DS:[EAX]
00401574   3C 02            CMP AL,2
00401576  ^75 D4            JNZ SHORT RoboAuth.0040154C
00401578   B8 00000000      MOV EAX,0
0040157D   5D               POP EBP
0040157E   C3               RETN

F9でデバッギー実行し、一度目の認証で正しいパスワードを入力しもう一度F9。
二度目の認証で"AAAAAAAAAA"と入力し、そこからはサブルーチン内の処理も追いたいのでF7で詳細ステップ実行していく。

00401555   0FB600           MOVZX EAX,BYTE PTR DS:[EAX]

でのEAXレジスタが002BFDCC番地の値となっているので、そこを右クリックしダンプ画面へというところをクリックする。
f:id:kira000:20140412020409p:plain
すると左下のダンプデータのところに"u1nnf2lg"という文字列が見つかった。

00401558   83F0 02          XOR EAX,2
0040155B   38C2             CMP DL,AL

はEAXレジスタの値(u1nnf2lg)を2とXORした値とDLの値を比べている。
したがってパスワードはu1nnf2lgのそれぞれの文字について2とXORして出てきた文字列になる。

s = 'u1nnf2lg'
Flag = '' 

for i in s:
    Flag += chr(ord(i)^2)

print Flag
print "".join(chr(ord(i)^2) for i in 'u1nnf2lg')
#include <stdio.h>

int main()
{
    char *s = "u1nnf2lg";

    while (*s) {
        printf("%c", *s^2);
        s++;
    }

    printf("\n");

    return 0;
}
  • 実行結果
w3lld0ne

確かめる。
f:id:kira000:20140412022039p:plain

Flagは"r0b0RUlez_w3lld0ne"であることが分った。

終わりに

アセンブリ言語もデバッガの使い方もまだまだ全然分からないのでこれから勉強していきたいです。

参考にした書籍

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

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

ファイルの16進ダンプとその逆

はじめに

[追記]
 以前は「バイナリ,ASCIIの相互変換」というタイトルでしたが,言葉がふさわしくないと思ったのでタイトルを「ファイルの16進ダンプとその逆」に変更し,内容も大幅に改変しました。

 言語はCとPython,
 Windows 8.1Cygwin入れた環境でテストしました。

ファイルの16進ダンプ

 ファイルの16進ダンプとは,ファイルデータを16進数のデータの集まりとして表示することです。
形式は、16進を2個で1セットとし、1セットずつは1つの半角空白で区切られ、0x10B(バイト)ごとに改行されているものとします。
 左に何バイト目かを表す行番号のようなもの、右に対応するASCIIがあったほうが見栄えがよくなると思いまが,ダンプの逆をやることを考慮してあえてそこは手を抜きました。
 実行結果として載せてるのはHelloWorldをダンプしたものです。

#!/usr/bin/env python
#-*- coding: utf-8 -*-
import sys
 
argv = sys.argv
argc = len(argv)
 
if argc != 2:
    print 'Usage python hex.py [file_name]'
    quit()
 
count = 0
 
try:
    f = open(argv[1],'rb')
except:
    print 'Usage %s is not found.' % argv[1]
    quit()
for char in f.read():
    print '%02x' % ord(char),
    count += 1
    if count % 16 == 0:
        print
f.close()
  • 実行結果
$ python hex.py Hello.exe 
4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00
b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 f0 00 00 00
0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68
69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f
74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20
6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00
d1 35 16 c1 95 54 78 92 95 54 78 92 95 54 78 92
(中略)
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
#include <stdio.h>
 
int main(int argc, char *argv[])
{
    int n;
    unsigned long long int count = 0;
    FILE *fp;
 
    if(argc != 2){
        puts("Usage: hex [file_name]");
        return 0;
    }
 
    if((fp = fopen(argv[1], "rb")) == NULL)
        printf("Usage: %s is not found.\n", argv[1]);
    else{
        while((n = fgetc(fp)) >= 0){
            printf("%02x ", n);
            count++;
            if(count%16 == 0)
                putchar('\n');
        }
        fclose(fp);
    }
 
    return 0;
}
  • 実行結果
$ gcc -o hex hex.c
$ ./hex Hello.exe
4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00
b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 f0 00 00 00
0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68
69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f
74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20
6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00
d1 35 16 c1 95 54 78 92 95 54 78 92 95 54 78 92
(中略)
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

ファイルの16進ダンプの逆

 16進ダンプによって作成されたテキストファイルから元のファイルを復元します。

#!/usr/bin/env python
#-*- coding: utf:8 -*-
import sys
 
argv = sys.argv
argc = len(argv)
 
if argc != 2:
    print 'Usage: python unhex.py [file_name]'
    quit()
 
try:
    infile = open(argv[1],'rb')
except:
    print 'Usage: %s is not found.' % argv[1]
    quit()
 
fname = raw_input('out file name:')
outfile = open(fname,'wb')
 
a = [str(x) for x in infile.read().split()]
for i in a:
    outfile.write(chr(int(i, 16)))
 
infile.close()
outfile.close()
  • 元ファイルとの比較
$ python hex.py Hello.exe > Hello.txt
$ python unhex.py Hello.txt
out file name:Hello2.exe
$ chmod a+x Hello2.exe
$ ./Hello2.exe
Hello World
$ diff Hello.exe Hello2.exe
 

diffコマンドで何も表示されないということは同じファイルということ。

#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char *argv[])
{
    int n;
    char str[255];
    FILE *fp,*fp2;
 
    if(argc != 2){
        puts("Usage: unhex [file_name]");
        return 0;
    }
 
    if((fp = fopen(argv[1], "rb")) == NULL){
        printf("Usage: %s Not Available.\n", argv[1]);
        return 0;
    }
 
    printf("out file name:");
    scanf("%254s%*[^\n]", str);
 
    if((fp2 = fopen(str, "wb")) == NULL){
        printf("Usage: %s Not Available.\n", str);
        return 0;
    }
 
    while(fscanf(fp, "%x", &n) != EOF)
        fputc(n, fp2);
 
    fclose(fp);
    fclose(fp2);
 
    return 0;
}
  • 元ファイルとの比較
$ gcc -o unhex unhex.c
$ ./unhex Hello.txt
out file name:Hello3.exe
$ chmod a+x Hello3.exe
$ ./Hello3.exe
Hello World
$ diff Hello.exe Hello3.exe
 

diffコマンドで何も表示されないということは(ry

より汎用性が高いスクリプト

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

import sys

argv = sys.argv
argc = len(argv)

if argc != 2:
    print 'Usage: python unhex.py [file_name]'
    quit()

try:
    infile = open(argv[1], 'rb')
    data = infile.read().replace(' ', '').replace('\r', '').replace('\n', '')

except:
    print 'Error: %s is not found.' % argv[1]
    quit()

outfile = open(raw_input('out file name:'), 'wb')

i=0

while i+1 < len(data):
    outfile.write(chr(int(data[i:i+2],16)))
    i+=2

infile.close()
outfile.close()

コマンドを使う

 実際には自分でコードを書くよりもLinuxのodコマンドやxxdコマンドを使うことのほうが多いような気がします。

$ od -t x1 -w16 -A x Hello.exe
000000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00
000010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00
000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000030 00 00 00 00 00 00 00 00 00 00 00 00 f0 00 00 00
000040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68
000050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f
000060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20
000070 6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00
000080 d1 35 16 c1 95 54 78 92 95 54 78 92 95 54 78 92
000090 d3 05 a5 92 96 54 78 92 d3 05 98 92 86 54 78 92
0000a0 d3 05 99 92 92 54 78 92 48 ab b3 92 97 54 78 92
(中略)
007730 34 30 68 35 6c 35 b4 35 a8 39 b4 3c d4 3e d8 3e
007740 f4 3e f8 3e 14 3f 18 3f 34 3f 38 3f 58 3f 00 00
007750 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
007a00
$ xxd -p Hello.exe > Hello2.txt
$ xxd -r -p Hello2.txt > Hello4.exe
$ diff Hello.exe Hello4.exe
 

GMP-使い方メモ

はじめに

 今回は任意精度演算ライブラリ、The GNU Multiple Precision Arithmetic Library(GMP)の使い方についてメモ書きみたいなことをします。

[追記]任意精度演算がしたいなら普通にPythonとか使ったほうが良さそう。

用語

  • 任意精度演算・・・数値の桁の精度がメモリ容量以外に制限されない計算方法。ようするに大きい数が扱える。

環境構築

 本来はLinux向けのツールだがCygwinにインストールして使った。
インストール方法は以下のサイトを参考にした。
GMP の使い方

使い方

基本

 まずは、

#include "gmp.h"

でgmpをインクルードする。
GMPは、数値を格納する変数型として

がある。
宣言は、

mpz_t a, b, c;

のように行う。
変数は初期化をしないと使えないので

mpz_init(a);
mpz_init(b);
mpz_init(c);

のようにして初期化を行う。初期化をすると値は0になる。

mpz_set_str(a, "10000000000", 10);

のようにすると指定した基数で数列を代入してくれる。最後の10は基数。
他にも代入方法はあるけど、個人的によく使うのはこれ。
出力は以下のようにする。

mpz_out_str(stdout, 10, a);

使い終わった変数は

mpz_clear(a);

で領域を解放する。

四則演算

mpz_add(a, b, c);       /*a = b + c;*/
mpz_sub(a, b ,c);       /*a = b - c;*/
mpz_mul(a, b, c);       /*a = b * c;*/
mpz_cdiv_q(a, b ,c);    /*a = b / c;*/

その他の演算

mpz_sqrt(a, b);     /*aにbの平方の整数部を代入する*/
mpz_cmp(a, b)       /*aとbを比較し a > b なら正 a = b なら0 a < b なら負を返す*/
mpz_mod(a, b ,c);   /*a = b mod c*/
mpz_powm(result, a, b, c); /*result = (a^b) mod c*/
mpz_lcm(a, b, c)    /*aにbとcの最小公倍数を代入*/
mpz_gcd(a, b, c)    /*aにbとcの最大公約数を代入*/

サンプルコード

#include <stdio.h>
#include "gmp.h"
#define BASE 10
int main()
{
    mpz_t a,b,c,result; /*宣言*/
	
    mpz_init(a);   /*初期化*/
    mpz_init(b);
    mpz_init(c);
    mpz_init(result);
	
    mpz_set_str(a, "100", BASE);  /*代入*/
    mpz_set_str(b, "50", BASE);
    mpz_set_str(c, "3", BASE);
	
    mpz_add(result, a, b);   /*result = a + b*/	
    mpz_out_str (stdout, BASE, result);
    printf("\n");
	
    mpz_init(result);

    mpz_sub(result, a, b);   /*restlt = a - b*/
    mpz_out_str(stdout, BASE, result);
    printf("\n");
	
    mpz_init(result);
	
    mpz_mul(result, a, b);    /*result = a * b*/
    mpz_out_str(stdout, BASE, result);
    printf("\n");
	
    mpz_init(result);
	
    mpz_cdiv_q(result, a, b);  /*result = a / b*/
    mpz_out_str(stdout, BASE, result);
    printf("\n");
	
    mpz_init(result);
	
    mpz_sqrt(result, a);  /*resultにaの平方の整数部を代入*/
    mpz_out_str(stdout, BASE, result);
    printf("\n");
	
    mpz_init(result);
	
    mpz_mod(result, a, b); /*result = a mod b*/
    mpz_out_str(stdout, BASE, result);
    printf("\n");
	
    mpz_init(result);
	
    mpz_powm(result, c, a, b); /*result = (c^a) mod b*/
    mpz_out_str(stdout, BASE, result);
    printf("\n");
	
    mpz_clear(a);    /*領域の解放*/
    mpz_clear(b);
    mpz_clear(c);
    mpz_clear(result);

    return 0;
}

実行結果

150
50
5000
2
10
0
1

コンパイルは注意が必要で、Cygwinhoge.cというソースファイルをコンパイルしたい場合は、

gcc -I/usr/local/include -L/usr/local/lib -Wall -O3 -o hoge.exe hoge.c -lgmp

のようにする。

終わりに

 GMPでmpz_t型(整数型)を扱うのにはある程度慣れてきたが、mpf_t型(浮動小数点型)はまだまだ満足に扱えてないのでこれから何とかしたい。そのためには英語のドキュメントを読めるようにならないとだめだよなと思った(小並感)。