FFRIエンジニアブログ

株式会社FFRIセキュリティのエンジニアが執筆する技術者向けブログです

RISC-V での簡単なアンチ逆アセンブル

はじめに

FFRI リサーチエンジニアの中川です。今回は小ネタではありますが、RISC-V において使えるアンチ逆アセンブルについて簡単に紹介します。

RISC-V について知らない方は、以前公開した記事に簡単にまとめましたので、そちらをご参照ください。

RISC-VのC拡張について

アンチ逆アセンブルを紹介する前に、RISC-V の C 拡張について簡単に説明します。

RISC-V の命令長は基本 32bit ですが、頻繁に使われる命令に関しては同等の操作を行える 16bit の縮約命令を利用することができます (ただし、C 拡張をサポートしている場合に限られます)。addi sp,sp,-176 というスタックポインタを操作する命令を例にとって見てみます。以下のようなアセンブリを書いてコンパイルします。

// test.s
_start:
        addi sp, sp, -176
        .option norvc
        addi sp, sp, -176

.option norvc というディレクティブがありますが、これはアセンブラに縮約命令を使わないように指示するディレクティブです。これが指定される前と後で命令長が変わることを逆アセンブル結果を見て確認してみましょう。

$ gcc -c hoge.s
$ objdump -d hoge.o
hoge.o:     file format elf64-littleriscv


Disassembly of section .text:

0000000000000000 <_start>:
   0:   7171                    addi    sp,sp,-176
   2:   f5010113                addi    sp,sp,-176

2 列目を見ると、同じ addi sp,sp,-176 という命令であるにもかかわらず、命令長が違っているのがわかります。

実際に実アプリケーションでどの程度、この縮約命令が使われているのかも見てみましょう。以下には、Fedora 30 に含まれる ls のバイナリを逆アセンブルした結果を示します。

# objdump -d /bin/ls | less
...
 00000000000040f0 <main@@Base>:
    40f0:       7171                    addi    sp,sp,-176
    40f2:       f506                    sd      ra,168(sp)
    40f4:       f122                    sd      s0,160(sp)
    40f6:       ed26                    sd      s1,152(sp)
    40f8:       e94a                    sd      s2,144(sp)
    40fa:       e54e                    sd      s3,136(sp)
    40fc:       e152                    sd      s4,128(sp)
    40fe:       fcd6                    sd      s5,120(sp)
    4100:       f8da                    sd      s6,112(sp)
    4102:       f4de                    sd      s7,104(sp)
    4104:       f0e2                    sd      s8,96(sp)
    4106:       ece6                    sd      s9,88(sp)
    4108:       e8ea                    sd      s10,80(sp)
    410a:       e4ee                    sd      s11,72(sp)
    410c:       00019797                auipc   a5,0x19
    4110:       5847b783                ld      a5,1412(a5) # 1d690 <__stack_chk_guard@GLIBC_2.27>
    4114:       639c                    ld      a5,0(a5)
    4116:       892a                    mv      s2,a0
    4118:       6188                    ld      a0,0(a1)
    411a:       fc3e                    sd      a5,56(sp)
    411c:       8a2e                    mv      s4,a1
    ...

2 列目を見ると、ほとんどが 16bit の命令になっていて、一部 32bit の命令になっているぐらいだということがわかります。このように、縮約命令があることで、非常に効果的に命令バイトサイズを削減できます。どの程度命令サイズを削減できるのかについては、文献 [1] に詳しく記載があるので、興味のある方はそちらも合わせてご参照ください。

この C 拡張を利用し、アンチ逆アセンブルを実現してみます。

アンチ逆アセンブルの方針

RISC-V のアンチ逆アセンブルについて説明する前に、x86 で利用されているアンチ逆アセンブルについて紹介します。

x86 のように可変長の命令セットの場合、複数通りに命令を解釈できるバイト列が存在します。x86 でアンチ逆アセンブルを実現する場合にはこの性質を利用します。具体例を見たほうが早いので、Practical Malware Analysis [2] に掲載されている例を出しますと

  jmp short near ptr loc_2+1 ; 次のcall命令の先頭から 1 バイト進めたアドレスにジャンプ
loc_2:
  call near ptr 15FF1A71h
  or [ecx], dl
  inc eax
  db 0

一見問題なく逆アセンブルできているように見えますが、謎の jmp 命令が見えます。このジャンプ先ですが、次の call 命令の先頭から 1 バイト先のアドレスになっています。そのため、上記の逆アセンブル結果で見えている call or inc などの命令は実行されず、実際に実行されるアセンブリは以下のようなものになります。

  jmp short loc_3
  db 0E8h ; 逆アセンブル結果を騙すための不要なデータ (以下ではガベージバイトと呼ぶ) が埋め込まれており、逆アセンブル結果がおかしくなっていた
loc_3:
  push 2Ah ; 第一引数をpushして
  call Sleep 1 ; Sleep関数をcall

このように、実行させたい命令が埋め込まれている別の命令を挿入し、ガベージバイト分だけジャンプすることにより、逆アセンブラを騙すことができます。

RISC-V でも、32bit 命令の上位 16bit に縮約命令として実行させたい命令を埋め込みジャンプすることで、同じように逆アセンブラを騙すことができます。

RISC-V でのアンチ逆アセンブル

今回は以下のコードにアンチ逆アセンブルを施します。

// main.c
#include <stdio.h>

extern void fizzbuzz(int in);

int main() {
        for (int i = 1; i < 100; i++) {
                printf("%d ", i);
                fizzbuzz(i);
        }
}
// fizzbuzz.s
        .globl  fizzbuzz
fizzbuzz:
        li      a5,15
        remw    a5,a0,a5
        beqz    a5,.puts_fizz_buzz
        li      a5,5
        remw    a5,a0,a5
        beqz    a5,.puts_buzz
        li      a5,3
        remw    a0,a0,a5
        bnez    a0,.puts_empty
        lla     a0,.fizz_str
        tail    puts@plt
.puts_buzz:
        lla     a0,.buzz_str
        tail    puts@plt
.puts_fizz_buzz:
        lla     a0,.fizz_buzz_str
        tail    puts@plt
.puts_empty:
        lla     a0,.empty_str
        tail    puts@plt
.fizz_buzz_str:
        .string "Fizz Buzz"
        .zero   6
.buzz_str:
        .string "Buzz"
        .zero   3
.fizz_str:
        .string "Fizz"
        .zero   3
.empty_str:
        .string ""
        .zero   7

アセンブリで書いた FizzBuzz です。「FizzBuzz って何?」という人のために簡単に説明をしておくと、与えられた数字が 3 で割り切れる場合には Fizz、5 で割り切れる場合には Buzz、両方で割り切れる場合には FizzBuzz と表示されるプログラムのことです。

コンパイルして実行すると以下のような表示が出ます。

$ gcc fizzbuzz.s main.c -o fizzbuzz.out
$ ./fizzbuzz.out
1
2
3 Fizz
4
5 Buzz
6 Fizz
7
8
9 Fizz
10 Buzz
11
12 Fizz
13
14
15 Fizz Buzz
16
17
18 Fizz
19
20 Buzz
21 Fizz
22
23
24 Fizz
25 Buzz
26
27 Fizz
28
29
30 Fizz Buzz
31
32
33 Fizz

まず、アンチ逆アセンブルの対象とする命令を決めます。このままだとどの命令が 16bit の縮約命令になっているのかが判断できないので、実行バイナリを objdump した結果を見ます。

0000000000000676 <fizzbuzz>:
 676:   47bd                    li      a5,15 ; <-- 今回アンチ逆アセンブルの対象とする 16bit 命令
 678:   02f567bb                remw    a5,a0,a5
 67c:   c78d                    beqz    a5,6a6 <.puts_fizz_buzz>
 67e:   4795                    li      a5,5
 680:   02f567bb                remw    a5,a0,a5
 684:   cb99                    beqz    a5,69a <.puts_buzz>
 686:   478d                    li      a5,3
 688:   02f5653b                remw    a0,a0,a5
 68c:   e11d                    bnez    a0,6b2 <.puts_empty>
 68e:   00000517                auipc   a0,0x0
 692:   04850513                addi    a0,a0,72 # 6d6 <.fizz_str>
 696:   ebbff06f                j       550 <puts@plt>

000000000000069a <.puts_buzz>:
 69a:   00000517                auipc   a0,0x0
 69e:   03450513                addi    a0,a0,52 # 6ce <.buzz_str>
 6a2:   eafff06f                j       550 <puts@plt>

00000000000006a6 <.puts_fizz_buzz>:
 6a6:   00000517                auipc   a0,0x0
 6aa:   01850513                addi    a0,a0,24 # 6be <.fizz_buzz_str>
 6ae:   ea3ff06f                j       550 <puts@plt>

00000000000006b2 <.puts_empty>:
 6b2:   00000517                auipc   a0,0x0
 6b6:   02c50513                addi    a0,a0,44 # 6de <.empty_str>
 6ba:   e97ff06f                j       550 <puts@plt>

li a5,15 という 16bit の命令が見えます。これを 32bit の別の命令に埋め込み、その上位 16bit にジャンプするようにしてみます。

li a5,15 のオペコード (0x47bd) が上位 16bit に埋め込まれた命令を探してみると beq s10, s11, pc + 3168 (命令オペコードが0x47bd00e3) などが見つかります。そこで、beq s10, s11, pc + 3168 を埋め込み、合わせてその直前に 4 バイトのジャンプ命令を埋め込みます。

        .globl  fizzbuzz
fizzbuzz:
        .half 0xa011 ; j pc + 0x4のオペコード
        .word 0x47bd00e3 ; li a5,15が上位16bitに埋め込まれている
        remw    a5,a0,a5
        beqz    a5,.puts_fizz_buzz
        li      a5,5
        remw    a5,a0,a5
        beqz    a5,.puts_buzz
        li      a5,3
        remw    a0,a0,a5
        bnez    a0,.puts_empty
        lla     a0,.fizz_str
        tail    puts@plt
.puts_buzz:
        lla     a0,.buzz_str
        tail    puts@plt
.puts_fizz_buzz:
        lla     a0,.fizz_buzz_str
        tail    puts@plt
.puts_empty:
        lla     a0,.empty_str
        tail    puts@plt
.fizz_buzz_str:
        .string "Fizz Buzz"
        .zero   6
.buzz_str:
        .string "Buzz"
        .zero   3
.fizz_str:
        .string "Fizz"
        .zero   3
.empty_str:
        .string ""
        .zero   7

コンパイルしてできた実行ファイルを objdump で逆アセンブルすると

00000000000104a0 <fizzbuzz>:
   104a0:       a011                    j       104a4 <fizzbuzz+0x4>
   104a2:       47bd00e3                beq     s10,s11,11102 <__FRAME_END__+0xaa6> ; li a5,15 命令が上位 16bit に埋め込まれている
   104a6:       02f567bb                remw    a5,a0,a5
   104aa:       c78d                    beqz    a5,104d4 <.puts_fizz_buzz>
   104ac:       4795                    li      a5,5
   104ae:       02f567bb                remw    a5,a0,a5
   104b2:       cb99                    beqz    a5,104c8 <.puts_buzz>
   104b4:       478d                    li      a5,3
   104b6:       02f5653b                remw    a0,a0,a5
   104ba:       e11d                    bnez    a0,104e0 <.puts_empty>
   104bc:       00000517                auipc   a0,0x0
   104c0:       04850513                addi    a0,a0,72 # 10504 <.fizz_str>
   104c4:       f0dff06f                j       103d0 <puts@plt>

00000000000104c8 <.puts_buzz>:
   104c8:       00000517                auipc   a0,0x0
   104cc:       03450513                addi    a0,a0,52 # 104fc <.buzz_str>
   104d0:       f01ff06f                j       103d0 <puts@plt>

00000000000104d4 <.puts_fizz_buzz>:
   104d4:       00000517                auipc   a0,0x0
   104d8:       01850513                addi    a0,a0,24 # 104ec <.fizz_buzz_str>
   104dc:       ef5ff06f                j       103d0 <puts@plt>

00000000000104e0 <.puts_empty>:
   104e0:       00000517                auipc   a0,0x0
   104e4:       02c50513                addi    a0,a0,44 # 1050c <.empty_str>
   104e8:       ee9ff06f                j       103d0 <puts@plt>

00000000000104ec <.fizz_buzz_str>:
   104ec:       6946                    ld      s2,80(sp)
   104ee:       7a7a                    ld      s4,440(sp)
   104f0:       4220                    lw      s0,64(a2)
   104f2:       7a75                    lui     s4,0xffffd
   104f4:       007a                    c.slli  zero,0x1e
   104f6:       0000                    unimp
   104f8:       0000                    unimp

li a5,15 命令が消えてしまい、 jbeq 命令の2つになっているのがわかります。このように逆アセンブル結果を騙すことができます。

今回は 4 バイトの相対ジャンプ命令を埋め込み、32bit 命令の上位 16bit にジャンプする形式を取りましたが、不要な命令をジャンプ命令と実行させたい命令との間に何命令か追加することにより、解析をより困難にすることも可能です。

まとめ

今回は RISC-V で縮約命令を使ったアンチ逆アセンブルについて紹介しました。今回紹介したアンチ逆アセンブルは、耐解析の技術の一つとして今後利用できるのではないかと思われます。また、今回は 16bit 命令を 32bit 命令の上位に埋め込むことを実行しましたが、32bit の命令を 2 つ連結させ、下位 16bit をガベージバイトとし、32bit + 16bit の命令として解釈させるというやり方もあり得ると思います。別のアンチ逆アセンブル手法の考案に関しては今後の課題です。

参考文献

[1] The Renewed Case for the Reduced Instruction Set Computer: Avoiding ISA Bloat with Macro-Op Fusion for RISC-V, https://people.eecs.berkeley.edu/~krste/papers/EECS-2016-130.pdf

[2] Practical malware analysis: the hands-on guide to dissecting malicious software. No Starch Press, 2012.