ハッカーバトル:PHDays CTFおよびCTF Qualsジョブの解析

Positive Hack Days CTF-国際的な情報保護競技。Capturethe Flagのゲーム原則に従って開催されます。 割り当てられた時間にいくつかのチームがネットワークを守り、見知らぬ人を攻撃します。 参加者の主なタスクは、敵のシステムの脆弱性を特定し、秘密情報(フラグ)にアクセスすると同時に、システムのこのような脆弱性を検出して排除することです。



今日のトピックでは、過去の大会の参加者が直面したいくつかの興味深いタスクの分析を紹介します。



歴史と地理



今年PHDays CTFは4回目の開催となります。 2011年のPositive Hack Daysフォーラムで初めてコンテストが開催され、その後、アメリカのPPPチームの参加者が勝者となり、ロシアチームのリートモアが翌年、オランダのEindbazenがPHDays IIIのチャンピオンになりました。 毎年、世界中のチームが、米国から日本まで、PHDays CTFに参加しています。



今年、600以上のチームが予選大会に参加するために登録しました。



画像



クエストと雰囲気



確立された伝統によると、ゲームのタスクとインフラストラクチャは、競技の伝説に基づいて準備されます。これは、PHDays CTFタスクの単純なセットを目標のあるエキサイティングな競技に変える特別なストーリーです。 たとえば、昨年、参加者は架空のD'Errorimの世界を死から救いました。 今後の競技会はこの物語を続けます。



競争のタスクは通常、実際のプロトタイプに基づいています。CTFのタスクとサービスの脆弱性は、実際のさまざまなシステムに見られます。 PHDays CTF競技は、オリジナルのゲームメカニクスにとっても興味深いものであり、ゲームをプレイするためのさまざまな異なる戦略の実装を可能にします( PHDaysのWebサイトを参照)。



通常、主催者は、ハッキングに直接関係のないチームのために異常なタスクを準備します。 たとえば、PHDays 2012では、特別なゴミ箱でボーナスフラグを見つけることで追加ポイントを獲得でき、PHDays IIIでは、「 ハッカー迷路 」-レーザーフィールド、追跡センサー、シークレットドア、バグやその他の興味深いテスト。



しかし、もちろん、主要なポイントは、情報セキュリティのさまざまな問題を解決する過程でのみ獲得されます。 それらのいくつかを見てみましょう。



解析



競争の資格段階(PHDays CTF Quals)は、タスクベースのCTFのタイプを指します。つまり、チームはタスクを解決し、ポイントを獲得する必要があります。 タスクは、次のカテゴリのいずれかに分類できます。





最後のカテゴリから始めましょう。



明白でないクエスト



MPDays IV CTF Qualsの参加者は、 mp3ファイルに隠されているメッセージを解読するために必要なタスクの1つとして。



原則として、問題の状態がコンテナーに隠されたメッセージの抽出に言及している場合、ステガノグラフィーの分野からの既成のソリューションの1つが使用されます。 この場合、答えを見つけるには、通常、復号化するプログラムを選択し、正しいキーで実行する必要があります。 つまり、特定のタスクを解決する際の「成功の鍵」は、著者によって以前に規定された適切なオプションの検索にあります。



私たちの場合、すべてが多少異なります。 提案されたファイルをテキストエディタで開くと、次のようになります。

画像

ファイルの先頭には、ID3形式のメタデータがあります。 最初にTRCK(トラック番号)タグがあり、次にいくつかのテキストがあります。



RGB7 5.183、NULL RGB6 0.42,159 RGB5 194,244,68 RGB4 47,77,6 RGB3 44,73,141 RGB2 140,207,72 RGB1 120,156,203



この情報は、7つのレコード(RGB7からRGB1まで)に分割できます。



RGB7 5.183、NULL

RGB6 0.42.159

RGB5 194,244.68

RGB4 47.77.6

RGB3 44.73.141

RGB2 140,207.72

RGB1 120,156,203



各RGB識別子の後に3つの値があります。 これらは通常数値ですが、ある場合にはNULLです。 これはレコードの配列であり、各レコードには最大3つのシングルバイト値が含まれると仮定するのは簡単です。 たとえば、次のプログラムを使用して、10進コードを文字にソート、結合、変換し、16進で印刷できます。



>>> a = [120,156,203, 140,207,72, 44,73,141, 47,77,6, 194,244,68, 0,42,159, 5,183]





>>> print "".join(map(chr, a)).encode("hex")







その結果、以下が得られます。



789ccb8ccf482c498d2f4d06c2f444002a9f05b7







16進シーケンスは、コード0x78 0x9Cのバイトで始まり、zlibデータ圧縮アルゴリズムが使用されていることがわかります。 デフォルトのパラメーターを使用して圧縮モードでzlibを使用する場合、出力シーケンスはこれらのバイトで始まります。



Pythonのzlibライブラリの解凍機能を1回呼び出すと、パックされたメッセージを解凍できます。



 >>> import zlib >>> print zlib.decompress("".join(map(chr, a)))
      
      





そして、テキストが表示されます:



i_hate_ucucuga



大会の主催者に送らなければならなかったのはこの旗でした。



無効な暗号化



この割り当ては、Cryptoカテゴリに属します。 伝説によれば、通信セッションが傍受されたため、チームは送信されたメッセージを解読する必要があります。

画像

まず、キーを交換してから暗号化されたデータを送信するプロセスがはっきりと見えます。 このような通信を構築できる暗号の基礎に基づいて理解する必要があります。



タスクはmarsと呼ばれ、これは変更されたRSAを意味すると想定できます。



各キーは2つの部分で構成され、両方の場合の2番目の部分は0x010001 == 65537-RSAの頻繁に使用される公開指数(e)です。 そのため、通信セッションでは、最初に公開鍵の交換(n 1 / e 1 、n 2 / e 2 )があり、次にそれらで暗号化されたメッセージの交換(c1、c2)があります。



これが本当にRSAに似ている場合、ci = pow(m i 、e i 、n i )。 m 1とm 2を見つける必要があります。

pow-モジュラーべき乗の関数、pow(val、exp、modulus)== val exp %modulus。



RSAアルゴリズムによると:





タスクn 1およびn 2の長さは1535ビットです。つまり、因数分解(単純な因子に分解)することはできません。



Pythonの拡張ユークリッドアルゴリズムの実装を使用します。



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





数値n 1およびn 2のGCD(最大公約数)を見つけます。



 gcd = egcd(n1,n2)[0]
      
      





GCD(n 1 、n 2 )の長さは1024ビットです。 数値n 1およびn 2の他の約数を見つけます。



 p1 = n1 / gcd p2 = n2 / gcd
      
      





p 1とp 2は512ビットの素数で、gcdは1024ビットの合成数(おそらく512 * 512)であり、分解するには大きすぎます...



目的のメッセージm iがp iを超えない数で表すことができる場合を考えます。



n i = p i * q * rとすると、0 <m i <p iの場合、次の式が有効になります。



pow(m i 、e i 、n i )%p i == pow(m i 、e i 、p i



次に、復号化の指数d ' iは、次の式を満たす必要があります。



e i * d 'i≡1 modφ(p i



d ' iの値は、代数の補数を計算することで見つけることができます。



d ' i = invmod(e i 、φ(p i ))



プライムp iの場合、



φ(p i )== p i-1



したがって:



d ' i = invmod(e i 、p i-1



代数補数の計算は、Pythonの次の関数によって実装されます。



 def invmod(a, m): # Modular inversion g, x, y = egcd (a, m) if g == 1: return x % m raise Exception("modular inverse does not exist")
      
      





また、数値を行に変換し、最後の文字「\ 0」から行末までのテキストのみを残す関数が必要になります。



 def showX(v): print ("%0256X" % v).decode("hex").split('\0')[-1]
      
      





diを計算し、復号化を実行します。



 d1 = invmod(e, p1-1) d2 = invmod(e, p2-1) showX(pow(c1, d1, p1)) showX(pow(c2, d2, p2))
      
      





そして結果が得られます:



リクエスト:GET_FLAG(署名:5e2d5e0323591b1c)。

応答:its_n0t_ab0ut_p4dd1ng



フラグは文字列「 its_n0t_ab0ut_p4dd1ng



」です。



CCC暗号割り当て



指定:ecc.pyおよびtask.pyファイルを含むsource.tar.gzアーカイブには楕円暗号を使用して実装されキー検証スキームが含まれます。 アドレス195.133.87.171でポート5555に接続することにより、サーバーとの接続を確立できることが知られています。



nc 195.133.87.171 5555





password: secch4l*







ソースが提供されているため、分析から始める価値があります。 実行することもできます。

libnumモジュールがなかったため、自分で作成する必要がありました。 前述のモジュラー反転の機能と、それによって使用される拡張ユークリッドアルゴリズムを実装するだけで十分です。



 def egcd(a, b): # Extended Greatest Common Divisor if a == 0: return (b, 0, 1) else: g, y, x = egcd (b % a, a) return (g, x - (b // a) * y, y) def invmod(a, m): # Modular inversion g, x, y = egcd (a % m, m) if g != 1: raise Exception("modular inverse does not exist") else: return x % m
      
      





したがって、 task.py



main



関数:



 def main(): print "Auth:“ auth = raw_input() if hashlib.sha1(auth).hexdigest() != "375d5c01ca1b8c3863024d10aac7713472eb5033": # secch4l* print "nope“ return prefix = os.urandom(8) print "Proof of work, please“ print "Prefix is (hexed) ", prefix.encode("hex") test = raw_input().decode("hex") if not test.startswith(prefix) or len(test) > 16: print "nope“ return h = hashlib.sha1(test).hexdigest() if not h.startswith("000000"): print "nope“ return goflag()
      
      





行が読み取られます。SHA-1は、指定された値( "secch4l *")と等しくなければなりません。

次に、ランダムな8バイトのプレフィックスがクライアントに送信されます。 バイトは16進文字列としてエンコードされます。 応答として、クライアントは指定されたプレフィックスで始まるように16バイト以下の文字列を送信する必要があり、この文字列のSHA-1値の最初の3バイトはゼロでなければなりません。 すべてのステップが成功すると、goflag()関数が呼び出されます。



次のコードは、サーバーに接続し、パスワードを送信し、プレフィックスを受信し、応答を計算して送信します。



 def readLn(sock): a = [] while True: c = sock.recv(1) if '\n' == c: return "".join(a) a.append(c) HOST = "195.133.87.171" PORT = 5555 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((HOST, PORT)) print readLn(sock) # Auth: sock.send("secch4l*\n") print readLn(sock) # Proof of work, please s = readLn(sock) print s # Prefix is (hexed) 0b3997e62b9ffbf4 prefix = s.split()[-1].decode("hex") for i in xrange(0x7FFFFFFF): s = "%s%X" % (prefix, i) if hashlib.sha1(s).digest()[:3] == '\0\0\0': break sock.send(s + '\n')
      
      





クライアント側でこのコードを実行した後、サーバーはgoflag()関数を実行し、次のテキストを表示します。



ECパスワードチェック

R = 572115218124168948525078362547166172445820217705568707355669424304224832114

共有秘密= R ^パスワード

暗号化されたメッセージ:7a93846a011e0d0382e94f32d705239e6298169dcec20da5d6



goflag



関数で何が起こるか:



 def goflag(): print "EC PASSWORD CHECK" r = random.randint(31337, 1 << 250) R = p256.power(G, r) print "R =", R print "SHARED SECRET = R ^ PASSWORD" S = p256.power(R, PASSWORD) key = p256.derive(S) cipher = encrypt(FLAG, key) print "ENCRYPTED MESSAGE:", cipher.encode("hex")
      
      





楕円曲線の非対称暗号化が使用されます。 NISTが推奨するP-256曲線が選択されています。 カーブポイントに対する操作の実装には、明らかな脆弱性は含まれていません。



Rの値は知っていますが、PASSWORD(password.txtファイルからサーバーによって読み取られる)の値がわからないと、Sを計算できません。Sを知っていると、キーを簡単に計算できます。 暗号化はエラーで実装されているのでしょうか?



task.py



encrypt



機能:



 def encrypt(msg, key): iv = os.urandom(8) stream = hashlib.sha256(iv + key).digest() stream = hashlib.sha256(stream + iv + key).digest() cipher = iv + xor(msg, stream) return cipher
      
      





このコードは、暗号化されたメッセージの前にランダムな8バイトの初期化ベクトルivがあり、2つのSHA-256計算の出力としてガンマが生成されたXORフラグとして暗号化が実行されることを示します。 キーの意味を知らなくても、ガンマを取得するのは非現実的です。 しかし、プログラムでキーはどのように取得されますか?



task.pyの派生関数:



 def derive(self, p): return hashlib.sha256(str((p[0] << 10) / p[1])).digest()
      
      





ポイントSの値(xとyの2つの座標で構成される)が入力SHA-256として使用されることがわかります。 実際、str(int(x * 1024 / y))がハッシュ入力に提供されます。 xとyは値が近い(これらは大きな整数である)ため、算術演算の結果は1024に近いはずです(ただし、数回それを超えることがあります)。



したがって、派生関数の特定の実装により、キー値は非常に少数の状態をとることができます。 それらすべてを単純にソートし、各キーのメッセージを復号化してみてください。印刷された文字のみで構成されている場合、成功しています。



 import hashlib, ecc enc = "7a93846a011e0d0382e94f32d705239e6298169dcec20da5d6".decode("hex") iv = enc[:8] def decrypt(key): stream = hashlib.sha256(iv + key).digest() stream = hashlib.sha256(stream + iv + key).digest() return ecc.xor(enc[8:], stream) for i in xrange(0x7FFFFFFF): s = decrypt(hashlib.sha256(str(i)).digest()) for c in bytearray(s): if c < 32 or c >= 128: break else: print s # ecc_is_too_s3cure break
      
      





したがって、フラグは「ecc_is_too_s3cure」という行です。



リバースエンジニアリング。 Shadelt900



リバースエンジニアリングは、別の一般的なジョブカテゴリです。 CTFに加えて、Best ReverserコンテストはPHDaysコンテストプログラムに含まれています。



Shadelt900の割り当ては、前の3つと同様に、2014年1月に開催されたPHDays IV CTF Qualsプログラムの一部でした。 チームは「derrorim_enc.bmp」という画像を復号化する必要がありました。 暗号化に使用したツールはShadelt9000.exeと呼ばれていましたが、復号化ツールが見つかりませんでした。 これが画像です:



画像



Shadelt9000.exeファイルをよく見ると、アプリケーションがOpenGLを使用していることが明らかになります。 著作権インフレート1.2.8 Copyright 1995-2013 Mark Adlerもあり、このプログラムが一般的なzlib圧縮ライブラリを使用していることを示しています。



zlib関数の呼び出し元の逆アセンブラを見ると、そのようなコードがすぐに見つかります。



画像



アドレス0x47F660および0x47F7B8には、zlibによってパックされたデータ配列があります。 それらを開梱します。



 from zlib import decompress as unZ base = 0x47C000 - 0x7AE00 # data section base ab=open("ShadeIt9000.exe", "rb").read() open("1.txt", "w").write(unZ(ab[0x47F660-base:],-15)) open("2.txt", "w").write(unZ(ab[0x47F7B8-base:],-15))
      
      





解凍後、1.txtファイルにはピクセルシェーダーが含まれています。



 #version 330 uniform sampler2D u_texture; uniform sampler2D u_gamma; varying vec4 texCoord0; varying vec3 v_param; uint func(vec3 co){ return uint(fract(sin(dot(co ,vec3(17.1684, 94.3498, 124.9547))) * 68431.4621) * 255.); } uvec3 rol(uvec3 value, int shift) { return (value << shift) | (value >> (8 - shift)); } const uvec3 m = uvec3(0xff); void main() { uvec3 t = uvec3(texture2D(u_texture, vec2(texCoord0)).rgb * 0xff) & m; uvec3 g = uvec3(texture2D(u_gamma, vec2(texCoord0)).rgb * 0xff) & m; int s = int(mod(func(v_param), 8)); t = rol(t, s); vec3 c = vec3((t ^ g) & m) / 0xff; gl_FragColor = vec4(c, 1.); }
      
      







2.txtファイルには頂点シェーダーが含まれています。



 attribute vec3 a_param; varying vec4 texCoord0; varying vec3 v_param; void main(void) { gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; texCoord0 = gl_MultiTexCoord0; v_param = a_param; }
      
      





ピクセルシェーダーに関する主な情報は赤で強調表示されています。



画像



変数tは、処理されたテクスチャ(入力ファイル)の現在の要素であることがわかります。

変数gには、現在のガンマ要素(擬似ランダムな方法で生成された)。

変数sには、後でsの循環シフトに使用される値があります。

出力値は実際には次のように計算されます



(rol(t,s) ^ g)







さらに、同じ入力ファイルでプログラムを複数回実行すると、各要素のgの値は開始から開始まで変化し、tとsは同じままです。



ガンマの生成方法を確認します。



 unsigned char *pbGamma = malloc(cbGamma); srand(time(0)); for (i = 0; i < cbGamma; i++) { pbGamma[i] = rand(); }
      
      





現在の時刻に依存していることがわかります。



元のアーカイブから、derrorim_enc.bmpファイルが2014年1月21日18時37分52秒に作成されたことがわかります。

その時点でtime()関数が返す値を取得します。



 >>> import time >>> print hex(int(time.mktime((2014,1,21, 18,37,52, 0,0,0))))
      
      







0x52de8640



次に、ShadeIt9000.exeファイルをShadeIt9000_f.exeにコピーして修正します。



オフセット00015557でバイトが必要



E8 A5 31 01 00







に置き換える



B8 40 86 DE 52







これは、置換と同等です



mov eax,52de8640h



ます。



したがって、ShadeIt9000_fのバージョンを取得しました。これは、対象のファイルが暗号化されたときと同じ色域で常に暗号化されます。

次に、画像の解読に役立つ値を準備する必要があります。



 import os bmp=open("derrorim_enc.bmp", "rb").read() hdr = bmp[:0x36] abData = bytearray(bmp[0x36:]) cbBody = len(bmp) - len(hdr) open("00.bmp", "wb").write(hdr + '\0'*cbBody) open("XX.bmp", "wb").write(hdr + '\2'*cbBody) os.system("ShadeIt9000_f.exe 00.bmp") os.system("ShadeIt9000_f.exe XX.bmp")
      
      





00_enc.bmpファイルには、ゼロバイトで構成される画像の暗号化の結果が含まれます。 これは、最も純粋な形式のガンマになります。



XX_enc.bmpファイルには、値2のバイトで構成される画像の暗号化結果が含まれます。これにより、各バイトが循環的にシフトしたビット数がわかります。



最後に、Shadelt9000を復号化します。



 def rol(v,i): return (((v<<i) & 0xFF) | ((v>>(8-i)) & 0xFF)) def ror(v,i): return (((v>>i) & 0xFF) | ((v<<(8-i)) & 0xFF)) dRot = {rol(1,i):i for i in xrange(8)} bmp=open("derrorim_enc.bmp", "rb").read() hdr = bmp[:0x36] abData = bytearray(bmp[0x36:]) abGamma = bytearray(open("00_enc.bmp", "rb").read()[0x36:]) abRot = bytearray(open("XX_enc.bmp", "rb").read()[0x36:]) for i,b in enumerate(abGamma): abRot[i] = dRot[abRot[i] ^ b] for i,b in enumerate(abGamma): abData[i] = ror(abData[i] ^ b, abRot[i]) open("derrorim.bmp", "wb").write(hdr + str(abData))
      
      





私達は得る:



画像



タスクを解決するための正しい方法ですが、最も効果的ではありません。 もっと短い方法があります。



アドレス0x47F848および0x47F9A0の頂点シェーダーのすぐ後ろに、ピクセルと頂点シェーダーのパックされたzlibコードがあり、逆変換を実行します。 おそらく、彼は誤ってタスクデザイナーに忘れられていたのでしょう。 または、意図的に残しておくこともできます。



暗号化と復号化の頂点シェーダーコードは同一であるため、それらに触れても意味がありません。 ピクセルシェーダーを交換するとどうなりますか?



ShadeIt9000_f.exeをShadeIt9000_d.exeにコピーして修正します。



00015775: 60 F6 ==> 48 F8







次に、ShadeIt9000_d.exe derrorim_enc.bmpを実行します。 そして、復号化されたファイルderrorim_enc_enc.bmpの出力を取得します。これは(小さなアーティファクトを除き)Pythonスクリプトで復号化されたものと一致します。



今日は以上です! ご清聴ありがとうございました。コメントの質問にお答えします。



PHDays IV CTFのファイナルは5月21日と22日にPositive Hack Daysフォーラムで開催されます。 競技の進行状況をサイト上で直接監視できるだけでなく、モバイルアプリケーションを使用して監視することもできます。 ニュースに従ってください!



また読む:





PHDays IVのオンラインコンテストHashRunnerと「 Competitive Intelligence 」への参加が既に登録されていることをお知らせします。



PSすべてのPHDays CTFおよびCTF Quals割り当てのアーカイブは、PHDays Webサイトで見つけることができます。 だから、あなたが自分自身をテストしたいのであれば、先に進んでください!



PPSこのトピックで示されているタスクの詳細な分析は、Dmitry Sklyarovが主催する特別なウェビナーで行われました。 ウェビナーエントリは、 http//my.webinar.ru/record/290241/で入手できます。



All Articles