2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ApplibotAdvent Calendar 2021

Day 22

GoでPadding Oracle Attackしたい!

Posted at

Applibot Advent Calendar 2021 」 22日目の記事になります。
前日は @ref3000 さんの Unity でサクッと機械学習を体験してみよう【ML-Agents】という記事でした。

はじめに

業務で暗号化に興味を持ってパディングオラクル攻撃という名前に惹かれたので今回はGo言語でパディングオラクル攻撃をやっていきます。

AES CBCモードについて

今回のパディングオラクル攻撃をする対象の暗号文はAESのCBCモードを使用して暗号化しました。AESは平文をブロック長ごとに暗号化するブロック暗号で、ブロック長が128bit、鍵長が128/192/256bitの3種類です。

CBCモードは一つ前の暗号文ブロックと平文ブロックをXORとってから暗号化する方法で前のブロックに依存するため同じ平文であっても異なる暗号文になるため推測されにくいという特徴があります。
先頭の平文ブロックの前には暗号文ブロックがないので初期化ベクトルを用いてXORとり暗号化します。

cbc.png

AESはブロック長が128bit(16Byte)のブロック暗号なので平文も16Byteの整数倍でないといけません、そのため足りない分を埋めるためにPKCS#7パディングという方式がよく使われます。
PKCS#7はパディング長を値として使います。パディング長が1Byteの場合は01、2Byteの場合は02 02というようになります。

pkcs7.png

Go言語によるCBCモード暗号化

package main

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"log"

	"golang.org/x/crypto/scrypt"
)

// pad PKCS#7パディング
func pad(data []byte) []byte {
	padSize := aes.BlockSize - (len(data) % aes.BlockSize)
	return append(data, bytes.Repeat([]byte{byte(padSize)}, padSize)...)
}

func main() {
	// 鍵を生成
	key, err := scrypt.Key([]byte("password"), []byte("salt"), 32768, 8, 1, 32)
	if err != nil {
		log.Fatal(err)
	}
	// パディング済み平文
	plainText := pad([]byte("平文"))
	cipherText := make([]byte, aes.BlockSize+len(plainText))
	// 先頭16Byteは初期化ベクトルに使う
	iv := cipherText[:aes.BlockSize]
	// 初期化ベクトルを乱数で初期化
	if _, err = rand.Read(iv); err != nil {
		log.Fatal(err)
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		log.Fatal(err)
	}
	mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(cipherText[aes.BlockSize:], plainText)
	// 暗号文をBase64エンコード
	cipherTextBase64 := base64.StdEncoding.EncodeToString(cipherText)

	fmt.Println(cipherTextBase64)
}

Go言語によるCBCモード復号化

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"fmt"
	"log"

	"golang.org/x/crypto/scrypt"
)

func unpad(data []byte) []byte {
	padSize := int(data[len(data)-1])
	return data[:len(data)-padSize]
}

func main() {
	// Base64デコード
	cipherText, err := base64.StdEncoding.DecodeString("6Zcc5fKwbDAwgZ4W5ubcRP+egspMunkjCV+RJEYpKA8=")
	if err != nil {
		log.Fatal(err)
	}
	// 鍵を生成
	key, err := scrypt.Key([]byte("password"), []byte("salt"), 32768, 8, 1, 32)
	if err != nil {
		log.Fatal(err)
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		log.Fatal(err)
	}
	// 先頭16Byteから初期化ベクトルを取り出す
	iv := cipherText[:aes.BlockSize]
	cipherText = cipherText[aes.BlockSize:]
	plainText := make([]byte, len(cipherText))

	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(plainText, cipherText)

	fmt.Println(string(unpad(plainText)))
}

Padding Oracle Attack(パディングオラクル攻撃)とは

CBCモードの平文nは暗号文'n-1 ⊕ 平文'n ⊕ 暗号文n-1で求められるので攻撃者はサーバーに暗号文'を送信してパディングが不正かどうかエラーを見ながら正しいパディング(最終的には16 16...16になる)ような暗号文'を見つけます。それによって攻撃者は鍵なしで暗号文を復号することができます。
このようにパディングが正しいかどうかの情報を返すことで、その情報をもとに攻撃者が暗号文を復号、改ざんすることをパディングオラクル攻撃といいます。

Go言語によるPadding Oracle Attack

サーバーの代わりにPKCS#7パディングを検証するvalidatePadding 関数を使って鍵なしで暗号文を復号します

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/base64"
	"fmt"
	"log"
	"math"

	"golang.org/x/crypto/scrypt"
)

func main() {
	base64CipherText := "4oiepBFtjdj7HkHjUSLA746350IiDBHakWKQZ6Ldb+iN7RIW1ypRmETQ178JyUG2"
	cipherText, err := base64.StdEncoding.DecodeString(base64CipherText)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(cipherText)
	blockLen := len(cipherText) / aes.BlockSize
	plainText := make([]byte, len(cipherText)-aes.BlockSize)

	for blockIndex := 0; blockIndex < blockLen-1; blockIndex++ {
		copyCipherText := make([]byte, (blockIndex+2)*aes.BlockSize)
		copy(copyCipherText, cipherText)
		cipherBlock := copyCipherText[blockIndex*aes.BlockSize : (blockIndex+1)*aes.BlockSize]
		for i := 0; i < aes.BlockSize; i++ {
			cipherBlock[i] = 0
		}
		for i := 0; i < aes.BlockSize; i++ {
			for j := 0; j < math.MaxUint8; j++ {
				cipherBlock[aes.BlockSize-(i+1)] = byte(j)
				fmt.Println(copyCipherText)
				var ok bool
				ok, err = validatePadding(copyCipherText)
				if err != nil {
					log.Fatal(err)
				}
				if !ok {
					continue
				}
				break
			}
			if i+1 == aes.BlockSize {
				break
			}
			for k := 0; k < i+1; k++ {
				cipherBlock[aes.BlockSize-(k+1)] ^= byte(i+1) ^ byte(i+2)
			}
		}
		for i := 0; i < aes.BlockSize; i++ {
			plainText[blockIndex*aes.BlockSize+i] = copyCipherText[blockIndex*aes.BlockSize+i] ^ aes.BlockSize ^ cipherText[blockIndex*aes.BlockSize+i]
		}
	}
	fmt.Println(string(unpad(plainText)))
}

func unpad(data []byte) []byte {
	padSize := int(data[len(data)-1])
	return data[:len(data)-padSize]
}

func validatePadding(cipherText []byte) (bool, error) {
	key, err := scrypt.Key([]byte("applibot"), []byte(""), 32768, 8, 1, 32)
	if err != nil {
		log.Fatal(err)
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return false, err
	}
	iv := cipherText[:aes.BlockSize]
	cipherText = cipherText[aes.BlockSize:]
	plainText := make([]byte, len(cipherText))

	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(plainText, cipherText)

	padding := int(plainText[len(plainText)-1])
	if !(0 < padding && padding <= aes.BlockSize) {
		return false, nil
	}
	idx := len(plainText) - padding
	if idx < 0 || len(plainText) < idx {
		return false, nil
	}
	for _, p := range plainText[len(plainText)-padding:] {
		if int(p) != padding {
			return false, nil
		}
	}
	return true, nil
}

おわりに

実行に時間がかかりますが、試してみて復号結果見てもらえると嬉しいです!

標準パッケージを使うことで簡単に暗号化できる反面、適切に実装しないと今回のように攻撃できてしまうことがわかりました。
今回、暗号と攻撃について興味を持ったのでもっと深く学んでいきたいと思います!

以上、「 Applibot Advent Calendar 2021 」 22日目の記事でした!

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?