株式会社Ninjastars 技術系ブログ

「本質的安全を提供し、デジタル社会を進化させる!!!」

Linux ハードウェアブレークポイントを実装する

株式会社Ninjastars
セキュリティエンジニア:一瀬健二郎

今回はLinux上でptraceシステムコールを用いて、ハードウェアブレークポイントを実装してみます。
ブレークポイントを設置したメモリ領域が読み書きされたり、実行された場合プログラムが停止し解析が可能になります。
デバッガの原理を知ることにより、より低レイヤーに興味を持っていただければ幸いです。

f:id:Ninjastars:20210130213234j:plain
ハードウェアブレークポイントに挑戦!

環境

Ubuntu 18.04.3 LTS
VMWare上で確認

今回やること

ハードウェアブレークポイントを実装してメモリのアクセス元を調べたり、プログラムにブレークポイントを設置します。
簡単のため対象プロセスはシングルスレッドを想定します。
※今回の内容はある程度ptraceについてやデバッガの基本的な原理が分かっている方向けです。

ハードウェアブレークポイントについて

プログラムをINT 3命令(x86の場合)等に書き換えるソフトウェアブレークポイントと違い、CPUのデバッグレジスタを用いてブレークポイントを設置します。
以下の方の記事が参考になるのでご一読ください。(※Windows用ですが実装の参考にさせて頂きました)
qiita.com

リバースエンジニアリング的観点からの利点
・プログラム領域を書き換える訳ではないため、チェックサム等がある場合も検知されない。
・特定メモリ領域にアクセスする処理を知ることが出来る。(通称WatchPoint)

特にWatchPointが重要であり、特定の文字列や特定の重要な値を参照してる箇所を特定できればプログラムの解析が容易になります。
逆に言えば、解析から守る上ではこういったデバッガの原理を知ることは非常に重要なことであると思います。

※WatchPointは該当メモリのアクセス属性を変更して、強制的に例外を発生させる方法も存在します。

ptraceシステムコールについて

当ブログでも幾つかの記事を書かせていただきました。
ptraceシステムコールによって対象プロセスのメモリの値を読み書きしたり、レジスタの値を取得・改変したり可能です。

自作ゲーム:チートチャレンジ - 株式会社Ninjastars 技術研究部
Linux/Android マルチスレッド対応のデバッガの実装 - 株式会社Ninjastars 技術研究部

今回はデバッグレジスタにアクセスするのに、PTRACE_PEEKUSER/PTRACE_POKEUSERというオプションを利用します。

詳しくはManのページを参照ください。
surf.ml.seikei.ac.jp

導入

まず64bitのOS上で32bitをコンパイルするため32bit用のライブラリをインストール必要があります。

sudo apt-get install libc6-dev-i386

以下をそれぞれgccコンパイルしてください。

gcc -no-pie counter.c -o counter
gcc -no-pie hwbp.c -o hwbp
gcc -m32 -no-pie counter.c -o counter32
gcc -m32 -no-pie hwbp.c -o hwbp32

上記ファイルについての内容は以下です。

  1. counter:変数の値とアドレスを表示するプログラム。
  2. hwbp:指定したアドレスにハードウェアブレークポイントを設置するプログラム。

実験

./counter
sudo ./hwbp pid x.address 3 #pidとx.addressはcounterに応じて変更。

counterを実行し、hwbpでxのアドレスにハードウェアブレークポイント(Read/Write)を設置します。
counter自身がxにアクセスする毎に、デバッガであるhwbpがアクセス時のRIPの値を取得します。
f:id:Ninjastars:20210131045450p:plain
f:id:Ninjastars:20210131045355p:plain

hwbpの引数の3はブレークする条件です。
0:Execute(実行時のみ)
1:Write Only(メモリ書き込み時のみ)
2:未定義
3:Read/Write(メモリ読み込み/書き込み時)

引数を変えたりして、色々と試してみましょう。

実装

レジスタの役割は最初に引用させていただいたQiitaの方の記事に詳しく書いてあります。
DR7レジスタの表は下記サイトが理解しやすかったので引用させて頂きます。
x86asm.net

DR7レジスタについて

bit(s) mnemonic description
31-30 LEN3 Length of Breakpoint #3
29-28 R/W3 Type of Transaction to Trap for Breakpoint #3
27-26 LEN2 Length of Breakpoint #2
25-24 R/W2 Type of Transaction to Trap for Breakpoint #2
23-22 LEN1 Length of Breakpoint #1
21-20 R/W1 Type of Transaction to Trap for Breakpoint #1
19-18 LEN0 Length of Breakpoint #0
17-16 R/W0 Type of Transaction to Trap for Breakpoint #0
6 L3 Local Exact Breakpoint #3 Enabled
4 L2 Local Exact Breakpoint #2 Enabled
2 L1 Local Exact Breakpoint #1 Enabled
0 L0 Local Exact Breakpoint #0 Enabled

ブレークポイントのサイズと値の対応

00b 1byte
01b 2 byte
10b 未定義(プロセッサによっては8 byte)
11b 4 byte

ブレークポイントのタイプと値の対応

00b 実行時にブレークする。この時サイズは1byte(値:00b)でなければいけない。
01b メモリ書き込み時ブレークする。
10b 未定義(※CR4のDEフラグによって異なる。)
11b メモリ読み込み時または書き込み時にブレークする。実行時はブレークしない。

まずDR0,DR1,DR2,DR3のレジスタにはブレークポイントを設置するアドレスを代入します。
次にDR7レジスタに上記表に応じて各bitに値を代入します。

//DR7レジスタの現在の状態を取得
int Dr7 =  ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, u_debugreg[7]), 0);
//slotに応じて0,2,4,6bit目にフラグを立てる
Dr7 |= 1<<(available*2);
//slotに応じて16,20,24,28bit目にブレークポイントのタイプを代入
Dr7 |= condition<<(available*4+16);
//slotに応じて18,22,26,30bit目にブレークポイントの長さ(上記表の対応で)を代入
Dr7 |= length<<(available*4+18);
//DR7レジスタの値を更新する。
ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[7]), Dr7);

このような形でハードウェアブレークポイントを設置します。
後はデバッガの原理で発生した例外のSIGTRAPを補足し、制御します。

まとめ

今回はハードウェアブレークポイントについて簡潔に説明させていただきました。
特にWatchPointを有効に利用すると、プロセスメモリ解析からその裏で動く処理を特定することが可能です。
「自分で実際に実装してみる」ことを通して解析の楽しさを感じたり、セキュリティ対策について学ぶきっかけとなれば幸いです。

ソースコード

counter.c

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

int main()
{
    int x = 0;
    printf("pid:%d\n",getpid());
    while(1)
    {
#if defined(__i386__)
        printf("x.value = %d x.addrss = 0x%x\n",x,(unsigned int)&x);
#else
        printf("x.value = %d x.addrss = 0x%llx\n",x,(unsigned long long)&x);
#endif
        x++;
        sleep(5);
    }

    return 0;
}

hwbp.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h> 
#include <sys/user.h>
#include <stddef.h>

#define CONDITION_EXECUTE                 0x0
#define CONDITION_WRITE_ONLY              0x1
#define CONDITION_IO_READ_WRITE           0x2//x86・x64では未実装
#define CONDITION_READ_WRITE              0x3
#define LENGTH_BYTE                       0x0
#define LENGTH_WORD                       0x1
#define LENGTH_QWORD                      0x2
#define LENGTH_DWORD                      0x3

char hardware_breakpoints[4] = {0,0,0,0};

int get_slot(int pid)
{
    int Dr6 =  ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, u_debugreg[6]), 0);
    
    int slot = 0;
    if (Dr6&0x1 && hardware_breakpoints[0] == 1)
        slot = 0;
    else if(Dr6&0x2 && hardware_breakpoints[1] == 1)
        slot = 1;
    else if(Dr6&0x4 && hardware_breakpoints[2] == 1)
        slot = 2;
    else if(Dr6&0x8 && hardware_breakpoints[3] == 1)
        slot = 3;
    else
        slot = -1;
    return slot;
}

int bp_del_hw(int pid,int slot)
{
    //入力値の例外処理
    if(!(0<=slot && 3>=slot))
        return -1;
    if(hardware_breakpoints[slot]==0)
        return 0;
    
    hardware_breakpoints[slot] = 0;
    int Dr7 =  ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, u_debugreg[7]), 0);
    Dr7 &= ~(3<<slot*2);
    Dr7 &= ~(3<<slot*4+16);
    Dr7 &= ~(3<<slot*4+18);

    ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[slot]), 0);
    ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[7]), Dr7);

    return 1;
}

int bp_set_hw(int pid ,void * address, int length, int condition)
{
    //入力値の例外処理
    if(!(0<=length && 3>=length))
        return -1;
    if(!(0<=condition && 3>=condition))
        return -1;
    
    int available = 0;
    if(0 == hardware_breakpoints[0])
        available = 0;
    else if(0 == hardware_breakpoints[1])
        available = 1;
    else if(0 == hardware_breakpoints[2])
        available = 2;
    else if(0 == hardware_breakpoints[3])
        available = 3;
    else{
        return -1;
    }

    hardware_breakpoints[available] = 1;

    int Dr7 =  ptrace(PTRACE_PEEKUSER, pid, offsetof(struct user, u_debugreg[7]), 0);
    Dr7 |= 1<<(available*2);

    Dr7 |= condition<<(available*4+16);
    Dr7 |= length<<(available*4+18);

    int r1 = ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[available]), address);
    int r2 = ptrace(PTRACE_POKEUSER, pid, offsetof(struct user, u_debugreg[7]), Dr7);
    if(r1==0 && r2==0)
    {
        return 1;
    }
    else
    {
        printf("set breakpoint error\n");
        return -1;
    }
}

int main(int argc, char *argv[])
{

    pid_t pid=atoi(argv[1]);
    unsigned long long address=strtoll(argv[2],NULL,0);
    int condition = atoi(argv[3]);

    //対象プロセスにアタッチ
    ptrace(PTRACE_ATTACH, pid, 0, 0);
    waitpid(pid, NULL, 0);

    //ハードウェアブレークポイントを設置
    bp_set_hw(pid,address,LENGTH_BYTE,condition);

    //プロセスを再開させる
    int status;
    ptrace(PTRACE_CONT, pid, 0, 0);
    printf("Continuing.\n");

    while(1)
    {
        //ブレイクするまで待機する
        waitpid(pid, &status, 0);
        
        if (WIFEXITED(status))
        {
            printf("Program exited normally.\n");
            exit(0);
        }

        if (WIFSTOPPED(status))
            printf("Breakpoint.\n");
        else
            exit(1);
        
        int signum = WSTOPSIG(status);
        if(signum==SIGTRAP)
        {
            //Dr0-3のどのレジスタ番号であるかを取得
            //int slot = get_slot(pid);
            //printf("slot:%d\n",slot);
            
            //レジスタの値を取得
            struct user_regs_struct regs;
            ptrace(PTRACE_GETREGS, pid, 0, &regs);
#if defined(__i386__)
            printf("EIP:0x%x\n",(unsigned int)regs.eip);
#else 
            printf("RIP:0x%llx\n",(unsigned long long)regs.rip);
#endif
            ptrace(PTRACE_CONT,pid,0,0);
        }
        else if(signum==19 ||signum==21)
        {
            //シグナル番号19と21は無視する。
            ptrace(PTRACE_CONT, pid, 0, 0);
        }
        else
        {      
            ptrace(PTRACE_CONT,pid,0,signum);
        }
    }
    //bp_del_hw(pid,0);
    //プロセスからデタッチする。処理が動き出す。
    //ptrace(PTRACE_DETACH, pid, 0, 0);

    return 0;
}

注意事項

本レポートに記載されている内容を許可されていないソフトウェアで行うと、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には許可されたソフトウェアに対してのみ実施するようにしてください。

本レポートについて
お問い合せ
E-mail:ichise@ninjastars-net.com

株式会社Ninjastarsエンジニア
一瀬健二郎