https://dreamhack.io/lecture/roadmaps/2
System Hacking
시스템 해킹을 공부하기 위한 로드맵입니다.
dreamhack.io
[ Pwnable: stage 10 Format String Bug ]
* 포맷 스트링
- 예: printf, scanf, fprintf, fscanf, sprint, sscanf 등
- 위의 함수들은 포맷 스트링을 채울 값들을 레지스터나 스택에서 가져옴
→ 그러나 해당 함수 내부에는 포맷 스트링이 필요로 하는 인자의 개수와 함수에 전달된 인자의 개수를 비교하는 루틴이 없음.
→ 따라서 사용자가 포맷 스트링을 입력할 수 있다면, 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽어낼 수 있음
→ 다양한 형식지정자를 활용하여 원하는 위치의 스택 값을 읽거나, 스택에 임의 값을 쓰는 것도 가능
→ 포맷 스트링 버그(Format String Bug, FSB)
* 포맷 스트링의 구조
%[parameter][flags][width][.precision][length]type
→ 형식 지정자(specifier)
: 인자를 어떻게 사용할지 지정
형식 지정자 | 설명 |
d | 부호 있는 10진수 정수 |
s | 문자열 |
x | 부호 없는 16진수 정수 |
n | 인자에 현재까지 사용된 문자열의 길이를 저장 |
p | void형 포인터 |
→ 최소 너비(width)
: 최소 너비를 지정
: 치환되는 문자열이 이 값보다 짧을 경우, 공백문자를 패딩 해줌
너비 지정자 | 설명 |
정수 | 정수의 값 만큼을 최소 너비로 지정 |
* | 인자의 값 만큼을 최소 너비로 지정 |
→ parameter
: 참조할 인자의 인덱스 지정
: 이 필드의 끝은 $로 표기
: 인덱스의 범위를 전달된 인자의 개수와 비교하지 않음
* 포맷 스트링 버그(Format String Bug, FSB)
: 포맷 스트링 함수의 잘못된 사용으로 발생하는 버그
: 포맷 스트링을 사용자가 입력할 수 있을 때, 공격자는 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기를 할 수 있음.
[figure 4. 포맷 스트링 버그 실습]
일단 %p를 이용해서 auth의 주소를 확인해보았다. Fsb_auth.c에서 auth에 0x42424242를 넣었으므로, Rsp+12에 위치해 있는 것을 알 수 있다.
Auth의 주소 = 7FFF FFFF DDDC
[exploit tech: format string bug]
→ 포맷 스트링 버그는 포맷스트링을 사용하는 모든 함수에서 발생할 수 있음.
먼저
강의에서 나온 코드를 복사해서 checksec을 돌려보았다.
그런데 강의에서는 canary found라고 표시되었는데 나는 canary가 없다고 한다
우분투 gcc는 기본적으로 카나리 적용한다면서 왜 안 해줄까?
계속해봐도 카나리 설정이 안 되길래 그냥 진행했다.
이제 코드를 확인해보자.
main함수 while 문에서 get_string으로 입력받은 0x20의 buf 값을
아래에서 printf의 인자로 사용하고 있으며, 해당 부분에서 포맷 스트링 취약점이 생길 것이다.
그리고 코드를 보니 changeme 변수가 1337이면 쉘을 탈취할 수 있다고 되어있다.
결국 changeme의 주소를 구하고, 포맷스트링 취약점을 사용해서 changeme를 1337로 설정해주면 쉘을 탈취할 수 있을 것 같다.
그럼 이제 changeme의 주소를 구해야 하는데, 일단 강의를 따라가 보았다.
위에서 printf 부분에 bp를 걸고, 강의랑 똑같이 12345678을 입력값으로 줬다.
그럼 위와 같은 위치에서 멈추는데
Stack 부분에서 rsp+16에 저장된 0x555555554940이(강의랑 다르다) 코드 영역에 포함되니까 이 주소를 사용해서 PIE 베이스 주소를 구할 수 있다고 한다.
이어서 진행을 하면, printf 함수는 “rdi”에 포맷 스트링을 전달하고, “rsi, rdx, rcx, r8, r9, 스택”에 포맷 스트링의 인자를 전달한다고 한다.
나도 똑같이 정리해보면
RSI 0x7fffffffdfb0 ◂— '12345678'
RDX 0x8
RCX 0x7ffff7af2031 (read+17) ◂— cmp rax, -0x1000 /* 'H=' */
R8 0x7ffff7dcf8c0 (_IO_stdfile_1_lock) ◂— 0x0
R9 0x7ffff7fe14c0 ◂— 0x7ffff7fe14c0
[RSP] ‘12345678’
[RSP+8] 0X0
[RSP+16] 0x555555554940
이렇게 된다.
이제 vmap으로 rsp+16에 저장된 값과 PIE 베이스 주소의 기준 주소와의 차이가 0X940인 것을 알 수 있다.
이제 changeme의 주소를 구해야 하는데
[%8$p로 출력한 주소] – [0x940] + [changeme의 오프셋] = [changeme의 주소]
라고 한다.
Changeme의 오프셋은 다음과 같고
이렇게 되니까 계산을 해보면
말도 안되는 값이 나온다...?
그냥 이어서 pwntools 스크립트를 작성해보자!!
진짜 하나도 안 맞는다. 왜 이럴까?
일단 changeme의 주소를 구했으니까, changeme에 1337을 넣어주면 될 것 같다.
1337바이트 길이의 문자열을 출력하고 %n을 사용해서 출력된 문자열의 길이를 인자에 저장하는 방식으로 익스플로잇을 진행한다.
그런데 위의 c 코드에서 buf 값을 0x20만큼 입력받으니 1337개를 입력할 수 없다. 이럴 때 포맷스트링의 width 속성을 사용할 수 있다고 한다.
Width 속성은 출력의 최소 길이를 지정하고, 출력할 문자의 길이가 최소 길이보다 작으면 그만큼 패딩 문자를 추가해준다고 한다.
그래서 총 페이로드를 정리해보면 먼저
1. %1337c: 1337을 최소 길이로 지정하는 문자를 출력
2. %8$n: 현재까지 사용된 문자열의 길이(1337)를 8번째 인자(changeme의 주소)에 작성
3. AAAAAA: 8의 배수를 위한 패딩
4. payload.encode()+p64(changeme): 페이로드 뒤에 changeme 변수의 주소 작성
이렇게 된다고 한다.
이렇게 된다!
'Pwnable > 개념' 카테고리의 다른 글
[Pwnable] Dreamhack STAGE 11 (0) | 2022.08.26 |
---|---|
[Pwnable] Dreamhack STAGE 8 (0) | 2022.08.26 |
[Pwnable] Dreamhack STAGE 8 (0) | 2022.08.26 |
[Pwnable] Dreamhack STAGE 7 (0) | 2022.08.26 |
[Pwnable] Dreamhack STAGE 6 (0) | 2022.08.25 |