はじめに
Stack overflowについて説明します。
gdbを使って理解します。
Stackとは
stackにはレジスタ、引数、ローカル変数、戻り値、リターンアドレスがおかれます。
例で説明します。
int main(int argc, char* argv[])
{
return 0;
}
$ gcc -O0 test.c
$ gdb a.out
(gdb) disas main
Dump of assembler code for function main:
0x00000000004004ed <+0>: push %rbp
0x00000000004004ee <+1>: mov %rsp,%rbp
0x00000000004004f1 <+4>: mov %edi,-0x4(%rbp)
0x00000000004004f4 <+7>: mov %rsi,-0x10(%rbp)
0x00000000004004f8 <+11>: mov $0x0,%eax
0x00000000004004fd <+16>: pop %rbp
0x00000000004004fe <+17>: retq
End of assembler dump.
push命令を実行するとstackに%rbpがおかれてrspがデクリメントされます。
push %rbp
次の命令はrsp=rbpをベースとして-0x4, -0x10にediとrsiを書き込みます。
rspは変更しませんがstackにデータを書き込みます。
ediとrsiはmain関数の引数argcとargvです。
mov %edi,-0x4(%rbp)
mov %rsi,-0x10(%rbp)
pop命令を実行するとstackのデータをrbpに読み出して、rspをインクリメントします。
pop %rbp
ローカル変数はstackにおかれます。次の例で説明します。
int main(int argc, char* argv[])
{
int v[10];
return 0;
}
$ gcc -O0 test.c
$ gdb a.out
(gdb) disas main
Dump of assembler code for function main:
0x00000000004004ed <+0>: push %rbp
0x00000000004004ee <+1>: mov %rsp,%rbp
0x00000000004004f1 <+4>: mov %edi,-0x34(%rbp)
0x00000000004004f4 <+7>: mov %rsi,-0x40(%rbp)
0x00000000004004f8 <+11>: mov $0x0,%eax
0x00000000004004fd <+16>: pop %rbp
0x00000000004004fe <+17>: retq
End of assembler dump.
ediを-0x34 = -52 しています。
mov %edi,-0x34(%rbp)
rbpのサイズは8です。ediのサイズは4です。 v[10]は40です。合計で52となります。
-8 ~ -(48-1)の範囲をv[10]として使用します。
このようにrspを操作しないでstackを暗黙的に確保することがあります。
stack address / size の 取得 / 設定
stack address / size は pthread_attr_getstackで取得します。
取得できるstack addressはアクセス可能な最小のアドレスです。
stack size は pthread_attr_setstackで設定します。最小サイズは16kです。
# include <stdio.h>
# include <limits.h>
# define _GNU_SOURCE
# include <pthread.h>
# define NAMELEN 16
void get_stack_info()
{
void *stackaddr;
size_t stacksize, guardsize;
char name[NAMELEN] = {0};
pthread_attr_t attr;
pthread_t self = pthread_self();
pthread_getname_np(self, name);
pthread_getattr_np(self, &attr);
pthread_attr_getstack(&attr, &stackaddr, &stacksize);
pthread_attr_getguardsize(&attr, &guardsize);
printf("name=%.4s range=[%p-%p] size=%zu guard size=%zu\n",
name, stackaddr, stackaddr+stacksize-1, stacksize, guardsize);
}
void *start_routine(void* arg)
{
get_stack_info();
return 0;
}
int main(int argc, char* argv[])
{
int ret;
pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
ret = pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN /* size_t stacksize */);
if (ret != 0) {
printf("error pthread_attr_setstacksize %d\n", ret);
return -1;
}
pthread_create(&thread, &attr, start_routine, 0 /* void * arg */);
pthread_join(thread, 0 /* void **thread_return */);
return 0;
}
$ gcc -g -O0 test.c -lpthread
$ ./a.out
name=a.ou range=[0x7f5597059000-0x7f559705cfff] size=16384 guard size=4096
stackは低位アドレスに成長します。
guardはread / write禁止領域です。アクセスするとSegmentation faultが発生します。
アクセス可能なアドレス範囲は[stackaddr + guard_size, stackaddr + stacksize - 1]です。
stack 使用量 チェック
gccの"-fstack-usage"オプションを利用することで静的にstack使用量を確認できます。
具体例で説明します。
int main(int argc, char* argv[])
{
char buf[512];
return 0;
}
$ gcc -fstack-usage test.c && cat test.su
test.c:1:5:main 560 static
560 byteのstackを使用していることがわかります。
bufにstaticをつけてみます。
int main(int argc, char* argv[])
{
static char buf[512];
return 0;
}
16 byteに減っています。
$ gcc -fstack-usage test.c && cat test.su
test.c:1:5:main 16 static
stack overflowが起こるケース
次のケースでstack overflowが起きる可能性があります。
- 深いcall stack , 再帰呼び出し
- サイズの大きなlocal変数
- サイズの大きな値渡し