割り込みによるスタックの上方の値の予期せぬ書き換えについて

,

CTFやbrainfuck golfではスタックの進む先の空間を普通の領域として利用することがある。 この領域が勝手に書き変わる場合について、具体例としてsignalによる割り込みを思い付いたため検証した。 結論としては、signalが飛び自分で設定したhandlerが走ると壊れることがあるということである。

設定

以下のようなC言語のプログラムを考える。 整数を入力させをれをそのまま出力するだけのプログラムである。 ただし、スタックの進む先のアドレスに対し、そこへ一瞬だけ書き込んで読み出す。

int main(void) {
    int x, *p;
    p = &x - 0x10;
    scanf("%d", &x);
    *p = x;
    x = *p;
    printf("%d\n", x);
}

これは間違いなく規格違反だろうが、実際のところ何事もなかったかのように動く。

$ gcc --version
gcc (Ubuntu 5.4.1-2ubuntu1~16.04) 5.4.1 20160904
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc a.c
$ ./a.out
1234
1234

signalが絡むとこのようなプログラムが失敗しうることを確認する。

確認

先に通常の場合を確認する。

この上の例をdisassembleすると以下のようになる。 *p = x;からx = *p;の間には特に他の命令はないため、ほぼ間違いなく値は保存されると言ってよいだろう。 この間にはpを触らない命令なら他に何を入れてもよいが、もちろん関数をcallするなどすれば値は壊れうる。

00000000004005f6 <main>:
  4005f6:       55                      push   rbp
  4005f7:       48 89 e5                mov    rbp,rsp
  4005fa:       48 83 ec 20             sub    rsp,0x20
  4005fe:       64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
  400605:       00 00 
  400607:       48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
  40060b:       31 c0                   xor    eax,eax
  # p = &x - 0x10;
  40060d:       48 8d 45 ec             lea    rax,[rbp-0x14]
  400611:       48 83 e8 40             sub    rax,0x40
  400615:       48 89 45 f0             mov    QWORD PTR [rbp-0x10],rax
  # scanf("&d", &x);
  400619:       48 8d 45 ec             lea    rax,[rbp-0x14]
  40061d:       48 89 c6                mov    rsi,rax
  400620:       bf f4 06 40 00          mov    edi,0x4006f4
  400625:       b8 00 00 00 00          mov    eax,0x0
  40062a:       e8 b1 fe ff ff          call   4004e0 <[email protected]>
  # *p = x;
  40062f:       8b 55 ec                mov    edx,DWORD PTR [rbp-0x14]
  400632:       48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
  400636:       89 10                   mov    DWORD PTR [rax],edx
  # x = *p;
  400638:       48 8b 45 f0             mov    rax,QWORD PTR [rbp-0x10]
  40063c:       8b 00                   mov    eax,DWORD PTR [rax]
  40063e:       89 45 ec                mov    DWORD PTR [rbp-0x14],eax
  # printf("%d\n", x);
  400641:       8b 45 ec                mov    eax,DWORD PTR [rbp-0x14]
  400644:       89 c6                   mov    esi,eax
  400646:       bf f7 06 40 00          mov    edi,0x4006f7
  40064b:       b8 00 00 00 00          mov    eax,0x0
  400650:       e8 6b fe ff ff          call   4004c0 <[email protected]>
  # return 0;
  400655:       b8 00 00 00 00          mov    eax,0x0
  40065a:       48 8b 4d f8             mov    rcx,QWORD PTR [rbp-0x8]
  40065e:       64 48 33 0c 25 28 00    xor    rcx,QWORD PTR fs:0x28
  400665:       00 00 
  400667:       74 05                   je     40066e <main+0x78>
  400669:       e8 42 fe ff ff          call   4004b0 <[email protected]>
  40066e:       c9                      leave  
  40066f:       c3                      ret    

signal

disassemble結果としてはまったく問題なくても値が保存されない例として、signalが考えられる。 つまり、*p = x;からx = *p;の間で何らかのsignalが飛びそのhandlerが呼ばれた場合、このhandlerは値を壊しうる。

狙った位置でsignalを飛ばすためbusy waitを入れ、検証コードは以下のようになった。

#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>

void func(void) {
    long long x;
    long long *p;
    p = &x - 0x20;
    scanf("%lld", &x);
    *p = x;
    while (x --) ; // busy wait
    x = *p;
    printf("%lld\n", x);
}

void handler(int sig) {
    char buf[4096];
    memset(buf, 0, sizeof(buf));
}

int main(void) {
    signal(SIGALRM, handler);
    alarm(4);
    func();
    return 0;
}

実際、実行すると以下のようになる。 signalによる割り込みが発生した場合では、結果が壊れていることが分かる。

$ echo 123456789 | time ./a.out
123456789
0.25s 1424KB
$ echo 12345678999 | time ./a.out
0
22.47s 1424KB

ただし、手元の環境では、SIGSTOPSIGCONTのdefault handlerではstackの破壊は起きなかった。