Padding Oracle系の問題を解いてみた
今週の勉強会は自分が担当だったので、前からずっと取り扱いかったPadding Oracle Attackの問題を繰り出しました。
基本
概略
CTFに置いての位置付け
- ちょくちょく見る
- 初見としはうざい
特徴 (あくまでも経験上の話)
CBCモード ←必須
AES類のブロック暗号 ←必須
サーバーが復号化をやってくれる
サーバーコード多くの場合は公開
IVまでこっちから送られる ←場合によって
何ができる?
Decryption Attack 難易度:高(特にEncryption Attackと融合する時)
暗号文の復号ができる
平文はflagの場合Encryption Attack 難易度:中
暗号文の改造による平文の改ざん
平文を特定なコマンド/データに改ざんし認証をバイパスできる
解法
詳しい解説は↓にある自分のスライドをみてください
Encryption Attack
要はCBCモードに暗号化された(既知)平文を自分が思う通りにすり替える手法
例題: INS'hack 2019 Jean-Sébastien Bash
競技中解けなかった問題です、ちょうど当てはまるので解いてみた。
まずはserver.py
inshack-2019/server.py at master · InsecurityAsso/inshack-2019 · GitHub
少し読んでみると以下のものがわかるようになりました:
* unpad失敗したらWhat do you mean?
が返ってくる
* /cmd {暗号文}
の後ろの暗号文をサーバーが復号化してos.system()
で内容(コマンドに想定)を実行
netcatでサーバーを立てて接続して、色々探ってみたら
Welcome on my server. /help for help >/help /help This is a tool so that only me can execute commands on my server (without all the GNU/Linux mess around users and rights). - /help for help - /exit to quit - /cmd <encrypted> to execute a command Notes (TODO REMOVE THAT) --------------------------- Ex: /cmd AES(key, CBC, iv).encrypt(my_command) /cmd 986b60ec3b7beef7bfd1642a25d20f02 >/cmd 986b60ec3b7beef7bfd1642a25d20f02 /cmd 986b60ec3b7beef7bfd1642a25d20f02 Running ls -l 合計 8 -rw-r--r-- 1 root root 21 6月 9 00:22 flag.txt -rw------- 1 root root 0 6月 9 00:21 nohup.out -rw-r--r-- 1 ingo ingo 2047 6月 9 00:19 server.py
どうやら例としてはls -l
を暗号化したようです。
server.py
と同じディレクトリにflag.txt
を置いてました。
しかも暗号文の長さは32ということはたった1ブロックだったことです。
ls
ではなくて、cat flag
だったらflagが見えるはずということでEncryption Attackを起用。
方針としては:
* 長さは32かつunpadもうまくunpadも通る暗号文を探す
* Encryption Attackによって後ろの1ブロックを; cat flag.txt\x02\x02
にすり替える
イメージとしては:
最後diffと後ろの暗号文とくっつけてサーバーに送れば潰された1ブロック目を飛ばして、後ろのcat flag.txt
が実行されるはず。
solverは↓
# -*- coding: utf-8 -*- from pwn import * from Crypto.Util.number import long_to_bytes,bytes_to_long from binascii import hexlify, unhexlify import os import random import string from Crypto.Cipher import AES import sys import signal ip,port = 'localhost',6000 io = remote(ip,port) print io.readuntil('>') # 32byte復号してくれる暗号文を探す ct = '' n = 0 cmd = '/cmd {}' pt_base = '' while True: ct = hex(n)[2:].zfill(64) io.sendline(cmd.format(ct)) print io.readline() rep = io.readline() print rep,len(rep) if 'What' in rep: n += 1 io.readuntil('>') continue # Runningの後ろの部分を切り取る pt_base = rep.strip()[8:] if len(pt_base) >= 30 and len(pt_base) <= 32: # pt_base io.readuntil('>') break print io.readuntil('>') n += 1 def pad(s): return s + (16 - len(s) % 16) * chr(16 - len(s) % 16) #padding付き w\x8a\x8b\xb6\x1d\x9a|\x89E\x1c\xbcS:<8\x0fa\x81= N-k + ¥x02¥x02 print '[+] start exploit' target = pad('; cat flag.txt') print pt_base[16:] print len(pt_base) d = xor(pad(pt_base[16:]),target) assert len(d) == 16 print d # assert xor(diff,pad(pt_base[16:])) == target ct = hexlify(d) + ct[32:] assert len(ct) == 64 io.sendline('/cmd {}'.format(ct)) while True: print io.readline()
Decryption Attack
平文が知らない/知りたいときはこの手法を使います。padding方式を逆手にとって、知りたいブロックを全部paddingに変換させまだxor
を取る前のDec(ct)
の値を求める。←さえ分かれば平文やそのあとEncryption Attackも展開できます。
例題: picoctf 2018 magic padding oracle
まずはサーバーコード
読んでみたら以下のようなものがわかりました:
* cookieはusername
,is_admin
,expires
この三つから構成されたjson形式
* is_admin==true
とexpires
のtimestampは「現在」の時間より未来の方であれば認証が通る
* unpad失敗したらinvalid padding
が返ってくる
サーバーがいまだに生きているので、接続してみたら
Welcome to Secure Encryption Service version 1.51 Here is a sample cookie: 5468697320697320616e204956343536a2e32fc2e97693e94706c97a85bb71cb23c7271421a8435ba8d6abbbb5e11fd5a27a82dba1480f03948f90ff0f122eeb397e8aa13e44c1e4fcfd7293cf99bf3b340213f70e50b6039a84189af70e5840 What is your cookie?
cookieはiv+暗号文、プラス暗号文の長さは80
全体としての方針としては:
* まずDecryption Attackで平文の全貌を暴く
* ↑を基づいて改ざんして認証をバイパス
今回こそちゃんと理解したいので、モジュールを頼らず実装を挑戦してみました。
まずはDecryption Attackの部分
* 最後の2ブロックから復号するから、基本[iv+block1+block2]の形式でサーバーに送る
* ブロック内も最後のバイトから弄る、invalid padding
が出ない時はbrute-forceの成功を意味します
* 最終的に全部\x10
にpaddingされた時、元の平文が復元できる
イメージとしては:
だんだん復号させた平文をpaddingに埋め尽くされにいって、最終的には全部paddingにする過程はDecryption attackのミソです。
この部分の実装は↓
for i in range(5,0,-1): b = '' for idx in range(15,-1,-1): #print 16-idx for d in range(256): if i == 5 and idx == 15 and d == 0: continue ip,port = 'localhost',6000 #io = remote(ip,port) s,f = sock(ip,port) #io.readuntil('Here is a sample cookie: ') read_until(f,'Here is a sample cookie: ') sample_cookie = read_until(f).strip() iv,cookie = sample_cookie[:32],sample_cookie[32:] #io.readuntil('?') read_until(f,'?') #print 'cookie length is',len(unhexlify(cookie)) cookie = unhexlify(cookie) iv = unhexlify(iv) assert len(cookie) % 16 == 0 block = chunk(iv+cookie,16) #assert len(block) == 6 current,prev = block[i],block[i-1] assert len(prev) == 16 and len(current) == 16 x1 = left_pad(chr(d)+b) x2 = left_pad((16-idx-1) * chr(16-idx)) x3 = left_pad(''.join([chr(j) for j in range(16-idx-1,0,-1)])) prev = xor(prev,xor(x1,xor(x2,x3))) #io.sendline(iv+hexlify(prev+current)) s.send(hexlify(iv+prev+current)+'\n') #io.readline() read_until(f) #io.readline() read_until(f) #rep = io.readline() rep = read_until(f) #print rep if 'invalid' in rep: #print '[+] nope' continue else: print "0x{:02x}: ".format(d) + "{:02x}".format(16-idx) * (16-idx) b = chr(d) + b if idx == 0: print xor(xor(prev,'\x10'*16),block[i-1]) break
動かす時はこんな感じ:
0x0c: 01 0x0f: 0202 0x0e: 030303 0x09: 04040404 0x08: 0505050505 0x0b: 060606060606 0x0a: 07070707070707 0x05: 0808080808080808 0x04: 090909090909090909 0x07: 0a0a0a0a0a0a0a0a0a0a 0x06: 0b0b0b0b0b0b0b0b0b0b0b 0x01: 0c0c0c0c0c0c0c0c0c0c0c0c 0x00: 0d0d0d0d0d0d0d0d0d0d0d0d0d 0x73: 0e0e0e0e0e0e0e0e0e0e0e0e0e0e 0x2d: 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f 0x75: 10101010101010101010101010101010 }\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f 0x38: 01 0xb4: 0202 0x7e: 030303 0x93: 04040404 0x20: 0505050505 0x12: 060606060606 0xe4: 07070707070707 0xf1: 0808080808080808 0x3f: 090909090909090909 0x34: 0a0a0a0a0a0a0a0a0a0a 0x28: 0b0b0b0b0b0b0b0b0b0b0b 0x64: 0c0c0c0c0c0c0c0c0c0c0c0c 0x08: 0d0d0d0d0d0d0d0d0d0d0d0d0d 0xe2: 0e0e0e0e0e0e0e0e0e0e0e0e0e0e 0xac: 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f 0x43: 10101010101010101010101010101010 s_admin": "true"
平文がわかった上に、Encryption Attackが展開できます。
すり替えたいcookieは↓
{"username": "guest", "expires":"2030-01-07", "is_admin": "true"}
少なくても二箇所が元のcookieと違い、1ブロックのEncryption Attackが済まないので、
いっそう全ブロックのEncryption Attackを実装しました。
まずはイメージ:
基本はDecryption Attackが済み、Dec(ct)
が分かる状態からさらに変化させる為に差分をつけ加えるとうまく欲しい平文が復号させることができます。
ですから実装もさっきのやつとだいたい一緒です。
wanted = '{"username": "guest", "expires":"2030-01-07", "is_admin": "true"}' wanted = "This is an IV456" + pad(wanted) wanted_chunck = chunk(wanted,16) # encrypt attackでflagを取る c = ['\x00'*16] for i in range(5,0,-1): b = '' for idx in range(15,-1,-1): #print 16-idx for d in range(256): if i == 5 and idx == 15 and d == 0: continue ip,port = '118.27.6.108',6001 #io = remote(ip,port) s,f = sock(ip,port) #io.readuntil('Here is a sample cookie: ') read_until(f,'Here is a sample cookie: ') sample_cookie = read_until(f).strip() iv,cookie = sample_cookie[:32],sample_cookie[32:] #io.readuntil('?') read_until(f,'?') #print 'cookie length is',len(unhexlify(cookie)) cookie = unhexlify(cookie) iv = unhexlify(iv) #assert len(cookie) % 16 == 0 block = chunk(iv+cookie,16) #assert len(block) == 6 current,prev = block[i],block[i-1] assert len(prev) == 16 and len(current) == 16 x1 = left_pad(chr(d)+b) x2 = left_pad((16-idx-1) * chr(16-idx)) x3 = left_pad(''.join([chr(j) for j in range(16-idx-1,0,-1)])) current = xor(current,c[0]) prev = xor(prev,xor(x1,xor(x2,x3))) #io.sendline(iv+hexlify(prev+current)) s.send(hexlify(block[0]+prev+current)+'\n') #io.readline() read_until(f) #io.readline() read_until(f) #rep = io.readline() rep = read_until(f) #print rep if 'invalid' in rep: #print '[+] nope' continue else: print "0x{:02x}: ".format(d) + "{:02x}".format(16-idx) * (16-idx) b = chr(d) + b if idx == 0: dec = xor(prev,'\x10'*16) pt = xor(dec,block[i-1]) diff = xor(pt,wanted_chunck[i]) c = [diff] + c print xor(xor(diff,block[i-1]),dec) break result = ''.join(c) ip,port = 'localhost',6000 #io = remote(ip,port) s,f = sock(ip,port) #io.readuntil('Here is a sample cookie: ') read_until(f,'Here is a sample cookie: ') sample_cookie = read_until(f).strip() assert len(unhexlify(sample_cookie)) == len(result) print [ord(i) for i in result] last = hexlify(xor(unhexlify(sample_cookie),result)) read_until(f,'?') s.send(last+'\n') while True: print read_until(f)
所感
やはり難しかった、さらにこれをメンバーに説明するのも大変でした、でもいい経験になりました。
リファレンス
Padding Oracle AttackによるCBC modeの暗号文解読と改ざん - security etc...