いい感じに生きる

低レイヤとMCUとMr.childrenを愛しています。

セキュリティキャンプ応募課題で学ぶアセンブリ言語(2020/02/12)

(2022/1/17)Qiitaからhatenaに移行しました。

はじめに

この記事はアセンブリ言語に慣れることを目的としているため、かなり遠回りな解き方になっています。

セキュリティ・キャンプ2019 A【脆弱性・マルウェア解析トラック】問6

問題

以下にDebian 9.8(amd64)上で動作するプログラムflatteningのmain関数の逆アセンブル結果(1)とmain関数で使われているデータ領域のダンプ結果(2)があります。 このプログラムは、コマンドライン引数としてある特定の文字列を指定されたときのみ実行結果が0となり、それ以外の場合は実行結果が1となります。 この実行結果が0となる特定の文字列を探し、その文字列を得るまでに考えたことや試したこと、使ったツール、抱いた感想等について詳細に報告してください。

(*1)"objdump -d -Mintel flattening"の出力結果のうち、main関数の箇所を抜粋しました。

0000000000000530 <main>:
 530:   48 8d 15 7d 03 00 00    lea    rdx,[rip+0x37d]        # 8b4 <_IO_stdin_used+0x4>
 537:   c7 44 24 f4 00 00 00    mov    DWORD PTR [rsp-0xc],0x0
 53e:   00 
 53f:   49 ba 9e fa 95 ef 92    movabs r10,0xedd5a792ef95fa9e
 546:   a7 d5 ed 
 549:   41 b9 cc ff ff ff       mov    r9d,0xffffffcc
 54f:   90                      nop
 550:   8b 44 24 f4             mov    eax,DWORD PTR [rsp-0xc]
 554:   83 f8 0d                cmp    eax,0xd
 557:   77 23                   ja     57c <main+0x4c>
 559:   48 63 04 82             movsxd rax,DWORD PTR [rdx+rax*4]
 55d:   48 01 d0                add    rax,rdx
 560:   ff e0                   jmp    rax
 562:   66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]
 568:   c7 44 24 f4 09 00 00    mov    DWORD PTR [rsp-0xc],0x9
 56f:   00 
 570:   8b 44 24 f4             mov    eax,DWORD PTR [rsp-0xc]
 574:   83 c1 01                add    ecx,0x1
 577:   83 f8 0d                cmp    eax,0xd
 57a:   76 dd                   jbe    559 <main+0x29>
 57c:   b8 01 00 00 00          mov    eax,0x1
 581:   c3                      ret    
 582:   66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]
 588:   48 63 c1                movsxd rax,ecx
 58b:   c7 44 24 f4 0c 00 00    mov    DWORD PTR [rsp-0xc],0xc
 592:   00 
 593:   44 0f b6 44 04 f8       movzx  r8d,BYTE PTR [rsp+rax*1-0x8]
 599:   eb b5                   jmp    550 <main+0x20>
 59b:   0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]
 5a0:   48 63 c1                movsxd rax,ecx
 5a3:   45 8d 1c 08             lea    r11d,[r8+rcx*1]
 5a7:   c7 44 24 f4 0b 00 00    mov    DWORD PTR [rsp-0xc],0xb
 5ae:   00 
 5af:   44 30 5c 04 f8          xor    BYTE PTR [rsp+rax*1-0x8],r11b
 5b4:   eb 9a                   jmp    550 <main+0x20>
 5b6:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
 5bd:   00 00 00 
 5c0:   83 f9 07                cmp    ecx,0x7
 5c3:   0f 86 18 01 00 00       jbe    6e1 <main+0x1b1>
 5c9:   c7 44 24 f4 0d 00 00    mov    DWORD PTR [rsp-0xc],0xd
 5d0:   00 
 5d1:   e9 7a ff ff ff          jmp    550 <main+0x20>
 5d6:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
 5dd:   00 00 00 
 5e0:   c7 44 24 f4 09 00 00    mov    DWORD PTR [rsp-0xc],0x9
 5e7:   00 
 5e8:   31 c9                   xor    ecx,ecx
 5ea:   e9 61 ff ff ff          jmp    550 <main+0x20>
 5ef:   90                      nop
 5f0:   c7 44 24 f4 08 00 00    mov    DWORD PTR [rsp-0xc],0x8
 5f7:   00 
 5f8:   45 89 c8                mov    r8d,r9d
 5fb:   e9 50 ff ff ff          jmp    550 <main+0x20>
 600:   83 f9 08                cmp    ecx,0x8
 603:   0f 85 73 ff ff ff       jne    57c <main+0x4c>
 609:   c7 44 24 f4 07 00 00    mov    DWORD PTR [rsp-0xc],0x7
 610:   00 
 611:   e9 3a ff ff ff          jmp    550 <main+0x20>
 616:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
 61d:   00 00 00 
 620:   83 c1 01                add    ecx,0x1
 623:   c7 44 24 f4 02 00 00    mov    DWORD PTR [rsp-0xc],0x2
 62a:   00 
 62b:   e9 20 ff ff ff          jmp    550 <main+0x20>
 630:   48 63 c1                movsxd rax,ecx
 633:   80 7c 04 f8 00          cmp    BYTE PTR [rsp+rax*1-0x8],0x0
 638:   0f 85 96 00 00 00       jne    6d4 <main+0x1a4>
 63e:   c7 44 24 f4 06 00 00    mov    DWORD PTR [rsp-0xc],0x6
 645:   00 
 646:   e9 05 ff ff ff          jmp    550 <main+0x20>
 64b:   0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]
 650:   4c 8b 5e 08             mov    r11,QWORD PTR [rsi+0x8]
 654:   48 63 c1                movsxd rax,ecx
 657:   c7 44 24 f4 04 00 00    mov    DWORD PTR [rsp-0xc],0x4
 65e:   00 
 65f:   45 0f b6 1c 03          movzx  r11d,BYTE PTR [r11+rax*1]
 664:   44 88 5c 04 f8          mov    BYTE PTR [rsp+rax*1-0x8],r11b
 669:   e9 e2 fe ff ff          jmp    550 <main+0x20>
 66e:   66 90                   xchg   ax,ax
 670:   83 f9 07                cmp    ecx,0x7
 673:   77 c9                   ja     63e <main+0x10e>
 675:   c7 44 24 f4 03 00 00    mov    DWORD PTR [rsp-0xc],0x3
 67c:   00 
 67d:   e9 ce fe ff ff          jmp    550 <main+0x20>
 682:   66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]
 688:   c7 44 24 f4 02 00 00    mov    DWORD PTR [rsp-0xc],0x2
 68f:   00 
 690:   31 c9                   xor    ecx,ecx
 692:   e9 b9 fe ff ff          jmp    550 <main+0x20>
 697:   66 0f 1f 84 00 00 00    nop    WORD PTR [rax+rax*1+0x0]
 69e:   00 00 
 6a0:   83 ff 02                cmp    edi,0x2
 6a3:   0f 85 d3 fe ff ff       jne    57c <main+0x4c>
 6a9:   c7 44 24 f4 01 00 00    mov    DWORD PTR [rsp-0xc],0x1
 6b0:   00 
 6b1:   e9 9a fe ff ff          jmp    550 <main+0x20>
 6b6:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
 6bd:   00 00 00 
 6c0:   4c 39 54 24 f8          cmp    QWORD PTR [rsp-0x8],r10
 6c5:   74 27                   je     6ee <main+0x1be>
 6c7:   c7 44 24 f4 0e 00 00    mov    DWORD PTR [rsp-0xc],0xe
 6ce:   00 
 6cf:   e9 7c fe ff ff          jmp    550 <main+0x20>
 6d4:   c7 44 24 f4 05 00 00    mov    DWORD PTR [rsp-0xc],0x5
 6db:   00 
 6dc:   e9 6f fe ff ff          jmp    550 <main+0x20>
 6e1:   c7 44 24 f4 0a 00 00    mov    DWORD PTR [rsp-0xc],0xa
 6e8:   00 
 6e9:   e9 62 fe ff ff          jmp    550 <main+0x20>
 6ee:   31 c0                   xor    eax,eax
 6f0:   c3                      ret    
 6f1:   66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
 6f8:   00 00 00 
 6fb:   0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]

(*2)"objdump -s flattening"の出力結果のうち、セクション .rodata の内容を抜粋しました。

セクション .rodata の内容:
 08b0 01000200 ecfdffff d4fdffff bcfdffff  ................
 08c0 9cfdffff 7cfdffff 6cfdffff 4cfdffff  ....|...l...L...
 08d0 3cfdffff 2cfdffff 0cfdffff ecfcffff  <...,...........
 08e0 d4fcffff b4fcffff 0cfeffff           ............ 

とりあえず眺める

ぱっと眺めてみて気がついたことは、

  • call命令がない(main関数のみで処理が完結している)
  • そこら中にjmp 550がある(ループしそう?)
  • 560にjmp raxがある(アドレスではなくレジスタの値が参照されている、謎)
  • 08b0,08c0,80d0,08e0のそれぞれ0~eまでのメモリダンプ
  • 1行目530のコメントアウトはメモリダンプと関係してそう(8b4)
  • 581と6f0にret命令

これくらいでした。 あまりにもjmp 550が多いことから、このプログラムは550からの処理にすべての愛を捧げていると判断したので、まずは550から読んでいきます。

550からの処理

まずeaxにDWORD PTR[rsp-0xc]の値を代入し、eaxと0xdをcmpで比較しています。537でDWORD PTR[rsp-0xc]に0x0を代入していることから、初回の処理のeaxの値は0x0であるとわかります。その後557でja命令、つまりeax>0xdとなった場合に57cへジャンプします。 ※57cからの処理は次のプロットで記述します。

  • ja命令について この問題はjaのような条件付きジャンプ命令が頻出するので、曖昧な方は復習するといいかもしれません。よかったらこちらの記事をどうぞ。

  • DWORD PTRについて DWORDは4byteとしてデータを扱うことを意味しています。ptrはデータの型を指定する演算子です。他にもWORD,QWORDなどが出てきます、下の表を参考にしてみてください。

Name Byte Bit
BYTE 1 8
WORD 2 16
DWORD 4 32
QWORD 8 64

57cへジャンプしなかった場合(eax<0xdだった場合)、559でDWORD PTR[rdx+rax*4]の値を64bitに拡張(movsxd)してraxに代入します。その後rdx+raxをraxに格納し、560でraxに格納されているアドレスへジャンプします。よってraxはループの度に値が変わり、それをもとに560で様々なアドレスへjmpしていると考えられます。

ret命令(57cからの処理)

57cからの処理を見てみましょう。57cではeaxに0x1が代入され、581でretされています。eaxは一般的に関数の返り値として使われるレジスタなので、57cに飛んでしまうと返り値が1、つまり問題文の「それ以外の実行結果」になってしまいます。よって、57cにジャンプしないためにはeax<0xdである必要がある、すなわちeaxに0xe,0xfの値を代入しては行けないということがわかります。(16進数におけるdより大きな数はeとf)

もう1つのret命令を見ていきます。6f0の直前の6eeではxor eax,eax、すなわちeaxに0が格納されています。私は最初なぜ0が格納されるのか理解できなかったので、メモしておきます。

xor命令について xor命令は論理演算といわれる命令の1つで、排他的論理和と言います。 xor A,Bのとき 2つのデータが等しい(真)であれば0,等しくなければ1を格納します。 xor eax,eaxの場合AとBは等しいので、eaxに0が格納されます。 詳しくはこちらをどうぞ

先程説明した通りeaxは関数の返り値であるとすると、6eeにジャンプできればゴールが見えますね。6eeにジャンプするような命令を探すと、6c5に1つだけ該当するものがあります。

 6c0:   4c 39 54 24 f8          cmp    QWORD PTR [rsp-0x8],r10
 6c5:   74 27                   je     6ee <main+0x1be>

QWORD PTR [rsp-0x8]とr10の値が等しければ6eeにジャンプできることがわかります。

raxを求める

この問題ではこのraxの演算が肝なので、ここでしっかり触れておきます。もう一度raxの部分の処理を見てみましょう。

 559:   48 63 04 82             movsxd rax,DWORD PTR [rdx+rax*4]
 55d:   48 01 d0                add    rax,rdx
 560:   ff e0                   jmp    rax

559でDWORD PTR[rdx+rax*4]の値を64bitに拡張(movsxd)してraxに代入しますが、rdxが初見ですね。どこかでrdxに対する処理があるはずなので、これより上の処理を見てみましょう。すると、一行目に[rip+0x37d]の値がrdxに格納されていることがわかります。

530: 48 8d 15 7d 03 00 00    lea    rdx,[rip+0x37d]  # 8b4 <_IO_stdin_used+0x4>

ここで、コメントアウトされている8b4ってrip+0x37dなんじゃね?と悟ります。試しに0x8b4-0x37d=ripを求めると0x537(530の次の行のアドレス)になるため、rip+0x37d=0x8b4=rdxとして話を進めます。本当は同じ挙動プログラムを作りデバックするなりして検証するのが一番良いと思われます。冒頭の気づきにも書いたように、メモリダンプを見てみると、0x8b4が参照するデータは0xecだということがわかります。しかしここで1つ疑問が浮かびます。

このときrdxには0x8b4と0xec、どちらが格納されるのでしょうか?

答えは0x8b4です

私はここでつまづいたので、備忘録のためにも詳しく書いておきます。

lea命令とmov命令の違い 端的に表すと、 - leaは演算結果のアドレスをそのまま格納 - movは演算結果のアドレスが参照するデータを格納 つまり、leaはメモリアクセスを行わず、movはメモリアクセスを行う命令だということです。 ややこしいですが、慣れるしかありませんね。詳しくはこちらをどうぞ

ということで、rdxが0x8b4だということがわかりました。話を559の処理に戻します。 次にraxの値ですが、先述したようにDWORDは4byte(32bit)のデータとして扱うことを意味しているので、raxの下位4byte(32bit)、つまりeaxの値を扱うことになります。537,550の処理を踏まえるとeaxは0であることがわかりますね。

register.png この図の通りです。ソースはこちら

ということで559の処理はrdx = 0x8b4, rax = 0となるのでDWORD PTR [0x8b4+0*4] = 0x8b4となります。しかし、ここでraxに格納されるのは0x8b4ではありません。上記の通り、movはメモリアクセスを行うので0x8b4が参照するメモリダンプのデータを4byte分(DWORD)格納することになります。つまり0x8b4,0x8b5,0x8b6,0x8b7が指すデータなので、rax=0xfffdecとなります。(リトルエンディアンに注意!)

そして、55dで0xfffdec+0x8b4=0x6a0=raxとなり、jmp先は6a0となります。 6a0からの処理はediと0x2を比較し、値が等しくない場合に57cへジャンプするようになっています。この比較命令はコマンドライン引数の個数が1つであることを示していますが、その検証は今回は省きます。ジャンプしなかった場合はDWORD PTR [rsp-0xc]に0x1が代入され、550にjmpします。

ここで、 mov DWORD PTR [rsp-0xc],0x0 の処理が0x0から0xeまでjmp 550間に1つ行われていることに気が付きました。

550でDWORD PTR [rsp-0xc]の値はeaxに代入され、554のcmpでeax>0xdとなると57cに飛ばされてしまいます。よって、6c7にはジャンプしてはいけないということがわかりました。

6c7:   c7 44 24 f4 0e 00 00    mov    DWORD PTR [rsp-0xc],0xe

jmp! jmp! jmp!

そしてここから、 550からの処理でraxを求める(rdxは固定値) ↓ jmp raxでアドレスにジャンプ ↓ 飛んだ先で処理をしてjmp 550で帰ってくる を繰り返していきます。以下はこの一連の動作を1回とし、計14回(eax=0x0~0xd)のループ(アドレスと処理)についてまとめたものです。 DWORD PTR [rdx+rax*4](以下D)の値,jmp raxの値,eaxの値,jmp先の処理 という形式とします。

1: 8b4,6a0,0x0 edi≠0x2のとき57cへジャンプ(返り値1) edi=0x2のときeax=0x1

2: 8b8,688,0x1 eax=0x2 ecx=0(xor ecx,ecx)

3: 8bc,670,0x2 ecx>0x7のとき63eへジャンプ。それ以外のときはeax=0x3 ※63eの処理 eax=0x6

4: 8c0,650,0x3 rax=ecx eax=0x4 BYTE PTR [rsp+rax*1-0x8]=r11b

5: 8c4,630,0x4 r11b≠0のとき6d4へジャンプ r11b==0ときはeax=0x6; ※6d4の処理 eax=0x5

6:8c8,620,0x5 ecx+=0x1; eax=0x2;

7:8cc,600,0x6 ecx≠0x8のとき57cにジャンプ(返り値1) ecx=0x8のときeax=0x7

8:8d0,5f0,0x7 eax=0x8 r8d=0xffffffcc ※549よりr9d=0xffffffccであるから

9:8d4,5e0,0x8 eax=0x9 ecx=0

10:8d8,5c0,0x9 ecx<=0x7のとき6e1へジャンプ それ以外のときはeax=0xd ※6e1の処理 eax=0xa

11:8dc,5a0,0xa rax=ecx r11d=[r8+rcx1]; eax=0xb BYTE PTR[rsp+rax1-0x8] xor r11b

12:8e0,588,0xb rax=ecx eax=0xc r8d=BYTE PTR[rsp=rax*1-0x8]

13:8e4,568,0xc eax=0x9 ecx+=0x1 eax<=0xdのとき559にジャンプ それ以外のときはeax=0x1(返り値1) ※559の処理は10:と同様(eax=0x9であるため)

14:8e8,6c0,0xd 引数==r10のとき6eeにジャンプ 引数≠r10のときはeax=0xe(返り値1) ※6eeの処理 eax=0 ret(返り値0)

順に要点をまとめていきましょう。

プログラムの全体の流れは、

1→コマンドライン引数の個数の確認(1つ以外は返り値1)

2→ecx=0を格納

3~6→ループによって引数を1文字づつスキャンしメモリに格納

7→文字数の判定(8byte以外は返り値1)

8→r8d=0xffffffcc

9→ecx=0を格納

10~13→ループによって引数を1文字づつ引数を演算

14→演算結果がr10と等しいとき返り値に0

となります。このプログラムにおいて重要な処理は3~6,10~13なので、以下に詳しく記します。

3~6の処理

3~6のループは6でeax=0x2が格納され、次の550からの演算で3にジャンプする仕組みになっています。このループから抜け出すパターンは2つ。3でecx>7を満たし63eにジャンプするパターンと、BYTE PTR [rsp+rax*1-0x8]==0を満たしeax=0x6を格納させるパターンです。6は処理が行われる度にecxはインクリメントされているので、9回目のループで抜け出せることがわかります。(ecx=0x0~0x8)。

重要なのは4の下記の部分です。

 650:    4c 8b 5e 08             mov    r11,QWORD PTR [rsi+0x8]
 654:   48 63 c1                movsxd rax,ecx
            :
 65f:   45 0f b6 1c 03          movzx  r11d,BYTE PTR [r11+rax*1]
 664:   44 88 5c 04 f8          mov    BYTE PTR [rsp+rax*1-0x8],r11b

前述したようにrsiレジスタが使われていることから、r11には引数が格納されていると予想できます。eaxはインクリメントされているため、raxの値もループする度に0x1加算されます。これにより65f,664の演算も0x1づつ変化し、BYTE PTRとしてデータを扱っていることから、引数の文字列を1文字づつスキャンし格納していると考えることができます。

レジスタについて(64bit Linuxの場合) 汎用レジスタのr10,r11は自由に使っていいレジスタで、特に使用に制限がありません。ちなみにr8,r9は引数を入れるレジスタとして使われます。 ここでr11d,r11bレジスタはr11の下位4byte(DWORD)、下位1byte(BYTE)のことを表しています。ここをおさえないと10~13の処理を理解する上でつまづきます(つまづきました)。d,w,bは頭文字をとっていることを頭に入れておくといいかもしれません。

10~13の処理

10~13のループでは13の568でeax=0x9となり、577でeax<=0xd(eax=0x9なので必ずtrueになる)のとき559にジャンプ、559でeax=0x9として演算されるため10に戻る仕組みになっています。 10の処理を見ると、ecx<=0x7のときは6e1へジャンプします。6e1ではeax=0xaが格納され、11に移動します。9の5e8でecx=0が代入されていること、13の574でecxがインクリメントされていることから、8回目のループで抜け出せることがわかります。6e1へのジャンプを回避すると5c9でeax=0xdとなり、14に移ることができます。

続いて11,12です。ここが最も重要です。

 5a0: 48 63 c1                movsxd rax,ecx
 5a3:   45 8d 1c 08             lea    r11d,[r8+rcx*1]
 5a7:   c7 44 24 f4 0b 00 00    mov    DWORD PTR [rsp-0xc],0xb
 5ae:   00 
 5af:   44 30 5c 04 f8          xor    BYTE PTR [rsp+rax*1-0x8],r11b

rax=ecxとなるためループ毎にraxもインクリメントされていくことがわかります。次のlea命令は、r8+rcx*1の演算結果のアドレスがr11dに格納されます。r8d=0xffffffcc(r8dはr8の下位4byte)、rcxの初回の値は0(ecxはrcxの下位4byte)なのでr11dには0xffffffccが格納されていることがわかります。eaxに0xbを代入した後、r11b(r11dの下位1byte)と入力された引数の文字列とでxor演算を行っています。1byteはすなわち1文字分です。

 588: 48 63 c1                movsxd rax,ecx
 58b:   c7 44 24 f4 0c 00 00    mov    DWORD PTR [rsp-0xc],0xc
 592:   00 
 593:   44 0f b6 44 04 f8       movzx  r8d,BYTE PTR [rsp+rax*1-0x8]

rax=ecxとなるためこちらもループ毎にraxがインクリメントされていきます。eaxに0xcを代入したのち、r8dに11でxorされた演算結果が格納されます。movzxは、BYTE PTR [rsp+rax*1-0x8]では足りないr8dのbitを0で埋める命令です。

答え

10~13のループにより引数を1byteづつ、計8回xorで演算した結果がQWORD PTR [rsp-0x8]に格納されます。14よりその値がr10、すなわち0xedd5a792ef95fa9e(53fより)と等しければ返り値0となります。 つまりr10を1byteづつに区切り、初回のr11bの値をもとに計算式を立てると以下のようになります。

引数の1文字目 xor 0xcc(r11b) = 0x9e(r10)

r11bはr11d下位1byte、つまり0xffffffccの下位1byteなので0xccとなります。 r10はリトルエンディアンを考慮し1byteづつに区切ると、先頭から0x9e,0xfa,0x95,0xef,0x92,0xa7,0xd5,0xed となります。

引数の1文字目 xor 0xcc(r11b) = 0x9e(r10)より 引数の1文字目 = 0xcc xor 0x9e = 0x52

0x52はasciiコードで'R'を意味するので頭文字はRであることがわかりました。 これ以降は、raxがインクリメントされていることに注意し、r10のデータに加算して計算します。すると

2(文字目) xor 0x9f = 0xfa 3 xor 0xfc = 0x95 4 xor 0x98 = 0xef 5 xor 0xf3 = 0x92 6 xor 0x97 = 0xa7 7 xor 0xad = 0xd5 8 xor 0xdc = 0xed

となり、1文字目から8文字目までの文字を順に並べると

Reiwa0x1となります!