setjmp, longjmpで使われるjmp_bufには何がどのように保存されているのか
setjmp, longjmpはC言語で実行コンテキストを保存し、保存したコンテキストに復帰するために使われる関数(orマクロ)である。g++の例外実装の一つはこのsetjmp, longjmpを用いて実現されている。
この記事では 実行コンテキスト とは何を指すのか? また、その保存方法の実装について調べる。
jmp_buf
setjmpの実装はglibcの setjmp/setjmp.h にある。
typedef struct __jmp_buf_tag jmp_buf[1]; /* Store the calling environment in ENV, also saving the signal mask. Return 0. */ extern int setjmp (jmp_buf __env) __THROWNL;
このjmp_buf
はsetjmp/bits/types/struct___jmp_buf_tag.h で定義されている。
/* Calling environment, plus possibly a saved signal mask. */ struct __jmp_buf_tag { /* NOTE: The machine-dependent definitions of `__sigsetjmp' assume that a `jmp_buf' begins with a `__jmp_buf' and that `__mask_was_saved' follows it. Do not move these members or add others before it. */ __jmp_buf __jmpbuf; /* Calling environment. */ int __mask_was_saved; /* Saved the signal mask? */ __sigset_t __saved_mask; /* Saved signal mask. */ };
x86-64の場合、__jmp_buf
はsysdeps/x86/bits/setjmp.hで定義されている。__jmp_buf
は8個のint型整数からなり、更に64bitかどうかで場合分けがなされていることがわかる。
# if __WORDSIZE == 64 typedef long int __jmp_buf[8]; # elif defined __x86_64__ __extension__ typedef long long int __jmp_buf[8]; # else typedef int __jmp_buf[6]; # endif
setjmp
setjmp自体の実装はsysdeps/x86_64/setjmp.Sにある。Linux x86-64では第一引数を%rdi
で渡すので、第一引数として渡された構造体にレジスタの値を保存していることがわかる。保存されているのは、%rbx
, %r12
, %r13
, %r14
, %r15
, %rsp
, PCである。しかし、%rsp
, %rbp
, PCはPTR_MANGLEで処理されてから保存されている。PTR_MANGLEとは何だろうか? また、なぜ必要なのだろう?
ENTRY (__sigsetjmp) /* Save registers. */ movq %rbx, (JB_RBX*8)(%rdi) #ifdef PTR_MANGLE # ifdef __ILP32__ /* Save the high bits of %rbp first, since PTR_MANGLE will only handle the low bits but we cannot presume %rbp is being used as a pointer and truncate it. Here we write all of %rbp, but the low bits will be overwritten below. */ movq %rbp, (JB_RBP*8)(%rdi) # endif mov %RBP_LP, %RAX_LP PTR_MANGLE (%RAX_LP) mov %RAX_LP, (JB_RBP*8)(%rdi) #else movq %rbp, (JB_RBP*8)(%rdi) #endif movq %r12, (JB_R12*8)(%rdi) movq %r13, (JB_R13*8)(%rdi) movq %r14, (JB_R14*8)(%rdi) movq %r15, (JB_R15*8)(%rdi) lea 8(%rsp), %RDX_LP /* Save SP as it will be after we return. */ #ifdef PTR_MANGLE PTR_MANGLE (%RDX_LP) #endif movq %rdx, (JB_RSP*8)(%rdi) mov (%rsp), %RAX_LP /* Save PC we are returning to now. */ LIBC_PROBE (setjmp, 3, LP_SIZE@%RDI_LP, -4@%esi, LP_SIZE@%RAX_LP) #ifdef PTR_MANGLE PTR_MANGLE (%RAX_LP) #endif movq %rax, (JB_PC*8)(%rdi) #ifdef SHADOW_STACK_POINTER_OFFSET # if IS_IN (libc) && defined SHARED && defined FEATURE_1_OFFSET /* Check if Shadow Stack is enabled. */ testl $X86_FEATURE_1_SHSTK, %fs:FEATURE_1_OFFSET jz L(skip_ssp) # else xorl %eax, %eax # endif /* Get the current Shadow-Stack-Pointer and save it. */ rdsspq %rax movq %rax, SHADOW_STACK_POINTER_OFFSET(%rdi) # if IS_IN (libc) && defined SHARED && defined FEATURE_1_OFFSET L(skip_ssp): # endif #endif #if IS_IN (rtld) /* In ld.so we never save the signal mask. */ xorl %eax, %eax retq #else /* Make a tail call to __sigjmp_save; it takes the same args. */ jmp __sigjmp_save #endif END (__sigsetjmp)
PTR_MANGLEは sysdeps/unix/sysv/linux/x86_64/sysdep.h に定義がある。
# define PTR_MANGLE(reg) xor %fs:POINTER_GUARD, reg; \ rol $2*LP_SIZE+1, reg # define PTR_DEMANGLE(reg) ror $2*LP_SIZE+1, reg; \ xor %fs:POINTER_GUARD, reg
Linux x86-64では%fs
レジスタがThread Local Storageのベースポインタとして使われることを思い出すと、このコードは
- スレッド固有の値(
%fs:POINTER_GUARD
)とのxorを取り $2*LP_SIZE+1
だけレジスタのビットを回転させる
ことがわかる。PTR_MAGNLEの詳細は Pointer Encryptionにある。
なぜsetjmp, longjmpに際してポインタを暗号化するのだろうか? 最近のglibcではatexit関数やjmp_bufを狙った攻撃は効かない (PTR_MANGLE)やPointer Subterfugeによれば、書き込み可能な領域に生のEIPを保存すると攻撃対象になるからだそうだ。
結論
- setjmpは
%rbx
,%r12
,%r13
,%r14
,%r15
,%rsp
, PCをjmp_bufに保存する - このとき、生のEIPを保存するとそのデータ自体が攻撃目標になるため、PTR_MANGLEで暗号化する