Daily AlpacaHack 2026/2/17 の author’s write-up です。
まずはすみません、出題不備がありました(意図していない入力でフラグが出てしまう)。それについては後半で言及します。 別解で解いた人もいるかもしれないですが、意図した解法についても考えてくださると author は喜ぶと思います。
下記の write-up の大半は出題前時点で書いていたものになります。
背景
浮動小数点演算は環境依存(で厄介)だと言われがちですが、環境変数によっても値が変わりますという話題です。 「環境依存」という言い回しがなされるときは詳細な要因が明確にされないことが多く、「よくわからないけどなんかそういう魔法で差異があるんだろう」のような扱われ方をしがちな気がします。 もちろん、(たとえばオープンソースでない部分の影響などで)調べられる範囲に答えがないこともしばしばありますが、深掘りしてみると面白い発見が得られることがしばしばあるかなと思います。
解答としては下記の記事で以前紹介していたものがそのまま使えるので、これを元々知っていた人にとってはすぐ済んでしまったかもしれませんが、そういう人も「元々知らなかった場合はどうやって調べたかな?」ということを考えてみるとよいかもしれません。
Daily AlpacaHack で出題される内容は基礎的・教育的なものが多いため、えびちゃん的には「元々知ってたのでその通りにしました」となることもよくあるのですが、「知らなかった場合にどうするか」「知らない人はどう調べるだろうか」ということを考えるのも面白いと思っています。
解法例
前準備
sin(x) の値が変わる必要があるというところで、まずは sin(x) の呼び出し時に何が行われているのかを調べていこうと思います。
ここでは gdb を使います。
試行錯誤をしながら挙動を確かめる場合、ASLR が有効だと実行のたびにアドレスが変わってしまってややこしくなる(メモリの状態などを書き残しておいたときに、「今のここのアドレスがさっきでいうここのアドレスで...」のような解釈を都度する必要が出てきたりする)ので、無効化しておくと楽になりがちです。
# sysctl kernel.randomize_va_space=0 kernel.randomize_va_space = 0
note: docker run を --privileged つきで実行してコンテナに root で入っているので、$ ではなく # のプロンプトになっています。コメントみたいな色になってしまい不服。
ASLR が有効な場合でのみ再現する事象などもありうるので、注意は必要です。再度有効化する場合は次のようにします。
# sysctl kernel.randomize_va_space=2 kernel.randomize_va_space = 2
余談ですが、ASLR でどの程度アドレスが散らばるかは次のようにして調整できます。
# sysctl vm.mmap_rnd_bits=28 vm.mmap_rnd_bits = 28
vm.mmap_rnd_bits に関しては元々 32 の環境もあると思います(私の環境ではそうでした)が、AlpacaHack のサーバ上では 28 っぽい?気がする*1ので、それに合わせておくと都合がいい場合がしばしばあると思います。
ステップ実行
まずは main に breakpoint を打って命令を追っていきます。
(gdb) b main Breakpoint 1 at 0x1171 (gdb) r Starting program: /mnt/chall ... Breakpoint 1, 0x0000555555555171 in main () (gdb) x/10i $rip => 0x555555555171 <main+8>: sub $0x10,%rsp 0x555555555175 <main+12>: movsd 0xebb(%rip),%xmm0 # 0x555555556038 0x55555555517d <main+20>: movsd %xmm0,-0x10(%rbp) 0x555555555182 <main+25>: mov -0x10(%rbp),%rax 0x555555555186 <main+29>: movq %rax,%xmm0 0x55555555518b <main+34>: call 0x555555555070 <sin@plt> 0x555555555190 <main+39>: movq %xmm0,%rax 0x555555555195 <main+44>: mov %rax,-0x8(%rbp) 0x555555555199 <main+48>: movsd -0x8(%rbp),%xmm0 0x55555555519e <main+53>: ucomisd 0xe9a(%rip),%xmm0 # 0x555555556040
<main+12> で movsd している値を確認してみましょう。
(gdb) x/a 0x555555556038 0x555555556038: 0x3fd0407d49e8acfc (gdb) x/f 0x555555556038 0x555555556038: 0.25393612115540498
丸めの桁数の違いはありますが、chall.c 内の x と一致していることがわかります。正確な値は次のようになります*2。
$$ \begin{aligned} &\phantom{{}={}} \texttt{1}.\texttt{0407D49E8ACFC}_{(16)}\times 2^{-2} \\ &= {\small 0.253936121155404}{\footnotesize 981308834067021}{\scriptsize 962255239486694}{\tiny 3359375}. \end{aligned} $$
さて、<main+34> で <sin@plt> というのを呼んでいるようなので、ここを追ってみましょう。
(gdb) b 'sin@plt' Breakpoint 2 at 0x555555555070 (gdb) c Continuing. Breakpoint 2, 0x0000555555555070 in sin@plt () (gdb) x/3i $rip => 0x555555555070 <sin@plt>: endbr64 0x555555555074 <sin@plt+4>: jmp *0x2f56(%rip) # 0x555555557fd0 <sin@got.plt> 0x55555555507a <sin@plt+10>: nopw 0x0(%rax,%rax,1)
<sin@got.plt> に置かれているアドレスに <sin@plt+4> からジャンプしているので、ジャンプ先を見てみましょう。
(gdb) x/10i 'sin@got.plt' 0x7ffff7f4c2d0 <__sin_fma>: endbr64 0x7ffff7f4c2d4 <__sin_fma+4>: push %rbp 0x7ffff7f4c2d5 <__sin_fma+5>: mov %rsp,%rbp 0x7ffff7f4c2d8 <__sin_fma+8>: push %rbx 0x7ffff7f4c2d9 <__sin_fma+9>: sub $0x38,%rsp 0x7ffff7f4c2dd <__sin_fma+13>: mov %fs:0x28,%rax 0x7ffff7f4c2e6 <__sin_fma+22>: mov %rax,-0x18(%rbp) 0x7ffff7f4c2ea <__sin_fma+26>: xor %eax,%eax 0x7ffff7f4c2ec <__sin_fma+28>: vstmxcsr -0x28(%rbp) 0x7ffff7f4c2f1 <__sin_fma+33>: mov -0x28(%rbp),%edx
どうやら <__sin_fma> という名前をした関数のようです*3。x/10i 'sin@got.plt' を実行した後に enter を押し続けていると続きの命令も見れるので、しばらく眺めてみましょう。
libc のバージョンによってアドレスや順序に違いがあったりする可能性はありますが、GNU C Library (Ubuntu GLIBC 2.39-0ubuntu8.1) では次のようになりました。関数の冒頭のみを抜粋したものです。
0x7ffff7f4c2d0 <__sin_fma>: endbr64 0x7ffff7f4cad0 <__cos_fma>: endbr64 0x7ffff7f4d2c0 <__sincos_fma>: endbr64 0x7ffff7f4dac0 <__tan_fma>: endbr64 0x7ffff7f4e300 <__cosf_sse2>: endbr64 0x7ffff7f4e5b0 <__sincosf_sse2>: endbr64 0x7ffff7f4e8a0 <__sinf_sse2>: endbr64 0x7ffff7f4eb40 <__exp2f_fma>: endbr64 0x7ffff7f4ec40 <__expf_fma>: endbr64 0x7ffff7f4ed50 <__log2f_fma>: endbr64 0x7ffff7f4ee50 <__logf_fma>: endbr64 0x7ffff7f4ef50 <__powf_fma>: endbr64 0x7ffff7f4f330 <__cosf_fma>: endbr64 0x7ffff7f4f570 <__sincosf_fma>: endbr64 0x7ffff7f4f800 <__sinf_fma>: endbr64 0x7ffff7f4fa30 <__ieee754_exp_fma4>: endbr64 0x7ffff7f4fc30 <__ieee754_log_fma4>: endbr64 0x7ffff7f4fe80 <__ieee754_pow_fma4>: endbr64 0x7ffff7f50510 <__ieee754_asin_fma4>: endbr64 0x7ffff7f50c50 <__ieee754_acos_fma4>: endbr64 0x7ffff7f513f0 <__atan_fma4>: endbr64 0x7ffff7f517d0 <__ieee754_atan2_fma4>: endbr64 0x7ffff7f52200 <__sin_fma4>: endbr64 0x7ffff7f52a50 <__cos_fma4>: endbr64 0x7ffff7f53280 <__sincos_fma4>: endbr64 0x7ffff7f53a80 <__tan_fma4>: endbr64 0x7ffff7f542c0 <__ieee754_exp_avx>: endbr64 0x7ffff7f544f0 <__ieee754_log_avx>: endbr64 0x7ffff7f547b0 <__atan_avx>: endbr64 0x7ffff7f54ce0 <__ieee754_atan2_avx>: endbr64 0x7ffff7f558c0 <__sin_avx>: endbr64 0x7ffff7f561d0 <__cos_avx>: endbr64 0x7ffff7f56ad0 <__sincos_avx>: endbr64 0x7ffff7f57440 <__tan_avx>: endbr64
各種数学関数に _fma _sse2 _fma4 avx の suffix がついているのが見て取れます。特に sin に関しては __sin_fma __sin_fma4 __sin_avx というものが含まれています。これらの suffix は CPU の命令セットの名前のように見えます。
他にもあるのかな?というところで、gdb の入力補完を利用して探してみます。^I と書いてある場所で tab を押しています。
(gdb) p __sin_^I __sin_avx __sin_fma __sin_fma4 __sin_ifunc __sin_sse2
__sin_sse2 というものもあるようです。先ほどの __sin_fma よりも下位アドレスにあるため先ほどは列挙されていなかったようですね。
(gdb) x/i &__sin_sse2 0x7ffff7f008e0 <__sin_sse2>: endbr64
__sin_ifunc というのもあり、これは CPU 命令セット名とは違いそうなので、少し調べてみます。
(gdb) x/20i __sin_ifunc
0x7ffff7f01ae0 <__sin_ifunc>: endbr64
0x7ffff7f01ae4 <__sin_ifunc+4>: mov 0xb74e5(%rip),%rdx # 0x7ffff7fb8fd0
0x7ffff7f01aeb <__sin_ifunc+11>: mov 0x9c(%rdx),%ecx
0x7ffff7f01af1 <__sin_ifunc+17>: test $0x10,%ch
0x7ffff7f01af4 <__sin_ifunc+20>: je 0x7ffff7f01b06 <__sin_ifunc+38>
0x7ffff7f01af6 <__sin_ifunc+22>:
lea 0x4a7d3(%rip),%rax # 0x7ffff7f4c2d0 <__sin_fma>
0x7ffff7f01afd <__sin_ifunc+29>: testb $0x20,0xb8(%rdx)
0x7ffff7f01b04 <__sin_ifunc+36>: jne 0x7ffff7f01b2e <__sin_ifunc+78>
0x7ffff7f01b06 <__sin_ifunc+38>:
lea 0x506f3(%rip),%rax # 0x7ffff7f52200 <__sin_fma4>
0x7ffff7f01b0d <__sin_ifunc+45>: testb $0x1,0xde(%rdx)
0x7ffff7f01b14 <__sin_ifunc+52>: jne 0x7ffff7f01b2e <__sin_ifunc+78>
0x7ffff7f01b16 <__sin_ifunc+54>: and $0x10000000,%ecx
0x7ffff7f01b1c <__sin_ifunc+60>:
lea 0x53d9d(%rip),%rax # 0x7ffff7f558c0 <__sin_avx>
0x7ffff7f01b23 <__sin_ifunc+67>:
lea -0x124a(%rip),%rdx # 0x7ffff7f008e0 <__sin_sse2>
0x7ffff7f01b2a <__sin_ifunc+74>: cmove %rdx,%rax
0x7ffff7f01b2e <__sin_ifunc+78>: ret
0x7ffff7f01b2f: nop
0x7ffff7f01b30 <__cos_ifunc>: endbr64
0x7ffff7f01b34 <__cos_ifunc+4>: mov 0xb7495(%rip),%rdx # 0x7ffff7fb8fd0
0x7ffff7f01b3b <__cos_ifunc+11>: mov 0x9c(%rdx),%ecx
C っぽく書くと次のような処理に相当しています。
typedef unsigned u32; typedef unsigned long u64; typedef void* p64; p64 __sin_ifunc() { p64 rdx = (p64)(*(u64*)0x7ffff7fb8fd0); u32 ecx = *(u32*)(rdx + 0x9c); p64 rax; int zf = 0; zf = ((ecx >> 8) & 0x10) == 0; if (zf) goto L38; rax = __sin_fma; zf = (*(u32*)(rdx + 0xb8) & 0x20) == 0; if (!zf) goto L78; L38: rax = __sin_fma4; zf = (*(u32*)(rdx + 0xde) & 0x1) == 0; if (!zf) goto L78; ecx &= 0x10000000; zf = (ecx == 0); rax = __sin_avx; rdx = __sin_sse2; rax = zf ? rdx : rax; L78: return rax; }
整理すると次のような処理になっています。
p64 __sin_ifunc() { p64 rdx = (p64)(*(u64*)0x7ffff7fb8fd0); u32 ecx = *(u32*)(rdx + 0x9c); if ((ecx & 0x1000) != 0 && (*(u32*)(rdx + 0xb8) & 0x20) != 0) { return __sin_fma; } else if ((*(u32*)(rdx + 0xde) & 0x1) != 0) { return __sin_fma4; } else { return ((ecx & 0x10000000) != 0) ? __sin_avx : __sin_sse2; } }
(rdx + 0x9c), (rdx + 0xb8), (rdx + 0xde) に何らかの重要なフラグが入っていて、その値に応じて __sin_fma, __sin_fma4, __sin_sse2, __sin_avx のいずれかを返すような処理をしているように見えます。実際、該当のフラグを確認すると __sin_fma に対応する条件に合致していました。
(gdb) p *(long*)(*(long*)0x7ffff7fb8fd0 + 0x9c) & 0x1000 $1 = 4096 (gdb) p *(long*)(*(long*)0x7ffff7fb8fd0 + 0xb8) & 0x20 $2 = 32 (gdb) p *(long*)(*(long*)0x7ffff7fb8fd0 + 0xde) & 0x1 $3 = 0 (gdb) p *(long*)(*(long*)0x7ffff7fb8fd0 + 0x9c) & 0x10000000 $4 = 268435456
ということで、ここのフラグを変えることができれば __sin_fma 以外を呼ぶことができそうかも?という気持ちになってきます。実際にはこの関数がどのように呼ばれ、どのように返り値が使われているかがまだわかっていないので、この段階ではただの期待です。
これらの関数において、該当の引数を与えたときに返り値が異なることは下記のように確かめられるので、たとえば __sin_avx を呼ぶようにできればよさそうということはわかります。
(gdb) p __sin_fma(0.25393612115540498) $5 = 0.25121578956912338 (gdb) p __sin_avx(0.25393612115540498) $6 = 0.25121578956912333
フラグ?
前項で突き止めたフラグを調べていきます。<sin@got.plt> の値がいつ変わるかを確認しながら処理を追いましょう。
(gdb) starti ... 0x00007ffff7fe4540 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) x/3i $rip => 0x7ffff7fe4540 <_start>: mov %rsp,%rdi 0x7ffff7fe4543 <_start+3>: call 0x7ffff7fe51d0 <_dl_start> 0x7ffff7fe4548 <_dl_start_user>: mov %rax,%r12 (gdb) x/a &'sin@got.plt' 0x555555557fd0 <sin@got.plt>: 0x1040 (gdb) ni 0x00007ffff7fe4543 in _start () from /lib64/ld-linux-x86-64.so.2 (gdb) x/a &'sin@got.plt' 0x555555557fd0 <sin@got.plt>: 0x1040 (gdb) ni [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, 0x0000555555555171 in main () (gdb) x/a &'sin@got.plt' 0x555555557fd0 <sin@got.plt>: 0x7ffff7f4c2d0 <__sin_fma>
_start の開始時点では 0x1040 で、_dl_start の処理内かつ main に到達する前に __sin_fma に変わっているようです。
しかし、b __sin_ifunc で breakpoint を打ってもうまく止まってくれないようです。アドレスを指定してみてもうまくいきません。
(gdb) b *0x7ffff7f01ae0 Breakpoint 3 at 0x7ffff7f01ae0: file ../sysdeps/x86_64/fpu/multiarch/s_sin.c, line 27. (gdb) r ... Warning: Cannot insert breakpoint 3. Cannot access memory at address 0x7ffff7f01ae0
_start の開始時点では libc が 0x7ffff7f01ae0 のアドレスにまだロードされていないため?ということで、ロードされてから breakpoint を作ってみましょう。
starti ni si ni 600 を実行しただけではまだ <sin@got.plt> は初期値のままで、そこから ni 100 をすると main に到達したので、その間のどこかでしょう。ということで ni を繰り返しながら探していきます。
=> 0x7ffff7fe5752 <_dl_start+1410>:
movaps %xmm3,0x186e7(%rip) # 0x7ffff7ffde40 <_rtld_global+3648>
0x7ffff7fe5759 <_dl_start+1417>: call 0x7ffff7fe3ec0 <_dl_sysdep_start>
0x7ffff7fe575e <_dl_start+1422>: mov %rax,%rbx
0x7ffff7fe5761 <_dl_start+1425>:
testb $0x80,0x17338(%rip) # 0x7ffff7ffcaa0 <_rtld_global_ro>
0x7ffff7fe5768 <_dl_start+1432>: jne 0x7ffff7fe5803 <_dl_start+1587>
0x7ffff7fe576e <_dl_start+1438>: add $0x88,%rsp
0x7ffff7fe5775 <_dl_start+1445>: mov %rbx,%rax
0x7ffff7fe5778 <_dl_start+1448>: pop %rbx
0x7ffff7fe5779 <_dl_start+1449>: pop %r12
0x7ffff7fe577b <_dl_start+1451>: pop %r13
実行を繰り返しながら確認すると、_dl_sysdep_start の内部処理で行っているようです。同様の手順で進めていくと、この中の dl_main が怪しそうでした。
b dl_main の breakpoint はうまく機能するようなので、活用しつつまた ni を繰り返していきます。b dl_main r ni 1500 としたあたりで、libc はロードされていてかつ <sin@got.plt> は初期値のままという状態になったので、b *0x7ffff7f01ae0 c をしてみましょう。
Breakpoint 2, 0x00007ffff7f01ae0 in ?? () (gdb) x/10i $rip => 0x7ffff7f01ae0: endbr64 0x7ffff7f01ae4: mov 0xb74e5(%rip),%rdx # 0x7ffff7fb8fd0 0x7ffff7f01aeb: mov 0x9c(%rdx),%ecx 0x7ffff7f01af1: test $0x10,%ch 0x7ffff7f01af4: je 0x7ffff7f01b06 0x7ffff7f01af6: lea 0x4a7d3(%rip),%rax # 0x7ffff7f4c2d0 0x7ffff7f01afd: testb $0x20,0xb8(%rdx) 0x7ffff7f01b04: jne 0x7ffff7f01b2e 0x7ffff7f01b06: lea 0x506f3(%rip),%rax # 0x7ffff7f52200 0x7ffff7f01b0d: testb $0x1,0xde(%rdx) (gdb) set *(long*)(*(long*)0x7ffff7fb8fd0 + 0x9c) &= ~0x1000L
シンボルは表示されていないですが、先ほど見た処理に来ています。該当のフラグを 0 にしてしまいましょう。 都度実行するのは大変なので、コマンドライン引数で渡してしまうことにすると、次のようなものに相当します。
% gdb -ex 'b dl_main' -ex 'r' -ex 'ni 1500' -ex 'b *0x7ffff7f01ae0' -ex 'c' -ex 'set *(long*)(*(long*)0x7ffff7fb8fd0 + 0x9c) &= ~0x1000L' -ex 'disa 2' -ex 'b main' -ex 'c' -ex "x/a &'sin@got.plt'" ./chall
Breakpoint 3, 0x0000555555555171 in main () 0x555555557fd0 <sin@got.plt>: 0x7ffff7f558c0 <__sin_avx>
期待通り __sin_avx に変わっていることが確かめられました。
ということで、このフラグを変える方法がわかればよく、このフラグがなんなのかを調べていきましょう。
(gdb) set $rip = __sin_ifunc (gdb) x/i $rip => 0x7ffff7f01ae0 <__sin_ifunc>: endbr64 (gdb) si sin_ifunc_selector () at ../sysdeps/x86_64/fpu/multiarch/ifunc-avx-fma4.h:32 warning: 32 ../sysdeps/x86_64/fpu/multiarch/ifunc-avx-fma4.h: No such file or directory
$rip を __sin_ifunc にしたりすることでファイル名を特定します。glibc のコード ifunc-avx-fma4.h @ glibc-2.39.9000 を見に行きましょう。
共通処理なのでマクロがたくさんありますが CPU_FEATURE_USABLE_P (cpu_features, FMA) というものがあり、これを false にできればよさそうです。コードの見た目も先ほど解析したものと似ています。
ということで、これが FMA に関する cpu_features のフラグであるということがわかりました。
次は、ここの値を設定している処理を突き止めましょう。
0x7ffff7ffcb3c <_rtld_global_ro+156>: 0x178881107ed83203
この値への書き込みを監視しましょう。
(gdb) info proc mappings
process 284
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
0x555555554000 0x555555555000 0x1000 0x0 r--p /mnt/chall
0x555555555000 0x555555556000 0x1000 0x1000 r-xp /mnt/chall
0x555555556000 0x555555557000 0x1000 0x2000 r--p /mnt/chall
0x555555557000 0x555555559000 0x2000 0x2000 rw-p /mnt/chall
0x7ffff7fbf000 0x7ffff7fc3000 0x4000 0x0 r--p [vvar]
0x7ffff7fc3000 0x7ffff7fc5000 0x2000 0x0 r-xp [vdso]
0x7ffff7fc5000 0x7ffff7fc6000 0x1000 0x0 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc6000 0x7ffff7ff1000 0x2b000 0x1000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ff1000 0x7ffff7ffb000 0xa000 0x2c000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7fff000 0x4000 0x36000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 --xp [vsyscall]
該当のアドレスは libc 内のものではなく ld-linux-x86-64.so.2 内のもののようなので、starti 直後からアクセスはできそうです。
と思ったのですが、watchpoint を使おうとしたところうまく動いてくれなかったため、この方針は諦めます。
ld-linux-x86-64.so.2 側のアドレスにあるということは、機能としてもそこで提供しているものだと考えられるので、ld-linux-x86-64.so.2 側について調べていきます。
“man ld-linux-x86-64.so.2” でググってみると ld.so(8) がトップに出てきました。
# ls -l $(which ld.so) | cut -d\ -f9- /usr/bin/ld.so -> ../lib64/ld-linux-x86-64.so.2 # ls -l /usr/lib64/ld-linux-x86-64.so.2 | cut -d\ -f9- /usr/lib64/ld-linux-x86-64.so.2 -> ../lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 # ls -l /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 | cut -d\ -f9- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 # readlink -e $(which ld.so) /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
複数段のシンボリックリンクになっているようですが、今回調べているものに一致していそうなのでこれを読んでみましょう。
一般に、man には ENVIRONMENT や FILES といった章があり、関連する環境変数やファイルについての一覧が載っています。
rev 御用達の環境変数 LD_PRELOAD についてもここに載っていますね。
なのですが、今回使えそうな環境変数は載っていないように見えます。
↑↑ ここ大見逃しでした。よく見るべきです。 ↑↑
関連がありそうな話題として glibc Hardware capabilities というものが載っています。ld.so も glibc に含まれるものなので、glibc のマニュアルを見てみましょう。
Tunables are a feature in the GNU C Library that allows application authors and distribution maintainers to alter the runtime library behavior to match their workload. These are implemented as a set of switches that may be modified in different ways. The current default method to do this is via the
GLIBC_TUNABLESenvironment variable by setting it to a string of colon-separated name=value pairs.
The
glibc.cpu.hwcaps=-xxx,yyy,-zzz...tunable allows the user to enable CPU/ARCH featureyyy, disable CPU/ARCH featurexxxandzzzwhere the feature name is case-sensitive and has to match the ones insysdeps/x86/include/cpu-features.h.
これぞまさにといった感じがします。sysdeps/x86/include/cpu-features.h のどの部分にマッチすればいいのかが若干不明瞭ではありますが、先ほど CPU_FEATURE_USABLE_P (cpu_features, FMA) と書かれていたので、たぶん FMA と書けばよさそうな気がします。
ということでこれを与えて実行します。
% nc localhost 1337
env key: GLIBC_TUNABLES
env value: glibc.cpu.hwcaps=-FMA
Wow, we got 0.2512157895691233!
Alpaca{FMA_is_eSSEntial_2_the_expected_behAVXior}
これにより計算結果が変わります。👏 flag は FMA, SSE2, AVX との掛詞でした。FMA を使う場合と使わない場合とで丸めを行うタイミングが変わるため、計算結果にも影響が出てきます。
補足
x/i 'sin@got.plt' したときにデバッグシンボルがないとこんな感じになってしまうかもしれません。
(gdb) si 0x0000555555555074 in sin@plt () (gdb) 0x00007ffff7f4c2d0 in ?? () from /lib/x86_64-linux-gnu/libm.so.6 (gdb) x/10i $rip => 0x7ffff7f4c2d0: endbr64 0x7ffff7f4c2d4: push %rbp 0x7ffff7f4c2d5: mov %rsp,%rbp 0x7ffff7f4c2d8: push %rbx 0x7ffff7f4c2d9: sub $0x38,%rsp 0x7ffff7f4c2dd: mov %fs:0x28,%rax 0x7ffff7f4c2e6: mov %rax,-0x18(%rbp) 0x7ffff7f4c2ea: xor %eax,%eax 0x7ffff7f4c2ec: vstmxcsr -0x28(%rbp) 0x7ffff7f4c2f1: mov -0x28(%rbp),%edx
基本的には勝手に入っていてくれる気がしますが、libc のバージョンを上げ下げした場合などにはついてこないかもしれません? 私は、glibc のバージョンを固定したいときは次のようなスクリプトで該当のパッケージを入れています。
fetch-libc.py
import requests from bs4 import BeautifulSoup series, target, version = "noble", "amd64", "2.39-0ubuntu8.1" # series, target, version = "jammy", "amd64", "2.35-0ubuntu3.8" # series, target, version = "focal", "amd64", "2.31-0ubuntu9.9" def get_url_and_name(series, target, package, version): url = f"https://launchpad.net/ubuntu/{series}/{target}/{package}/{version}" html = BeautifulSoup(requests.get(url).text, features="lxml") a = html.select_one("#files").select_one("a.sprite") return (a.attrs["href"], a.text) libc_dev_bin = get_url_and_name(series, target, "libc-dev-bin", version) libc6 = get_url_and_name(series, target, "libc6", version) libc6_dev = get_url_and_name(series, target, "libc6-dev", version) libc6_dbg = get_url_and_name(series, target, "libc6-dbg", version) cmd = rf""" RUN apt-get remove -y libc6-dev libc6-dbg RUN apt-get -y install curl && \ curl -O {libc_dev_bin[0]} && \ curl -O {libc6[0]} && \ curl -O {libc6_dev[0]} && \ curl -O {libc6_dbg[0]} RUN dpkg -i {libc_dev_bin[1]} && \ dpkg -i {libc6[1]} && \ dpkg -i {libc6_dev[1]} && \ dpkg -i {libc6_dbg[1]} """ print(cmd)
今回の問題は glibc のバージョンにはそこまで神経質にならなくても大丈夫であった想定ですが、バージョンが重要になってくる問題もしばしばあるため、固定できるようにしておくと便利かもしれません。
バージョンを気にせずやっていると、apt-get update やら apt-get install gdb やらをした拍子に glibc のバージョンが上がり、そのせいでサーバと glibc のバージョンがずれてうまくいかなくなるなど、そういう不幸がしばしば起きます。
あとがき
浮動小数点型あるあるとして、「== で比較してはいけない」とか「差の絶対値が適当な閾値以下になるかで判定すべき」とかいう主張がしばしばなされますが、えびちゃんとしてはそういう主張は正しくないと思っています。それについてはこの問題の範囲を逸れるのでここでは触れないですが、安易にそうした方法を取ってしまう人には考え直してほしいです。過去に書いた記事を読んでくださるとよいかもしれません。
さて、CTF に関してですが下記の概念については調べておくとよいかもしれません。pwn でもよく話題になってきます。
- PLT (procedure linkage table)
- GOT (global offset table)
- RELRO (relocation read-only)
- ASLR (address space layout randomization)
特に relocation やシンボルの名前解決まわりに関しては今回の write-up できちんと説明しきれなかった(write-up を書くまでにちゃんと理解できなかった)ので、また今度書こうかなと思っています。
今回は GLIBC_TUNABLES を使いましたが、たとえば「別の定義の sin() が載っている shared object がたまたま存在していて、それを LD_PRELOAD で読み込ませる」といった解法も考えられそうな気がします。試した限りではそうしたファイルは存在しなさそうに見えましたが、そういう別解もたまたま見つけられたら面白いかなと思いました。
問題名に関しては、罪の方の sin を含むフレーズを適当に探してそれと掛けたものでした。罪と罰の罪は crime の方なんですね。
そもそもの題材が題材なので、環境差異をどう処理するかで何度かリテイクが入りました。たとえば macOS で colima start --arch aarch64 の環境で実行すると CPU_FEATURE_USABLE_P (cpu_features, FMA) が false になるため、適当な値を入れるだけでフラグが出てきてしまっていました。
colima start --arch x86_64 の環境とそれぞれ cat /proc/cpuinfo | grep -E 'flags|Features' | uniq や ld.so --list-diagnostics の出力を見比べて「そっか〜」となりました。cpuinfo を提供するとか「こういう環境だとうまくいかないからこうしてね」という旨のテキストを配布ファイルに含めるとかを当初は想定していたのですが、結局サーバでも qemu-x86_64 -cpu Skylake-Client-v3,-xsavec を用いて実行する方針になりました。いろいろと minaminao さんにお世話になりました(し、たぶん今後もなる気がします)。ありがとうございます。
2/20 もえびちゃんの問題が出る予定なので、そちらもよろしくおねがいします。
別解について
ここから懺悔パートです。
日が変わる前あたりに下記のような問題について考えていました。
import os import subprocess key, value = input("env key: "), input("env value: ") os.environ[key] = value if subprocess.run(["false"]).returncode == 0: print("Alpaca{Fundamental Ability Lost: Successful Exit}")
これに対する解法が書けてやったやったと思っていたところ、同様の解法で今日の問題が解けてしまうことに気づきました。気づいた時点では 2/17 0:02 でした。ううむ。
server.py で判定するのではなく、chall.c 側で次のようにするべきだったろうと思います。
if (sin_x == EXPECTED) { puts("failed"); } else { puts(FLAG); }
環境変数を与えて返り値で判定する問題を見たときは気をつけましょう(1 敗)。
こちらの問題に対する入力は次の通りです。
ネタバレ
LD_TRACE_LOADED_OBJECTS=t
write-up を書いたときに ld.so(8) をよく読んでいれば間に合っただろうなぁという気持ちでいっぱいです。悔し〜〜〜。 そんなこんなで今日は悔しみながら過ごしました。それもまたよい経験ですね。学びはありました。
おわり
おわり