일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 안드로이드
- tabbar
- LifeCycle
- drift
- CustomScrollView
- data
- 테스트
- Kotlin
- binding
- Flutter
- DART
- textview
- appbar
- Navigation
- 앱
- livedata
- viewmodel
- Button
- TEST
- scroll
- activity
- android
- Coroutines
- Compose
- ScrollView
- Dialog
- textfield
- intent
- 계측
- 앱바
- Today
- Total
Study Record
[리버싱 기초개념] 포맷 스트링 공격 구문 본문
포맷 스트링 개요 및 메모리 값 바꾸기
1. 포맷 스트링 개요
※ 포맷 스트링?
일반적으로 사용로부터 입력을 받아들이거나 결과를 출력하기 위하여 사용하는 형식이다.
매개변수 | 형식 | 매개변수 | 형식 |
%d | 정수형 10진수 상수(integer) | %c | 문자 값(char) |
%f | 실수형 상수(float) | %s | 문자열 |
%lf | 실수형 상수(double) | %o | 8진수 양의 정수 |
%n | * int(총 바이트 수) - 이전까지 쓴 문자열의 바이트 수 |
%u | 10진수 양의 정수 |
%hn | %n의 반인 2바이트 단위 | %x | 16진수 양의 정수 |
포맷 스트링 형식 중 %x 는 버그가 있는 함수와 사용됐을 때 예상치 못한 동작을 하는 경우가 있다.
바로 printf("%x") 이다. 다음과 같은 코드(format.c)가 있다고 해보자.
#include <stdio.h>
// vi format.c
// gcc -mpreferred-stack-boundary=2 -o format format.c
int main(int argc, char *argv[])
{
int a=10;
char *RokHacker="I am ROKHacker!";
char *SuperUser="I am SuperUser!";
printf(argv[1]);
printf("\n");
}
디버거(gdb)를 이용해 format 프로그램을 분석하여 스택의 값들을 알아보기 전에 프로그램이 실행되면 [그림1] 과 같이 SFP(이전 프로그램에서 사용됐던 ebp값), RET(돌아갈 주소값), argc, argv*, env* 값들이 기본적으로 들어있다.
디버거를 사용하여 format "%8x %8x %8x %8x %8x %8x %8x %8x" 프로그램에서 printf(argv[1]); 을 실행하기 바로 전의 스택의 내용들을 분석해보았다.
이것을 보기 좋게 정리하면 [그림2]와 같다.
format "%8x %8x %8x %8x %8x %8x %8x %8x" 의 실행결과는 다음과 같다.
결과를 보니 이상한 값들이 출력됐는데 [그림2] 와 비교하면 스택에 들어있던 값들이 출력된 것을 알 수 있다!
2. 메모리 값 바꾸기
그렇다면 이걸 알았다고 아직 할 수 있는 것은 없다. 여기서 "%n" 이라는 포맷 스트링을 사용해볼 수 있다.
%n 포맷 스트링은 지정자 앞에 쓰인 문자의 수를 %n 지정자에 표기된 바이트 수만큼 이동한 메모리에 있는 주소에 값을 쓴다. 다음과 같은 예시를 보자.
#include <stdio.h>
main()
{
int i=1;
printf("i's address: %x\n", &i);
printf("i's value: %d\n", i);
printf("test%n\n", &i);
printf("chaged i's value: %d\n", i);
}
printf("test%n\n", &i); 에서 변수 i에 %n 지정자 앞에 표기된 바이트 수(4)가 저장된다. 결과를 보면 다음과 같다.
※ printf("%5c", k) → 1byte이 아니라 5byte을 만들어서 한쪽에 변수 k 값을 넣는다. 따라서 printf("%100c%n", k, &i) 는 i = 100 와 같다.
다음은 a.out 코드 부분이다.
#include <stdio.h>
// vi test.c
// gcc -mpreferred-stack-boundary=2 -o a.out test.c
main() {
int i = 4660;
char a[8];
printf("string input : ");
fgets(a, 100, stdin);
printf("i : %p value : %d\n", &i, i);
printf("a : %s\n", a);
}
여기서 fgets 함수는 표준 입력(stdin)에서 최대 100바이트만큼 a 배열에 쓰기를 할 수 있다. 하지만 배열 a의 크기는 8바이트이므로 a배열 바로 뒤에 있던 변수 i 까지 영향을 미치게 될 것이다. 다음은 표준 입력으로 "AAAAAAAAAB"를 입력한 뒤 스택의 값을 출력해본 결과이다.
"A"는 아스키값에 의해 0x41 이고 "B"는 0x42, "\n"는 0x0a 이다. char a 배열이 공간이 8바이트밖에 없기 때문에 나머지 "AB\n"부분이 변수 int i 영향을 준 모습을 볼 수 있다. 여기서 의문점이 드는건 "AB\n"를 입력했는데 실제로 변수 int i 의 값은 "\nBA"(0x0a4241) 순으로 읽혀졌다는 것이다. 메모리 주소 상으로 봤을 때 char a 배열의 바로 다음 주소인 0xbffffad4 에 'A' 가 들어갔고 그 다음번지인 0xbffffad5 에 'B' , 0xbffffad6 에는 '\n' 값이 들어갔다. 이렇게 반대로 읽혀지는 이유는 리틀 엔디안 방식을 사용하고 있기 때문이다.
그렇다면 어떤 변수에 주소값 0xbfffff13 을 넣는 작업도 할 수 있을 것이다. 0xbffffff13 은 10진수로 바꿀 경우 너무 큰 수라 범위를 초과하여 인식하지 못한다. 따라서, 0xbfff 와 0xff13 을 따로 넣어야 할 것이다. 다음의 예시는 변수 i값을 0xbfffff13 바꾸는 예시이다. 여기서 알아두어야 할 것은 리틀 엔디안 방식을 따르기 때문에 0xbfffff13 의 값은 0xff13(2byte) , 0xbfff(2byte) 의 순서로 넣어야 할 것이다. (0xbfff = 49151(10진수) , 0xff13 = 65299(10진수))
#include <stdio.h>
main()
{
long i=0x00000014, j=1, *k;
printf("i's address : %x\n",&i);
+printf("i's value : %x\n",i);
k=&i;
printf("%65299d%n%49388d%n\n",j,k,j,k+2); // 49388 = 1bfff - ff13 십진수로 표기한값
printf("chaged i's value : %x\n",i);
}
※ K 에는 0xff13(65299) 값이 들어가고 K+2 에는 %n 지정자 앞까지 표기된 바이트의 수이므로 (65299 + 49388 = 114687 = 0x1bfff) 에서 1은 버려져 0xbfff가 저장된다. 실제로 코드를 실행하면 세그먼트 폴트(segment fault)오류가 나오게 된다.
다음과 같은 소스 코드를 갖는 format3 프로그램이 있다고 하자.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
static int i=0;
char str[128];
strcpy(str, argv[1]);
printf(str);
printf("\n i=%p, i=%d\n", &i, i);
}
# ./format3 "AAAA %x %x %x %x"
위와 같이 실행했을 때 i의 주소값은 0x8049484 이고, strcpy() 명령어로 str 배열에는 "AAAA %x %x %x %x %x %x %x"가 들어가있을 것이다. A는 아스키코드값으로 0x41이기 때문에 4번째 %x 에서 str 배열 의 값이 출력되는 것을 알았다. 즉, 4번째 %x 부터 str 배열을 출력하기 시작했다.
만약 ./format3의 인자를 "AAAA%8x%8x%8x%n" 으로 입력한다면, "%8x%8x%8x" 을 출력하는데 스택포인터가 이동할 것이고 "%n" 에 해당하는 동작을 하기위해 현재 스택 포인터(SP)가 가리키는 곳에 저장된 문자열인 "AAAA"의 16진수 값인 0x41414141에 해당하는 주소에 strlen(AAAA%8x%8x%8x)의 결과에 해당하는 0x1c(28)을 쓰려고 할 것이다.
※ ./format3 "AAAA %x %x %x %x" 를 실행했을 때, %x 의 동작을 실행시키기 위해 현재 스택 포인터가 가리키는 값인 bffffc22 가 출력될 것이고 스택 포인터는 다음을 가리킬 것이다. 그 다음 %x 의 동작을 실행시키기 위해 현재 스택 포인터가 가리키는 값인 0 이 출력될 것이다. 이런식으로 4번째 %x 에서의 스택 포인트는 str 배열을 가리키는 주소였기 때문에 str 배열의 내용이 출력되었던 것이다.
그렇다면 format3에서 변수 i의 값을 120으로 바꾸려면 어떻게 해야할까?
char str 배열의 처음 4바이트를 i의 주소값으로 바꾸고 %n기호를 사용하면 바꿀 수 있을 것 같다.
i의 주소값인 0x8049484를 그대로 str에 입력하기 위해 쉘에서 $(printf "\x84\x94\x04\x08") 을 입력하면 된다.
./format3 $(printf "\x84\x94\x04\x08")%8x%8x%8x%8x 를 실행해보자.
실행결과 4번째 %8x 즉 str 배열에 i의 주소값이 잘 들어간 것을 볼 수 있다.
이제 ./format3 $(printf "\x84\x94\x04\x08")%8x%8x%100x%n 을 실행하면 i의 값은 120(4+8+8+100)이 된다.
※ ./format3 $(printf "\x84\x94\x04\x08")%8x%8x%8x%8x 에서 %8x 의 출력결과를 보면 4번째 %8x 에서 str 배열의 메모리의 처음값(8049484)이 출력되는 것을 볼 수 있다. 이것은 곧 4번째 포맷 스트링의 입력값은 str 배열의 메모리의 처음값(8049484)이라는 의미가 된다. 따라서 ./format3 $(printf "\x84\x94\x04\x08")%8x%8x%100x%n 의 결과값은 4번째 포맷 스트링의 입력값은 무조건 str 배열의 메모리의 처음값(8049484)이기 때문에 포맷 스트링이 %8x 면 16진수로 이 값을 출력해주고 %n 일 경우 str 배열의 메모리의 처음값(8049484)에 해당하는 주소에 %n 지정자 앞까지 표기된 바이트의 수(120)을 쓸 것이다.
조금 더 응용하여, ./format3 $(printf "\x41\x41\x41\x41\x84\x94\x04\x08")%8x%8x%8x%8x%8x 과 같이 실행했다고 해보자.
# ./format3 $(printf "\x41\x41\x41\x41\x84\x94\x04\x08")%8x%8x%8x%8x%8x
이번에는 앞에 AAAA(\x41\x41\x41\x41)를 붙여서 실행해본 결과 똑같이 4번째 %8x 의 입력값은 str배열의 메모리의 처음 값(41414141)이다. 이 형식에서 i에 120을 넣으려고 한다면 5번째 포맷 스트링 입력값이 i의 주소값(0x804984)이므로 ./format3 $(printf "\x41\x41\x41\x41\x84\x94\x04\x08")%8x%8x%8x%88x%n 로 실행하면 된다.
(printf 결과 8byte + 8 + 8 + 8 + 8 + 88 = 120)
+ 포맷 스트링 취약점 여부 판단
segment fault : 잘못된 주소 포인트할 경우 생기는 오류로 공격자가 RET 값 변조 여부를 판단할 수 있다.
포맷 스트링 매개변수 중 %x 를 입력했을 때, "%x" 문자열이 그대로 출력되는 것이 아니라 스택 주소값이 출력되면 포맷 스트링 취약점이 있다.
+ 아스키코드값 확인하기
# man ascii (필요하다면, export LANG=C)
'리버싱 > 기초 개념' 카테고리의 다른 글
[리버싱 기초개념] 메모리 보호기법 (0) | 2021.11.18 |
---|---|
[리버싱 기초개념] 공유메모리 (0) | 2021.11.17 |
[리버싱 기초개념] 버퍼 오버플로우 (0) | 2021.11.16 |
[리버싱 기초개념] 진법 전환/John the ripper(암호 크랙) (0) | 2021.11.16 |
[리버싱 기초개념] 레이스 컨디션/signal (0) | 2021.11.15 |