株式会社Ninjastars 技術研究部

「もっと楽しく安全なゲームの世界を創る」

Linux/Android マルチスレッド対応のデバッガの実装

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

このブログでも紹介した『ptrace入門』等を基に、簡易的なデバッガやstraceの作成に挑戦した方も居られると思います。
今回は一歩進めてシングルスレッドを想定したものから、マルチスレッドに対応した簡易デバッガを作成します。
マルチスレッドに対応させることで、実際のアプリケーションなどに対してある程度実用的なものを作成することが可能になります。

f:id:Ninjastars:20200227145433p:plain:w300
デバッガでアプリケーションを解析しよう!!

環境

Ubuntu18.04

Androidについては触れませんが、x86の場合ndkでコンパイルできます。

今回やること

マルチスレッド対応の簡易デバッガを作ります。
※今回の内容はある程度ptraceについてやデバッガの基本的な原理が分かっている方向けです。
ptraceについては冒頭でも触れた『ptrace入門』という書籍で体系的にまとめられています。
【書評】ptrace入門 大山恵弘著 - 株式会社Ninjastars 技術研究部

導入

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

sudo apt-get install libc6-dev-i386

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

gcc -m32 -no-pie debugger.c -o debugger 
gcc -m32 -no-pie singledebugger.c -o singledebugger
gcc -m32 -no-pie tracer.c -o tracer -lpthread

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

  1. debugger:マルチスレッドに対応した簡易的なデバッガ
  2. singledebugger:シングルスレッドのみ対応の簡易的なデバッガ
  3. tracer:簡易的なマルチスレッドプログラム

実験

シングルスレッドのみ対応、マルチスレッド対応のデバッガでtracerのfunc関数内のgetchar呼び出し直後にソフトウェアブレークポイントを設置します。

objdumpでtracerを逆アセンブルします。

objdump -d -M intel tracer

f:id:Ninjastars:20200228105107p:plain
tracerの逆アセンブル結果

コンパイル環境によって異なりますが、上記の画像のような場合0x804858fにソフトウェアブレークポイントを設置します。

Linxu環境でのデバッガによるブレークポイント設置は下記の方の記事が参考になります。
th0x4c.github.io

また弊社ブログでもAndroid、ARMの内容ですがソフトウェアブレークポイントについて説明しています。
Android ARM バイナリ解析 入門 - 株式会社Ninjastars 技術研究部

シングルスレッドのみ対応のデバッガによる解析

まずtracerを実行しましょう。

./tracer

プロセスIDは

ps -A | grep tracer

コマンドで調べます。

例えばプロセスIDが11274だとすると以下で該当アドレスにソフトウェアブレークポイントを設置できます。

sudo ./singledebugger 11274 0x804858f

この状態でtracerでEnterを押してみましょう。

f:id:Ninjastars:20200228110945p:plain
SIGTRAPでプログラムが強制終了。

デバッガでアタッチしたスレッドとは別のスレッドでSIGTRAPが発生したことにより、例外を捕捉できずにプログラムが強制終了してしまいました。
このようにマルチスレッドプログラムをデバッグする場合、予め全スレッドにアタッチする必要があります。

マルチスレッド対応のデバッガによる解析

同じようにtracerを実行し、debuggerでブレークポイントを設置します。

debuggerの実行結果は以下のようになり、正常にデバッグできていることが分かります。

f:id:Ninjastars:20200228111656p:plain
マルチスレッド対応により正常にデバッグできている。

マルチスレッドに対応するにあたり実装面において今回重要な点は以下の三つです。

  1. 予め全スレッドにアタッチした(attach_all_thread)。
  2. アタッチ後はSIGSTOPシグナルを送り、プロセスを停止させた。
  3. waitpidの第一引数を-1に、第三引数を『__WALL』にした。

2.について
ptraceシステムコールでは対象プロセスが停止していないとメモリの読み書きは出来ません。
そのため全スレッドにアタッチ後SIGSTOPシグナルを送り、停止させています。
3.について
waitpidの第一引数を-1にすることにより、子プロセスのどれかが停止するまで待機することができます。
また第三引数を『__WALL』にすることでスレッドを含めて待つことができるようになります。
詳しくはManのページを参照ください。
linuxjm.osdn.jp

今回は簡易的なものの紹介にとどめましたが、基本的なことさえ分かればあとは手間と実装力の問題かと思われます。
デバッガ自作はOSやコンパイラ自作と同じように大変学びのある興味深い世界だと思われるので、挑戦する仲間が増えれば嬉しい限りです。

まとめ

インターネット上のデバッガ関連記事の多くがシングルスレッドを想定したものだったため、今回この記事を書かせていただきました。
私自身も常に試行錯誤しながらデバッガや低レイヤーの勉強を行っています。
このブログが同じように勉強されている方等に対して技術的な悩みの解決や、学ぶ楽しさを知る糸口となれば幸いです。

ソースコード

tracer.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* func()
{
    printf("please input key.\n");
    getchar();
}

int main()
{
    pthread_t thread;
    void* ret;
    pthread_create(&thread,NULL,(void*)func,NULL);
    pthread_join(thread,&ret);
    
    return 0;
}

debugger.c

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

int attach_all_thread(int pid)
{
  char _taskdir[255];
  DIR *taskdir;

  sprintf(_taskdir, "/proc/%d/task",pid);

  taskdir=opendir(_taskdir);

  if (taskdir)
  {
    struct dirent *d;

    d=readdir(taskdir);
    while (d)
    {
      int tid=atoi(d->d_name);
      int status;
      ptrace(PTRACE_ATTACH,tid,0,0);
      int id = waitpid(-1,&status,__WALL);
      ptrace(PTRACE_CONT,id,0,0);
      d=readdir(taskdir);
    }
    closedir(taskdir);
  }

  return 0;
}

int main(int argc, char *argv[])
{
    pid_t pid=atoi(argv[1]);
    unsigned long address=strtoll(argv[2],NULL,0);

    //対象プロセスの全スレッドにアタッチ
    attach_all_thread(pid);
 
    //対象プロセスを停止させる
    syscall(__NR_tkill, pid, SIGSTOP);
    int s;
    waitpid(pid,&s,0);
    int original_text;

    //対象アドレスのメモリの値を保存
    original_text = ptrace(PTRACE_PEEKTEXT, pid, address, NULL);

    printf("OriginalText:%x\n",original_text);
    //{0xCC}を対象アドレスに書き込み
    int opcode = 0x000000CC;
    ptrace(PTRACE_POKETEXT, pid, address, ((original_text & 0xFFFFFF00) | opcode ));
    
    ptrace(PTRACE_CONT, pid, NULL, NULL);
    printf("Continuing.\n");
    
    //プロセスを再開させる
    int status;
    while(1)
    {
      //ブレイクするまで待機する
      int tid = waitpid(-1, &status, __WALL);

      if (WIFEXITED(status))
      {
        printf("Program exited normally.\n");
        exit(0);
      }

      if (WIFSTOPPED(status))
      {
        printf("Breakpoint.\n");
      }
      else
        exit(1);
      
      int signum = WSTOPSIG(status);
      printf("SignalNumber:%d\n",signum);
      if(signum==SIGTRAP)
      {
        printf("SIGTRAP\n");
        //レジスタの値を取得
        struct user_regs_struct regs;
        ptrace(PTRACE_GETREGS, tid, 0, &regs);
        printf("EIP_RegisterValue:%x\n",regs.eip);

        //処理を書き戻す
        ptrace(PTRACE_POKETEXT, tid, address,original_text);

        //int3は既に実行されており、1命令分進んでいる。
        //よって1命令分eipを減算する。
        regs.eip--;
        ptrace(PTRACE_SETREGS,tid,0,&regs);

        //ここでは再度ブレークポイントを設置するためSINGLESTEPさせている。
        ptrace(PTRACE_SINGLESTEP,tid,0,0);
        waitpid(tid,&status,__WALL);
        ptrace(PTRACE_POKETEXT, tid, address, ((original_text & 0xFFFFFF00) | opcode ));

        //プロセスを再開する
        ptrace(PTRACE_CONT, tid, 0, 0);
      }
      else if(signum==19 ||signum==21)
      {
        //シグナル番号19と21は無視する。
        ptrace(PTRACE_CONT, tid, 0, 0);
      }
      else
      {      
        ptrace(PTRACE_CONT,tid,0,signum);
      }
    }

    
}

singledebugger.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>

int main(int argc, char *argv[])
{
    
    pid_t pid=atoi(argv[1]);
    unsigned long address=strtoll(argv[2],NULL,0);

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

    //対象アドレスのメモリの値を保存
    original_text = ptrace(PTRACE_PEEKTEXT, pid, address, NULL);

    printf("OriginalText:%x\n",original_text);
    //{0xCC}を対象アドレスに書き込み
    int opcode = 0x000000CC;
    ptrace(PTRACE_POKETEXT, pid, address, ((original_text & 0xFFFFFF00) | opcode ));
    
    //プロセスを再開させる
    int status;
    ptrace(PTRACE_CONT, pid, NULL, NULL);
    printf("Continuing.\n");

    //ブレイクするまで待機する
    waitpid(pid, &status, 0);

    if (WIFEXITED(status))
    {
      printf("Program exited normally.\n");
      exit(0);
    }

    if (WIFSTOPPED(status))
      printf("Breakpoint.\n");
    else
      exit(1);
    
    //レジスタの値を取得
    struct user_regs_struct regs;
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
    printf("EIP_RegisterValue:%x\n",regs.eip);

    //処理を書き戻す
    ptrace(PTRACE_POKETEXT, pid, address,original_text);
    
    regs.eip--;
    ptrace(PTRACE_SETREGS,pid,0,&regs);

    //プロセスからデタッチする。処理が動き出す。
    ptrace(PTRACE_DETACH, pid, 0, 0);

}

注意事項

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

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

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