魚脳の池

CTF:Little Twoos

Padding Oracle系の問題を解いてみた

今週の勉強会は自分が担当だったので、前からずっと取り扱いかったPadding Oracle Attackの問題を繰り出しました。

基本

概略

  • 共通鍵ブロック暗号のCBCモードに対する攻撃手法
  • 14年SSL3.0のPOODLEという脆弱性もこの手法

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にすり替える

イメージとしては:

https://dl.dropboxusercontent.com/s/x77pmkuqk8f7u90/jean_bash.png?

最後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

まずはサーバーコード

github.com

読んでみたら以下のようなものがわかりました:
* cookieusername,is_admin,expiresこの三つから構成されたjson形式
* is_admin==trueexpiresの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された時、元の平文が復元できる

イメージとしては:

https://dl.dropboxusercontent.com/s/ln0h2f6azxpvqyz/decryption.png https://dl.dropboxusercontent.com/s/lvaswift1t5def0/decryption1.png

だんだん復号させた平文を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を実装しました。
まずはイメージ:

https://dl.dropboxusercontent.com/s/6zmzk5pubd05sni/step1.png

https://dl.dropboxusercontent.com/s/p4ftwishchcy51l/step2.png

https://dl.dropboxusercontent.com/s/ny4chb9s3sl23at/step3.png

基本は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...