printf関数でヒープ領域が確保される理由を調べてみた
先日pwnの勉強がてらtcache poisoningについて調べていたところ、以下のページを見つけたので、サイトに載っていたサンプルプログラムをコンパイルしてGDBでヒープ領域を確認してみた。
http://tukan.farm/2017/07/08/tcache/
ヒープ領域にはtcache_entryやtcache_perthread_struct、mallocで確保されたchunkが格納されていると思っていたが、他にprintf関数で表示する文字列もこのヒープ領域に含まれていることに気がついた。(ちなみに自分の環境だと上手くtcache poisoning出来ていなかった。対策されたのであろうか。)
気になったので簡単なプログラムhello.cで試したところ、malloc関数を呼び出していないにも関わらず、printf関数(コンパイル時にputs関数に変換)の呼び出し前後によってヒープ領域がセットされてそこに文字列が格納されていることがわかった。
// hello.c
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
↑ puts関数呼び出し前のメモリマップ。heap領域は割り当てられていない。
↑ puts関数実行後のメモリマップではheap領域が確認できる。
↑ heap領域にはputs関数で出力した文字列が格納されていた。
色々気になったので調べてみることにした。ちなみに自分のglibcのバージョンは以下の通り。
ここ( https://ftp.gnu.org/gnu/glibc/ )からglibc 2.31のソースコードを手に入れ、gdbで先程のhello.cの動作を追う。
puts@plt は共有ライブラリへと遅延リンクを行う際に使われるpltテーブルである。リロケーション部分を省略してglibcのputs関数本体へと入る。
関数本体は__GI__IO_putsという名前らしい。ソースコードはglibc-2.31/libio/ioputs.cにあった。ここから、いつヒープ領域が割り当てられるかを見つけるための長大な捜索が始まる。
startとbreakを幾度となく繰り返しながら確かめていくと、__GI__IO_putsの先頭から212バイトの call *0x38(%r14) の前後でヒープが割り当てられていた。
siによってcallされる関数の内部に入ると、関数名が_IO_new_file_xsputnで libio/fileops.cに存在することがわかる。
コメントから、ファイルバッファに関する処理を行っているようだがソースコードを読むだけではよく理解できないので、引き続きgdbで動作を追う。
同じように試行錯誤を繰り返すと、先頭から222バイト先にあるcall *0x18(%r13) の前後でヒープ割り当て(文字列はまだ格納されていない)が確認できていた。
呼び出された関数は同じくfileops.cにある_IO_new_file_overflow関数であった。ファイルのバッファの確認と割り当てを行っていることがソースのコメントからわかる。関数内部では__GI__IO_doallocbufというgenops.cにある関数を呼び出していた。
この__GI__IO_doallocbufについてさらに探索を行うと、__GI__IO_file_doallocateという如何にもな名前の関数にたどり着く。
/* Allocate a file buffer, or switch to unbuffered I/O. Streams for
TTY devices default to line buffered. */
int
_IO_file_doallocate (FILE *fp)
{
size_t size;
char *p;
struct stat64 st;
size = BUFSIZ;
if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
{
if (S_ISCHR (st.st_mode))
{
/* Possibly a tty. */
if (
#ifdef DEV_TTY_P
DEV_TTY_P (&st) ||
#endif
local_isatty (fp->_fileno))
fp->_flags |= _IO_LINE_BUF;
}
#if defined _STATBUF_ST_BLKSIZE
if (st.st_blksize > 0 && st.st_blksize < BUFSIZ)
size = st.st_blksize;
#endif
}
p = malloc (size);
if (__glibc_unlikely (p == NULL))
return EOF;
_IO_setb (fp, p, p + size, 1);
return 1;
}
libc_hidden_def (_IO_file_doallocate)
filedoalloc.cにあるソースコードを読んでみると、ここでmalloc(size) が呼ばれていることがわかった。自分の環境ではsizeは1024バイトだった。
以上の探索によって、puts関数を呼び出すとファイルバッファの割り当てのためにmallocが呼び出されることがわかった。
ところでバッファのためにヒープが使われるなら、バッファを利用しない設定にすればmallocも呼び出されないのだろうか。
printfの前にsetbuf(stdout, NULL)でバッファリングを無効化するhello2.cを使って確認してみる。
// hello2.c
#include <stdio.h>
int main() {
setbuf(stdout, NULL);
printf("Hello, world!\n");
return 0;
}
先程と同じようにgdbでprintf前後のメモリマッピングを見てみる。
printf関数が呼ばれた後もヒープ領域が割り当てられていないことがわかる。バッファリングを無効化するとmallocも呼ばれないことがわかった。
【感想】
とりあえずprintfの中でmallocが行われる理由はわかりましたが、途中で呼ばれていた様々な関数の処理については正直ほとんど理解できていないです。
glibcのソースコードは手元にあるので、これからも勉強したいです。
追記
printf関数において確保される領域は、stdoutの_IO_FILE構造体のなかでbufferとしてヒープ領域に確保されたもの。
本環境(glibc 2.31)では_IO_FILE構造体はlibio/bits/types/struct_FILE.hにおいて定義されており、以下のようになっている。
/* The tag name of this struct is _IO_FILE to preserve historic
C++ mangled names for functions taking FILE* arguments.
That name should not be used in new code. */
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
この中で、_IO_read_ptrや_IO_read_endは確保されたヒープ領域を指している。ちなみにstdoutやstdin, stderrの_IO_FILE構造体はglibc内に存在している。
参考ページ
・https://www.slideshare.net/AngelBoy1/play-with-file-structure-yet-another-binary-exploit-technique
・https://ptr-yudai.hatenablog.com/entry/2019/05/31/235444
・https://dhavalkapil.com/blogs/FILE-Structure-Exploitation/