✔️ Format String Bug
C언어의 다양한 문자열 출력 write, puts, printf 등의 함수가 있는데,
그 중에서 printf는 포맷 스트링(Format String)을 이용해서 다양한 형태로 값을 출력한다.
C언어에는 printf외에도 scanf, fprintf, fscanf, spritnf, sscanf 등이 포맷 스트링을
인자로 사용하는데, 함수 이름이 f로 끝나고 문자열을 다루면 포맷 스트링을
사용할 것이라고 추측해볼 수 있다.
이 함수들은 포맷 스트링을 채울 값을 레지스터나 스택에서 가져오게 되는데,
이들 내부에 포맷 스트링이 필요로 하는 인자의 개수와 함수에 전달된
인자의 개수를 비교하는 루틴이 없어서 포맷 스트링을 입력할 수 있다면
악의적으로 다수의 인자를 요청해 레지스터나 스택의 값을 읽어낼 수 있게 될 것이다.
그리고 다양한 형식지정자를 활용해 원하는 위치의 스택 값을 읽거나
스택에 임의의 값을 쓰는 것이 가능한데 이러한 버그를 포맷 스트링 버그,
Format String Bug(FSB)라고 부른다.
포맷 스트링은 %[parameter][flags][width][.precision][length]type 과 같이
구성된다. 형식 지정자는 10진수 d, 문자열 s, 16진수 x, 문자열 길이 n, 포인터 p
이런식으로 사용이 되고 최소 너비를 지정하는 너비 지정자에는
정수의 값만큼, 인자의 값만큼으로 나누어 지정할 수 있다.
여기서 %n은 코드를 작성한 시점에 완성된 포맷 스트링의 길이를 알 수 없을 때
포맷 스트링의 길이를 코드에 사용해야 한다면 %n을 사용해 해결할 수 있다.
해당 예제에서는 각각 printf의 형식 지정자를 활용한 출력을 보여준다.
다음은 파라미터에 대한 예시인데, 파라미터는 참조할 인자의 인덱스를 지정한다.
해당 필드의 끝은 $로 표시하게 되고 인덱스의 범위를 전달된 인자의 갯수와
비교하지는 않는다. %1에 1, %2에 2가 대입되어 1, 2가 출력된다.
포맷 스트링 버그는 포맷 스트링 함수의 잘못된 사용으로 발생하는 버그를 이루는데
포맷 스트링을 사용자가 입력할 수 있을 때 공격자는 레지스터와 스택을 읽을 수 있고
임의 주소 읽기 및 쓰기를 진행할 수 있다.
실습에서는 auth 변수를 0xff로 덮어쓰는 포맷 스트링을 입력해보는 것이다.
다음과 같이 A로 입력을 받아주면 나머지 공간은 남겨져 있는채로
buf에만 모든 공간이 할당되게 된다.
포맷 스트링을 통해서 버퍼의 시작점을 찾아봤을때 0x41을 넣은지점이 9번째의 %x
지점인 것을 확인할 수 있다. %x를 9번 적어준 다음 %n을 적게되면 %n이 값을 넣을
주소가 주소를 입력한 위치로 정해지게 된다.
이 실습에 조금 삽질을 했는데 그 이유는 non-printable character 때문이다.
공백을 입력하고싶은데 스페이스로 공백을 입력하면 ascii 변환값인 0x20이 들어가서
자꾸 Fail의 늪에 빠져들었다.. 후 그래서 이것저것 검색해보고 찾던 중에
https://dreamhack.io/forum/qna/571 해당 질문에서 답변을 찾을 수 있었다.
(추가 참고: https://shayete.tistory.com/entry/5-Format-String-Attack-FSB)
encode 모드에서 null에 해당하는 0000을 미리 입력한다음에 encode를 풀면
hex가 decode 되는데 까먹고 있다가 이걸로 해당하는 것을 넣어주니 되었다..
9번째 자리부터 입력한 값을 페이로드처럼 덮어씌울수가 있는데,
주소 8자리 + 247만큼에 의해서 값이 255가 되고, 255는 0xff가 되기때문에
Fail이 아닌 Win 이라는 문자열을 출력할 수 있게 된다.
그 다음은 레지스터 및 스택 읽기인데 위에서 삽질을 하면서 %p, %x를 통해
해당 값들을 읽을 수 있고 디버깅 모드를 통해서 rsi, rdx, rcx, r8, r9, rsp+0, rsp+8 ...
값들이 출력되는 것을 확인할 수 있다.
위의 결과에서 rsp부터는 8글자씩 참조하는 것을 확인할 수 있는데,
이를 이용해서 %숫자$s 와 같은 형식으로 해당 주소의 데이터를 재 참조해
읽을 수 있게 된다.
위 코드를 컴파일한 프로그램에서 임의의 주소를 읽을 수 있는 코드이다.
secret이 전역 변수로 설정되어 있는데, secret의 주소를 참조하는 출력문에서
7번째 만큼에서 왼쪽으로 8만큼 정렬한것을 패킹하여 페이로드로 보내면
주소에 담겨있는 문자열이 출력되는것을 확인할 수 있다.
해당 주소를 임의로 읽어온 것이다.
이번에는 임의 주소 읽기와 마찬가지로 포맷 스트링에 임의 주소를 넣고
형식 지정자를 사용하면 해당 주소에 데이터를 쓸 수 있는데,
해당 주소에 31337에 해당하는 값을 넣어줌으로써 secret 전역 변수의
값이 31337 쓰여지는것을 확인할 수 있다.
✔️ Format String Bug (함께)
해당 프로그램에서 포맷 스트링 공격을 통해서 changeme를 1337로 바꾸어
셸을 실행시키는 공격을 진행할 수 있다. 현재 예제에서는 get_string 함수를 통해서
buf에 32바이트 입력을 받고 (=0x20) 입력한 buf를 printf 함수 인자로 사용하기 때문에
해당 과정에서 포맷 스트링 버그 취약점이 발생하게 된다.
해당 프로그램을 익스플로잇하기 위해서는 changeme 주소를 우선 구하는데,
PIE 보호 기법이 적용되어 실행할 때마다 주소값이 바뀌게 될 것이다.
이전 글처럼 PIE base 주소를 먼저 구한다음에 해당 주소에 changeme 주소를 더해
주소를 구한다. 구한 주소를 스택에 저장하고 %n을 통해 값을 조작하여
1337 바이트 문자열로 덮어서 설정하면된다.
해당 프로그램을 디스어셈블하여 printf가 호출되는 오프셋을 보자.
main+72번으로 가게 되면
RSI에는 0x7fffffffe290, RDX에는 0x4, RCX에는 0x7ffff7af2031,
R8에는 0xffff7dcf8c0, R9에는 0x7ffff7fe34c0, RSP에는 "AAAA",
RSP+8에는 0, RSP+16에는 0x0555555554940이 들어있다.
최상단을 보게되면 RSP+16에 저장도니 값과 PIE base 주소 차이는 0x940임을 알 수 있다.
changeme의 오프셋은 다음과 같은데, %8$p로 출력한 주소에서 0x940을 빼고
changme의 오프셋을 더하면 changeme의 주소를 구할 수 있을 것이다.
다음 코드를 통해서 changeme 변수의 주소를 구할 수 있다.
해당 변수에 1337을 쓰기 위해서는 1337 바이트 크기만큼의 문자열을 먼저 출력해야하는데,
위 프로그램에서는 get_string에 0x20으로 길이를 제한하고 있다.
해당 길이를 우회하기 위해 포맷 스트링의 width 속성을 사용할 수 있는데,
width는 출력의 최소 길이를 지정하고 출력할 문자의 길이가 최소 길이보다
작으면 그 만큼의 패딩 문자를 추가하게 된다. %1337c에 대응되는 길이가 1337보다 작으면,
인자를 출력한다음 남은 길이를 공백으로 채우는것이다.
이러한 과정을 통해서 %1337c + %8$n을 페이로드에 작성해주고,
패딩 문자를 추가한다음에 인코딩된 페이로드와 주소를 작성해줄 것이다.
아까 위에서 실습한 것처럼 %1337c%8$n[패딩][changme 주소]을 역처럼 작성해준다.
그러면 정상적으로 셸을 획득할 수 있다.
🔰 퀴즈 - 1,2
✔️ basic_exploitation_002
문제 정보는 다음과 같다.
이 문제는 서버에서 작동하고 있는 서비스(basic_exploitation_002)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.
nx만 적용되어있고 PIE는 적용되어있지 않은 것을 확인할 수 있다.
소스코드를 보게되면 앞서 배운 fsb 취약점(printf(buf))에 관한 문제로 볼 수 있는데,
80만큼의 공간을 받으면서 출력하는 곳에 get_shell 함수를 가리키면서
셸을 실행시키는 방법으로 진행하면 될 것이다. 여기서는 get_shell을 실행하기 위해서
exit가 실행될 때 get_shell 함수 주소를 덮는 과정을 진행하면 될 것 같다.
주소 값이 저장된 공간인 exit@got를 활용해 got overwrite를 활용해보자.
메인과 exit를 디스어셈블하면 다음과 같은 로직으로 되어있는데,
exit got주소와 get_shell의 주소를 추가적으로 확인하여 페이로드에 작성해주자.
그리고 프로그램을 돌려보면 입력한 입력 값을 첫번째 x에서 인자로 받고 있다.
페이로드를 작성해주기 위해서 [exit@got][get_shell 주소 - (앞바이트)4]c%1$n으로 작성해주면 되는데
x24xa0x04x08%131514181(dec)%1$n 처럼 되면 프린트되는 크기가 너무 커서 timeout이 걸리게 된다.
해당 이슈를 해결하기 위해서는
[exit@got+2][exit@got][get_shell 주소 - (앞바이트)8]c%1$hn[get_shell 주소 뒤 두자리 - 8 - get_shell 주소 앞 두자리 -8]c%2$hn과 같이 작성해주게 된다. 그럼 x26xa0x04x08x24xa0x04x08%2044c%1$hn%32261%2$hn이 되는데, 해당 페이로드를 통해서 804, 8609가 작성되어 get_shell 주소에 도달할 수 있게 된다.
다음과 같이 정상적으로 플래그를 획득할 수 있다.
✔️ basic_exploitation_003
문제 정보는 다음과 같다.
이 문제는 서버에서 작동하고 있는 서비스(basic_exploitation_003)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 셸을 획득한 후, “flag” 파일을 읽으세요.
“flag” 파일의 내용을 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{…} 입니다.
코드와 보호기법을 보게되면 nx 적용된 것 말고는 딱히 없다.
코드는 이전처럼 get_shell을 어딘가로 호출해서 셸을 실행시키는 것인데,
이번에는 malloc으로 80만큼 힙 buf가 할당되어 있고,
stack buf는 90만큼이 주어지고 있다. 힙에 있는 문자열에 read로 입력한다음
문자열을 스택에 복사하고 printf로 출력하게 되는데 사이즈 제한이 있는 read가 아닌
sprintf를 통해 fsb 취약점을 활용하면 될 것 같다. 여기서 스택 buf는 문자열을
주는 크기만큼이 입력되기 때문에 오버플로우로 함께 익스플로잇을 시도할 수 있을 것같다.
메인에서 malloc 이후에 ebp-0x8에 힙 문자열 주소값이 들어가고,
ebp-0x98부터 0x24번 만큼(ecx) edi에 0을 쓰는 것으로 보인다.
0x24 * 4만큼을 하면 0x90이 되고 해당 스택 문자열을 초기화하게 된다.
그리고 나서 ebp-98에 위치한 곳에 스택 문자열이 들어갈 것이다.
get_shell 주소는 8048669라는 것을 확인할 수 있다.
여기서 페이로드를 구상해보면 0x98은 152바이트 만큼의 크기를 나타내는데
ret에 get_shell 함수를 덮어서 실행시킬 수 있으므로
ebp-0x98 + sfp + ret에 156바이트 만큼을 더미값으로 채우고 나서
ret에 get_shell 주소를 입력해서 페이로드를 작성해보자.
정상적으로 플래그를 획득할 수 있었다.
화이팅 💪
'CTF > 시스템해킹' 카테고리의 다른 글
[Dreamhack] Double Free Bug (0) | 2022.10.30 |
---|---|
[Dreamhack] Use After Free (0) | 2022.10.29 |
[Dreamhack] Out of bounds (0) | 2022.10.28 |
[Dreamhack] Bypass PIE & RELRO (0) | 2022.10.27 |
[Dreamhack] Bypass NX & ASLR (0) | 2022.10.25 |