前回(クイズ)の続きです。自分で解いてみた結果を貼っておきます。(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になると思ってしまいがちですが、そうはならないわけです。
クイズの回答例は以上です。
→ 続く