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 +++
(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になると思ってしまいがちですが、そうはならないわけです。
クイズの回答例は以上です。
→ 続く