Malware Data Science Chapter 8 のデータセットで少し遊んでみる

Malware Data Science の Chapter 8 では実際に機械学習を使ってマルウェア検知器を作る手順が書かれている。 このChapterの内容を再現できるように、マルウェアと良性ファイルを入手できるようになっており、Supportページからダウンロードできる。 データ数は良性ファイルが991でマルウェアが428でそこまで多くはないが、これを使って少し遊んでみる。

実施した内容

Malware Data Science では Feature Hashing Trick を使って、バイナリに含まれる文字列情報から特徴ベクトルを作り、それを使ったマルウェア検知器の作成方法が書かれている。これとは別の特徴量を使った場合と比較して性能がどの程度変化するのかを調べてみることにした。

特徴量としては、Endgameが出しているEmberデータセットで使われている機械学習モデルで使われているものを採用した。

github.com

このモデルでは以下が特徴ベクトルとして採用されている。

  • バイナリに含まれるバイトヒストグラム
  • バイトエントロピーヒストグラム
  • バイナリに含まれる文字列
  • ファイルサイズやメモリに読み込まれた際のサイズ
  • PEヘッダーの情報
  • セクションの情報
  • インポートAPIの情報
  • エクスポートAPIの情報

ちなみに、APIの情報や文字列の情報を特徴ベクトルにする際は、Malware Data Scienceでも使われているFeature Hashing Trickが使われている。

実験結果

実験の結果はgistにまとめた。

gist.github.com

AUCを比較すると、Emberのほうが優れていた (Malware Data Scienceのモデルだと AUC が0.9951、Emberだと AUC が0.9972) が、そこまで大きく差はない。

次に、どの特徴量が最も検知に効いているのか、調べてみると (ちなみに、先程のgistの一番最後のところに特徴量の重要度の比較をまとめている) 、圧倒的に Byte Histogram が大きい。二番目に文字列の情報が重要な特徴量として使われているようだ。

結局のところ、Ember では文字列情報以外にも色々特徴量が使われているけども、今回に限れば文字列とバイトヒストグラム・バイトエントロピーヒストグラムぐらいしか推論結果に寄与していないみたいである。

これだったら、バイトヒストグラムだけでも良い精度が出せるのではと思い、こちらも試してみる。

gist.github.com

もとのEmberのモデルから特徴ベクトルのサイズが1/10ぐらいになっているが、AUCが0.9975と上がった。バイトヒストグラム強い。

Embenchを試そうとしたが動かせなかった

Dhrystone に変わるフリーのCPUを性能を測るベンチマークとして Embench が David Patterson によって提案されているのを、RISC-V Workshop Zurich で知った。発表スライドとビデオはこのリンクの"Embench TM: A Free Benchmark Suite for Embedded Computing from an Academic-Industry Cooperative (Towards the Long Overdue and Deserved Demise of Dhrystone)" を参照。

GitHubソースコードがすでに公開されているので早速試そうとしたのだが、結論から言うと実行することができず orz。

riscv64 向けに一応ビルドするところまではできたのだが、ベンチマークを実行しその結果を出力するところまではできなかった。

リポジトリにあるISSUEを見る限り同じ問題に困っている人がいるらしく、ビルドスクリプトからして書き直しを検討しているらしいので、修正されるのを待つことにするか。 github.com

riscv64 向けにビルドできるようにしたものを置いた。 github.com

Malware Data Science: Attack Detection and Attributionを読んだ

Malware Data Scienceを読んだ。

マルウェア解析のBasicから入り、機械学習の入門・マルウェア検知器への応用事例・深層学習のイントロダクションとその適用事例などが紹介されている。

機械学習のプロフェッショナルには正直物足りない内容だと思う。ある程度マルウェア解析の経験がある人で、機械学習の手法を取り入れた検知器を作りたいと考えている人向けかなと思った。

(ただ、マルウェア解析の経験がある人向けの本にしては最初の二章は正直余分な気がして、少しターゲットとしている読者層がよく見えない感じの本かなと思った。)

この本むけに筆者が作成したデータセットが公開されている。そこそこのデータ量があって結構遊べた。その遊んだ結果はまた後日投稿しようと思う。

ChiselのハードウェアデザインからVerilogのRTLを生成し、Treadleでシミュレーションを実行する

ものすごくタイトルが長くなってしまった。調べても意外と出てこなかったのでメモしておく。

VerilogのRTLを生成する方法

この記事を参考に以下のように書いてみた。

package GenVerilogTest

import chisel3._

class Top(in0Bits: Int, in1Bits: Int) extends Module {
  val io = IO(new Bundle {
    val in0 = Input(UInt(in0Bits.W))
    val in1 = Input(UInt(in0Bits.W))
    val out = Output(UInt((in0Bits + 1).W))
  })

  io.out := io.in0 + io.in1
}

object Elabolate extends App {
  chisel3.Driver.execute(args, () => new Top(32, 32)) match {
    case ChiselExecutionSuccess(Some(_), emittedStr, Some(firrtlExecutionResult)) => {
      firrtlExecutionResult match {
        case firrtl.FirrtlExecutionSuccess(_, compiledFirrtl) => {
          println(compiledFirrtl) // FirrtlからVerilogに変換した結果が表示される
          println("")
          println(emittedStr) // Firrtlが表示される
        }
        case firrtl.FirrtlExecutionFailure(message) =>
          throw new Exception(s"FirrtlBackend: Compile failed.")
        case _ =>
          throw new Exception("Problem with compilation")
      }
    }
  }
}

上記コードにおいてchisel3.Driver.execute(args, () => new Top(32, 32))においてVerilogとFirrtlの2つを生成している。生成されたファイルはbuild.sbtが置かれているディレクトリと同じ箇所に置かれる。

生成されたファイルは以下の通り。この程度ぐらいであればなんとか読めるレベルではある。

;buildInfoPackage: chisel3, version: 3.1.8, scalaVersion: 2.11.12, sbtVersion: 1.1.1, builtAtString: 2019-07-08 17:44:42.884, builtAtMillis: 1562607882884
circuit Top : 
  module Top : 
    input clock : Clock
    input reset : UInt<1>
    output io : {flip in0 : UInt<32>, flip in1 : UInt<32>, out : UInt<33>}
    
    node _T_11 = add(io.in0, io.in1) @[Top.scala 12:20]
    node _T_12 = tail(_T_11, 1) @[Top.scala 12:20]
    io.out <= _T_12 @[Top.scala 12:10]
module Top( // @[:@3.2]
  input         clock, // @[:@4.4]
  input         reset, // @[:@5.4]
  input  [31:0] io_in0, // @[:@6.4]
  input  [31:0] io_in1, // @[:@6.4]
  output [32:0] io_out // @[:@6.4]
);
  wire [32:0] _T_11; // @[Top.scala 12:20:@8.4]
  wire [31:0] _T_12; // @[Top.scala 12:20:@9.4]
  assign _T_11 = io_in0 + io_in1; // @[Top.scala 12:20:@8.4]
  assign _T_12 = io_in0 + io_in1; // @[Top.scala 12:20:@9.4]
  assign io_out = {{1'd0}, _T_12}; // @[Top.scala 12:10:@10.4]
endmodule

また、FirrtlとVerilogへの変換結果は文字列としても受け取ることができる。

object Simulator {
  // buildした結果のFirrtlとVerilogの2つをタプルとして返す
  def build(args: Array[String]): (String, String)= {
    chisel3.Driver.execute(args, () => new Top(32, 32)) match {
      case ChiselExecutionSuccess(Some(_), firrtlSource, Some(firrtlExecutionResult)) => {
        firrtlExecutionResult match {
          case firrtl.FirrtlExecutionSuccess(_, verilogSource) => {
            (verilogSource, firrtlSource)
          }
          case firrtl.FirrtlExecutionFailure(message) =>
            throw new Exception(s"FirrtlBackend: Compile failed.")
          case _ =>
            throw new Exception("Problem with compilation")
        }
      }
    }
  }
 
  def main(args: Array[String]): Unit = {
    // .. Simulatorの定義
  }
}

上記コードのbuild関数では、戻り値の1つめがVerilogのコード、2つめがFirrtlのコードに対応する。

次に、Firrtlのコードを使ってシミュレーターを起動してみる。Treadleを使うと以下のように書くことができる。

class SimulatorOptionsManager extends TreadleOptionsManager

object Simulator {
  // buildした結果のFirrtlとVerilogの2つをタプルとして返す
  def build(args: Array[String]): (String, String)= {
    // .. 上のコードを参考
  }

  def main(args: Array[String]): Unit = {
    val (_, firrtlSource) = build(args)
    val optionsManager = new SimulatorOptionsManager // simulatorを作成する際のオプションを指定できるみたいなのだが、正直良くわかっていない

    optionsManager.setTargetDirName("simulator_run_dir")

    val simulator = TreadleTester(firrtlSource, optionsManager) // 生成されたFirrtl自体を渡す

    val maxCycles = 100
    var cycles = 0
    println("Running...")

    simulator.poke("io_in0", 1) // io.in0 ではなく、 io_in0 のようにアンダースコアでつないで表現する必要があるので注意
    simulator.poke("io_in1", 2)
    while (simulator.peek("io_out") != 12 && cycles < maxCycles) {
      simulator.step(1)
      cycles += 1
      simulator.poke("io_in0", 10)
      simulator.poke("io_in1", 2)
      println(s"Simulated $cycles cycles")
      val out = simulator.peek("io_out")
      println(s"output is $out")
    }
  }
}

以下のような出力が出てくるはず。

[info] Compiling 1 Scala source to /mnt/src/GenVerilogTest/target/scala-2.11/classes ...
[warn] there were three feature warnings; re-run with -feature for details
[warn] one warning found
[info] Done compiling.
[info] Packaging /mnt/src/GenVerilogTest/target/scala-2.11/chisel-module-template_2.11-3.1.1.jar ...
[info] Done packaging.
[info] Running GenVerilogTest.Sumulator 
[info] [0.001] Elaborating design...
[info] [0.689] Done elaborating.
Total FIRRTL Compile Time: 240.4 ms
Total FIRRTL Compile Time: 12.9 ms
file loaded in 0.051272257 seconds, 7 symbols, 3 statements
Running...
Simulated 1 cycles
output is 12
[success] Total time: 3 s, completed 2019/08/03 15:52:56
[IJ]sbt:chisel-module-template> 

Chisel3でメモリにテキストデータを読み込む際の手順

やり方がよくわからず結構苦戦した。一応ドキュメントにそれっぽい記載はあるんだけども。

chisel3.util.experimental.loadMemoryFromFileという関数を使うのが簡単。例えば、

class InstMemory(memfile: String) extends Module {
  val io = IO(new Bundle{
    val read_addr = Input(UInt(4.W))

    val read_data = Output(UInt(8.W))
  })

  val memory = Mem(16, UInt(8.W))
  loadMemoryFromFile(memory, memfile) // memoryにデータを読み込む

  io.read_data := memory(io.read_addr)
}
class MemoryTester extends ChiselFlatSpec {
  "InstMemory" should s"read data correctly" in {
    Driver(() => new InstMemory("src/test/resources/raw/test.txt"), "treadle") {
      m => new MemoryUnitTest(m)
    } should be (true)
  }
}

みたいな感じで使う。ここでtest.txtはデータの最小単位を改行で区切って並べたものとする。以下のような感じ。

00
01
02
03
04
05
06
07
08
09
0a
0b
0c
0d
0e
0f

こうすると、memoryの第0要素から第15要素に0から15までがそれぞれ埋められる。

ちなみに試した限りでは、この方法だとバイナリファイルを読み込むことができない。以下のようなエラーが出てくる。

[info]   treadle.executable.TreadleException: loading memory memory[0] <=  : error: Zero length BigInteger

実行ファイルとかを読み込む際には他の方法を使うしかなさそうである。

RISC-V Linuxでptrace使う場合の注意点

「たのしいバイナリの歩き方」の本やバイナリの歩き方に掲載されているptraceのサンプルプログラムが、RISC-V QEMU上で動いているLinuxではどうもうまく動かない。その原因を調査していた。

うまく動かないと悩んでいたコードはバイナリの歩き方に掲載されていたもので以下の通り。PTRACE_SINGLESTEPで実行が進まない上、PTRACE_GETREGSでそれっぽいレジスタの値を取得できないという問題に遭遇していた。(ちなみに、手元のx86実機マシンでは正しく以下のサンプルコードは動くことは確認済み。)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <sys/user.h>
#include <errno.h>

void err(char *str)
{
    fprintf(stderr, "ERROR: %s\n", str);
}

void target(char *argv[], char *argp[])
{
    if(ptrace(PTRACE_TRACEME, 0, NULL, NULL) != -1)
        execve(argv[0], argv, argp);
    else
        err("PTRACE_TRACEME");
    exit(0);
}

void controler(int pid)
{
    int status;
    struct user_regs_struct regs;

    while(1){
        waitpid(pid, &status, 0);
        if(WIFEXITED(status))
            break;
        ptrace(PTRACE_GETREGS, pid, 0, &regs);
        printf("%08x: \n", (unsigned int)regs.eip);
        ptrace(PTRACE_SINGLESTEP, pid, 0, NULL);
    }
}

int exec_prog(char *argv[], char *argp[])
{
    int pid;

    switch(pid = fork())
    {
    case 0:
        target(argv, argp);
        break;
    case -1:
        err("FORK");
        break;
    default:
        controler(pid);
        break;
    }
    return 0;
}

int main(int argc, char *argv[], char *argp[])
{
    if(argc < 2){
        fprintf(stderr, "%s <args>\n", argv[0]);
        return 1;
    }

    argv++;
    exec_prog(argv, argp);
    return 0;
}

結論だけ述べると

  • PTRACE_GETREGS PTRACE_SETREGS PTRACE_SINGLESTEPを指定してptraceを呼び出しても、そもそも実装されていないので必ずEIOがエラーとして返る。
  • アタッチしているプロセスのレジスタの情報を取得/設定する場合には PTRACE_GETREGSET PTRACE_SETREGSET を用いる必要がある。
  • PTRACE_SINGLESTEPは使えず、代替手段もない。PTRACE_SYSCALLPTRACE_CONTを使い、特定の場所で止めたいのであれば、止めたい箇所にebreakを埋め込む。

である。

調査概要

ここからLinux kernelのソースコードを落としてくる。今回5.2.3を落としてきた。

落としてきたLinux kernelのソースコードを展開し、arch/riscv/kernel/ptrace.cソースコードをみる。該当する箇所は以下の通り。

long arch_ptrace(struct task_struct *child, long request,
         unsigned long addr, unsigned long data)
{
    long ret = -EIO;

    switch (request) {
    default:
        ret = ptrace_request(child, request, addr, data);
        break;
    }

    return ret;
}

問答無用でptrace_requestへ飛ぶコードが書かれている。 ptrace_requestkernel/ptrace.cで定義されている。

int ptrace_request(struct task_struct *child, long request,
           unsigned long addr, unsigned long data)
{
    bool seized = child->ptrace & PT_SEIZED;
    int ret = -EIO;
    kernel_siginfo_t siginfo, *si;
    void __user *datavp = (void __user *) data;
    unsigned long __user *datalp = datavp;
    unsigned long flags;

    switch (request) {
    case PTRACE_PEEKTEXT:
    case PTRACE_PEEKDATA:
        return generic_ptrace_peekdata(child, addr, data);
    case PTRACE_POKETEXT:
    case PTRACE_POKEDATA:
        return generic_ptrace_pokedata(child, addr, data);

#ifdef PTRACE_OLDSETOPTIONS
    case PTRACE_OLDSETOPTIONS:
#endif
    case PTRACE_SETOPTIONS:
        ret = ptrace_setoptions(child, data);
        break;
    case PTRACE_GETEVENTMSG:
        ret = put_user(child->ptrace_message, datalp);
        break;
        
    // 長いので省略

この中にはPTRACE_GETREGSが含まれていなかった。どうりでレジスタの値が取得できないわけだ。

ちなみに、x86の場合だとarch_ptrace関数の中に、PTRACE_GETREGSに該当する処理が記述されており、アタッチしているプロセスのレジスタを取得する処理が書かれている。

 case PTRACE_GETREGS:  /* Get all gp regs from the child. */
        return copy_regset_to_user(child,
                       task_user_regset_view(current),
                       REGSET_GENERAL,
                       0, sizeof(struct user_regs_struct),
                       datap);

同じようにPTRACE_SINGLESTEPについても、該当するソースコードを見てみる。

int ptrace_request(struct task_struct *child, long request,
           unsigned long addr, unsigned long data)
{
    bool seized = child->ptrace & PT_SEIZED;
    int ret = -EIO;
    kernel_siginfo_t siginfo, *si;
    void __user *datavp = (void __user *) data;
    unsigned long __user *datalp = datavp;
    unsigned long flags;

    switch (request) {
    case PTRACE_PEEKTEXT:
    case PTRACE_PEEKDATA:
        return generic_ptrace_peekdata(child, addr, data);
    case PTRACE_POKETEXT:
    case PTRACE_POKEDATA:
    
    // .. 長いので省略
    
    
#ifdef PTRACE_SINGLESTEP
    case PTRACE_SINGLESTEP:
#endif
#ifdef PTRACE_SINGLEBLOCK
    case PTRACE_SINGLEBLOCK:
#endif
#ifdef PTRACE_SYSEMU
    case PTRACE_SYSEMU:
    case PTRACE_SYSEMU_SINGLESTEP:
#endif
    case PTRACE_SYSCALL:
    case PTRACE_CONT:
        return ptrace_resume(child, request, data);
        
    // .. 長いので省略

PTRACE_SINGLESTEPが選択された際には、ptrace_resumeが呼ばれる。

static int ptrace_resume(struct task_struct *child, long request,
             unsigned long data)
{
    bool need_siglock;

    if (!valid_signal(data))
        return -EIO;

    if (request == PTRACE_SYSCALL)
        set_tsk_thread_flag(child, TIF_SYSCALL_TRACE);
    else
        clear_tsk_thread_flag(child, TIF_SYSCALL_TRACE);

#ifdef TIF_SYSCALL_EMU
    if (request == PTRACE_SYSEMU || request == PTRACE_SYSEMU_SINGLESTEP)
        set_tsk_thread_flag(child, TIF_SYSCALL_EMU);
    else
        clear_tsk_thread_flag(child, TIF_SYSCALL_EMU);
#endif

    if (is_singleblock(request)) {
        if (unlikely(!arch_has_block_step()))
            return -EIO;
        user_enable_block_step(child);
    } else if (is_singlestep(request) || is_sysemu_singlestep(request)) { // <-- single stepの際に実行される部分
        if (unlikely(!arch_has_single_step())) // arch_has_single_step マクロが asm/ptrace.h に定義されている場合には1となるが、それ以外は0になる。
            return -EIO;
        user_enable_single_step(child);
    } else {
        user_disable_single_step(child);
    }
    
    // .. 省略

arch/riscv/include/asm/ptrace.hを見てみると、arch_has_single_stepは定義されていない。つまり、PTRACE_SINGLESTEPを引数に設定して実行したとしても、-EIOのエラーが返ってくるだけである。

RISC-VのLinux、ptraceの実装に関してはまだまだ不完全なところが多い。

ちなみにアタッチしているプロセスのレジスタの情報を取得する場合には、

    struct user_regs_struct regs = {0};
    struct iovec iov;
    iov.iov_len = sizeof(regs);
    iov.iov_base = &regs;
    int ret = ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov);
    printf("%llx: \n", regs.pc);

のように書けばできる。

QEMU RISC-V Fedora の Dockerfileを更新した

RISC-V を簡単に試すことができるDockerfileを作成した。Qiitaの記事からDockerfileの更新したもの。Docker Hubから利用できる。

前回からの変更点として、ブートローダにbblが使われなくなっていることがあげられる。READMEにも以下のような記載がある。

This image introduces bootflow changes. We use now OpenSBI -> U-Boot (S-mode)
[extlinux] -> kernel. The kernel and initramfs are wrapped in U-Boot container
for now. This should change later this year once kernel header patches are
merged in kernel and U-Boot.

OpenSBI -> U-Boot -> kernel の順番で起動するようになったとのこと。

これに伴い、QEMUFedoraを起動する際のコマンドが変わった。

qemu-system-riscv64 -nographic -machine virt -smp 4 -m 2G -kernel fw_payload-uboot-qemu-virt-smode.elf -device virtio-blk-device,drive=hd0 -drive file=Fedora-Developer-Rawhide-20190703.n.0-sda.raw,format=raw,id=hd0 -device virtio-net-device,netdev=usernet -netdev user,id=usernet,hostfwd=tcp::10000-:22

-kernelにはfw_payload-uboot-qemu-virt-smode.elfファームウェアとして指定する。

OSの起動に成功すると以下のような標準出力が表示される。

OpenSBI v0.4 (Jul  3 2019 10:27:18)
   ____                    _____ ____ _____
  / __ \                  / ____|  _ \_   _|
 | |  | |_ __   ___ _ __ | (___ | |_) || |
 | |  | | '_ \ / _ \ '_ \ \___ \|  _ < | |
 | |__| | |_) |  __/ | | |____) | |_) || |_
  \____/| .__/ \___|_| |_|_____/|____/_____|
        | |
        |_|

Platform Name          : QEMU Virt Machine
Platform HART Features : RV64ACDFIMSU
Platform Max HARTs     : 8
Current Hart           : 2
Firmware Base          : 0x80000000
Firmware Size          : 112 KB
Runtime SBI Version    : 0.1

PMP0: 0x0000000080000000-0x000000008001ffff (A)
PMP1: 0x0000000000000000-0xffffffffffffffff (A,R,W,X)


U-Boot 2019.07-rc4 (Jul 02 2019 - 11:46:47 +0000)

CPU:   rv64imafdcsu
Model: riscv-virtio,qemu
DRAM:  2 GiB
In:    uart@10000000
Out:   uart@10000000
Err:   uart@10000000
Net:   
Warning: virtio-net#1 using MAC address from ROM
eth0: virtio-net#1
Hit any key to stop autoboot:  0 

Device 0: QEMU VirtIO Block Device
            Type: Hard Disk
            Capacity: 8192.0 MB = 8.0 GB (16777216 x 512)
... is now current device
Scanning virtio 0:1...
Found /extlinux/extlinux.conf
Retrieving file: /extlinux/extlinux.conf
576 bytes read in 10 ms (55.7 KiB/s)
Ignoring unknown command: ui
Ignoring malformed menu command:  autoboot
Ignoring malformed menu command:  hidden
Ignoring unknown command: totaltimeout
Fedora-Developer-Rawhide-20190703.n.0 Boot Options.
1:  Fedora-Developer-Rawhide-20190703.n.0 (5.2.0-0.rc7.git0.1.0.riscv64.fc31.riscv64)
Enter choice: 1:    Fedora-Developer-Rawhide-20190703.n.0 (5.2.0-0.rc7.git0.1.0.riscv64.fc31.riscv64)
Retrieving file: /uInitrd-5.2.0-0.rc7.git0.1.0.riscv64.fc31.riscv64
15447501 bytes read in 89 ms (165.5 MiB/s)
Retrieving file: /uImage-5.2.0-0.rc7.git0.1.0.riscv64.fc31.riscv64
7603947 bytes read in 48 ms (151.1 MiB/s)
append: ro root=UUID=3b75927f-d59f-4b04-96ce-823ead0046bc rhgb quiet LANG=en_US.UTF-8
## Booting kernel from Legacy Image at 84000000 ...
   Image Name:   5.2.0-0.rc7.git0.1.0.riscv64.fc3
   Image Type:   RISC-V Linux Kernel Image (gzip compressed)
   Data Size:    7603883 Bytes = 7.3 MiB
   Load Address: 80200000
   Entry Point:  80200000
   Verifying Checksum ... OK
## Loading init Ramdisk from Legacy Image at 88300000 ...
   Image Name:   initramfs
   Image Type:   RISC-V Linux RAMDisk Image (gzip compressed)
   Data Size:    15447437 Bytes = 14.7 MiB
   Load Address: 00000000
   Entry Point:  00000000
   Verifying Checksum ... OK
## Flattened Device Tree blob at ff76dd40
   Booting using the fdt blob at 0xff76dd40
   Uncompressing Kernel Image ... OK
   Using Device Tree in place at 00000000ff76dd40, end 00000000ff771d99

Starting kernel ...

以前と比較してOSの起動にかなり時間がかかる。気長に待つのが良い。