integer overflow 流行りな昨今 (2)

前回(クイズ)の続きです。自分で解いてみた結果を貼っておきます。(8)とか(9)はそれなりに面白いかと。

(1) 引数の与え方によっては、Buffer Overflow

bool func1(const char* name, std::size_t cbBuf) {
  unsigned short cbCalculatedBufSize = cbBuf;
  char* buf = (char*)std::malloc(cbCalculatedBufSize);
  if (buf) {
    std::memcpy(buf, name, cbBuf);
    // do stuff with buf
    std::free(buf);
    return true;
  }

  return false;
}

これは、

  static const char* const shellcode = "shellcode"; // dmy
  func1(shellcode, USHRT_MAX + 2);

のように呼ぶと、2行目で32bitから16bitへの切り捨てが行われ、

$ ltrace ./a.out 1
__libc_start_main(0x8048958, 2, 0xbfe5f114, 0x80489a4, 0x80489f8 <unfinished ...>
__strtol_internal("1", NULL, 10)                                                              = 1
malloc(1)                                                                                     = 

0x8158008
memcpy(0x8158008, "shellcode", 65537 <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

1バイトのmallocに対して65537バイトのmemcpyを行ってSEGVします。

(2) 同じくBO

void func2(int argc, char** argv) {
  unsigned short total;
  total = std::strlen(argv[1]) + std::strlen(argv[2]) + 1;
  char* buff = (char*)std::malloc(total);
  std::strcpy(buff, argv[1]);
  std::strcat(buff, argv[2]);
}

これは、

      char shellcode2[USHRT_MAX * 10 + 10] = {0};
      std::memset(shellcode2, 'a', sizeof(shellcode2) - 1);
      char* v[] = {"", shellcode2, " ", 0};
      func2(3, v);

のように呼ぶと、(1)と同様に3行目で切り捨てが起こり、

$ ltrace ./a.out 2
__libc_start_main(0x8048958, 2, 0xbfe819f4, 0x8048a1c, 0x8048a70 <unfinished ...>
__strtol_internal("2", NULL, 10)                                                              = 2
memset(0xbfde1960, '\000', 655360)                                                            = 

0xbfde1960
memset(0xbfde1960, 'a', 655359)                                                               = 

0xbfde1960
malloc(1)                                                                                     = 

0x9be6008
strcpy(0x9be6008, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"... <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

1バイトのmallocに対して長大な文字列のstrcpyを行ってSEGVします。確実にクラッシュさせるために、shellcode2を長めにしています。

(3) プロセスがabort

bool func3(std::size_t cbSize) {
  if (cbSize < 1024) {
    char* buf = new char[cbSize - 1];
    std::memset(buf, 0, cbSize - 1);

    // do stuff

    delete[] buf;
    return true;
  } else {
    return false;
  }
}

これは

    func3(0); // DoS only

のように呼ぶと、

$ ltrace --demangle ./a.out 3
__libc_start_main(0x8048958, 2, 0xbff0f8e4, 0x8048a34, 0x8048a88 <unfinished ...>
__strtol_internal("3", NULL, 10)                                                              = 3
operator new[](unsigned int)(-1, 0, 10, 0, 0x485ff4 <unfinished ...>
__gxx_personality_v0(1, 1, 0x432b2b00, 0x474e5543, 0xb7e004a0)                                = 8
__gxx_personality_v0(1, 1, 0x432b2b00, 0x474e5543, 0xb7e004a0)                                = 8
__gxx_personality_v0(1, 1, 0x432b2b00, 0x474e5543, 0xb7e004a0)                                = 8
__gxx_personality_v0(1, 1, 0x432b2b00, 0x474e5543, 0xb7e004a0)                                = 8
__gxx_personality_v0(1, 1, 0x432b2b00, 0x474e5543, 0xb7e004a0)                                = 8
terminate called after throwing an instance of 'std::bad_alloc'
__gxx_personality_v0(1, 1, 0x432b2b00, 0x474e5543, 0xb7e004a0)                                = 8
__gxx_personality_v0(1, 1, 0x432b2b00, 0x474e5543, 0xb7e004a0)                                = 6
__gxx_personality_v0(1, 2, 0x432b2b00, 0x474e5543, 0xb7e004a0)                                = 8
__gxx_personality_v0(1, 6, 0x432b2b00, 0x474e5543, 0xb7e004a0)                                = 7
  what():  St9bad_alloc
--- SIGABRT (Aborted) ---
+++ killed by SIGABRT +++

new char[-1] が

 operator new(static_cast<std::size_t>(sizeof(char) * (-1) + x))

と解釈されて(xは未規定*1の負でない整数)、たとえばLinux/x86では具体的には

 operator new(UINT_MAX)

と解釈されて、メモリ確保失敗でstd::bad_allocが飛びます。詳細は、C++規格(ISO/IEC 14882:2003)の§5.3.4 new式の、特に5.3.4/10, 5.3.4/12 を参照のこと。

(4) BO

bool func4(const char* s1, std::size_t len1,
           const char* s2, std::size_t len2) {
  if (1 + len1 + len2 > 64)
    return false;

  char* buf = (char*)std::malloc(len1 + len2 + 1);
  if (buf) {
    std::strncpy(buf, s1, len1 + len2);
  }

  // do other stuff with buf

  if (buf) std::free(buf);

  return true;
}

これは

    func4(shellcode, UINT_MAX, "", 0);

のように呼ぶと、3行目のif文の不等号の左辺が0になり、if文全体は偽と評価され、

$ ltrace ./a.out 4
__libc_start_main(0x8048a7a, 2, 0xbfe37694, 0x8048bdc, 0x8048c30 <unfinished ...>
atoi(0xbff1bc65, 0, 0, 0, 0)                                                                  = 4
malloc(0)                                                                                     = 

0x8cb9008
strncpy(0x8cb9008, "shellcode", 4294967295 <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

0バイトのmallocに対して*2、UINT_MAXバイトのstrncpyを行ってSEGVします。

(5) BO, 上と似ていますが引数がsigned int

bool func5(const char* s1, int len1,
           const char* s2, int len2) {

  char buf[128];

  if (1 + len1 + len2 > 128)
    return false;

  if (buf) {
    std::strncpy(buf, s1, len1);
    std::strncat(buf, s2, len2);
  }

  return true;
}

これは、

    func5(shellcode, 5000, "", -5000);

のように呼ぶと、やはり6行目のif文は偽となり、

$ ltrace ./a.out 5
__libc_start_main(0x8048a7a, 2, 0xbfeef124, 0x8048bd8, 0x8048c2c <unfinished ...>
atoi(0xbff77c65, 0, 0, 0, 0)                                                                  = 5
strncpy(0xbfe4efc0, "shellcode", 5000)                                                        = 

0xbfe4efc0
strncat(0xbfe4efc0, 0, 0, 0, 0)                                                               = 

0xbfe4efc0
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

if文でのチェックをすり抜けて128バイトのスタック上の配列に5000バイトを書いてSEGVします。そういえば、この問題のMSDNの解説は間違っているような。

(6) BO, MSのGDI+で妙なJPEG読みこみで任意コード実行されてしまう件の実例

void func6_getComment(unsigned int len, const char* src) {
  // real world example - MS GDI+ vlun
  unsigned int size;
  size = len - 2;
  char* comment = (char*)std::malloc(size + 1);
  std::memcpy(comment, src, size);
  return;
}

これは、

    func6_getComment(1, shellcode);

のように呼ぶと、4行目でsize変数の値がUINT_MAXになり(size変数の型が符号なしintなのがポイント)、

$ ltrace ./a.out 6
__libc_start_main(0x8048a7a, 2, 0xbff070c4, 0x8048bec, 0x8048c40 <unfinished ...>
atoi(0xbff83c65, 0, 0, 0, 0)                                                                  = 6
malloc(0)                                                                                     = 

0x9642008
memcpy(0x9642008, "shellcode", 4294967295 <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

結果、0バイトmallocした領域にUINT_MAXバイトmemcpyしてSEGVしています。なお、size変数がsigned int型だったとしても、memcpyの第三引数はsize_t型(符号なし)ですから結果は一緒です。

(7) BO

#define BUFF_SIZE 10
void func7(int argc, char** argv){
  int len;
  char buf[BUFF_SIZE];
  len = std::atoi(argv[1]);
  if (len < BUFF_SIZE){
    std::memcpy(buf, argv[2], len);
  }
  else
    std::printf("Too much data\n");
}

これは、

      char* v[] = {"", "-1", "shellcode", 0};
      func7(3, v);

のように呼ぶと、5行目でlenが-1となり、直後のif文でのチェックをすりぬけて

$ ltrace ./a.out 7
__libc_start_main(0x8048ae6, 2, 0xbffdd214, 0x8048cc8, 0x8048d1c <unfinished ...>
atoi(0xbffeec65, 0, 0, 0, 0)                                                                  = 7
atoi(0x8048e1f, 0x38b99f, 0xbffeec65, 0, 10)                                                  = -1
memcpy(0xbff3d110, "", 4294967295 <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

SEGVします。memcpyの第三引数はsize_t型(符号なし)ですから、-1を渡すとUINT_MAXを渡したのと同様に振舞います。

(8) ほぼ任意の場所の4バイトを書き換え可能

int* table = NULL;
int func8(int pos, int value){
  if (!table) {
    table = (int *)std::malloc(sizeof(int) * 100);
  }
  if (pos > 99) {
    return -1;
  }
  table[pos] = value;
  return 0;
}

これは、

    func8(-3, 0x80123456); 

のように呼ぶと、6行目のif文のチェックをすり抜けた後、9行目の配列アクセスで配列の範囲外を読み書きしてしまい、

$ ltrace ./a.out 8
__libc_start_main(0x8048ae6, 2, 0xbff87974, 0x8048cc8, 0x8048d1c <unfinished ...>
atoi(0xbffc3c65, 0, 0, 0, 0)                                                                  = 8
malloc(400)                                                                                   = 

0x895e008
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

SEGVします。C99規格の§6.5.2.1(配列の添字演算子)や§6.5.6(加減演算子)の脚注88を見ると、int型の配列tableに対して、table[pos] は

 *((int*)((char*)(table)) + ((pos) * sizeof(int)))

と等価ですので、負数を与えると、配列tableの終わりよりもっと後ろの方にもアクセスできます。つまり、メモリ上のほぼお好きな4バイトをお好きな値に書き換えることができます。応用方法は自由自在です。


ProPoliceなどのcanary系プロテクションでは検出できないメモリの改ざんができるので、厄介ですね。この件は面白いので、別エントリでも扱います

(9) BO, NetBSDで似た事例があった

int func9(const char* str, int buf_len) {
  if (!str) return 1;
  std::size_t str_len = std::strlen(str);

  if (str_len > buf_len - sizeof(char)) {
    // buffer too small
    return 1;
  }

  char* buf = (char*)std::malloc(buf_len);
  strcpy(buf, str);
  return 0;
}

これは、

      char shellcode3[1000000] = {0};
      std::memset(shellcode3, 'a', sizeof(shellcode3) - 1);
      func9(shellcode3, 0);

のように呼ぶと、

$ ltrace ./a.out 9
__libc_start_main(0x8048abc, 2, 0xbff37434, 0x8048cd8, 0x8048d2c <unfinished ...>
atoi(0xbff9bc65, 0, 0, 0, 0)                                                                  = 9
memset(0xbfda3150, '\000', 1000000)                                                           = 

0xbfda3150
memset(0xbfda3150, 'a', 999999)                                                               = 

0xbfda3150
strlen("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"...)                                                 = 

999999
malloc(0)                                                                                     = 

0x88f1008
strcpy(0x88f1008, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"... <unfinished ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++

0バイトmallocした領域に長大な文字列をstrcpyしてSEGVします。ちょっとややこしいですが、

  if (str_len > buf_len - sizeof(char)) {

という判定方法に問題があります。buf_lenが0にもかかわらず、if文が真になりません。不等号の右辺は、次の理由によりunsigned int型の最大値であるUINT_MAXになります。

  • sizeofの戻りはsize_t型(これは、私の環境ではunsigned int)
  • intよりもunsigned intのほうが、integer conversion rank *3が高い。だから、int と unsigned int の減算の結果は、usual arithmetic conversion 規則*4によって unsigned int になる。

右辺が-1になると思ってしまいがちですが、そうはならないわけです。

クイズの回答例は以上です。

続く

*1:unspecified: 処理系が好きに決めるが、どう決めたかをドキュメント化しなくてよい項目

*2:テスト環境では、このmallocは成功してNULLではないポインタを戻した。どのように振舞うかはPOSIXによれば処理系定義動作

*3:JISの呼び名は「整数変換の順位」

*4:JISの呼び名は「通常の算術型変換」