일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 유스케이스
- DP
- 게임개발
- STCF
- Rr
- DSP
- sampling theory
- 운영체제
- ret2libc
- Security
- dtft
- 메카님
- linear difference equation
- frequency-domain spectrum analysis
- Race condition
- 언리얼엔진
- TSet
- RBAC
- stride
- 유니티
- Double free
- Unity #Indie Game
- AINCAA
- 게임 개발
- dirty cow
- CTF
- MAC
- MLFQ
- pdlc
- 배경 그림
- Today
- Total
다양한 기록
[모의 해킹] GOT Overwrite with Format String Vulnerability 본문
Linux, GOT overwrite
ELF파일의 다이나믹 링킹과 관련이 있는 내용이다. ELF는 리눅스에서 사용되는 파일 포맷이다. 이 ELF 파일에 .dynamic 섹션이 존재하는데, 여기서 다이나믹 링킹 관련 정보를 관리한다. 다이나믹 링킹 관점에서 보면, 보통 컴퓨터 과학에서 다이나믹(동적)이라는 단어가 나오면, 스태틱(정적)이라는 단어도 같이 나온다. 그리고 스태틱은 런타임 이전에 발생하는 일들에 대해 이야기할 때 쓰고, 다이나믹은 런타임에 발생하는 일들에 대하여 사용한다. 그리고 링킹의 경우는 여러 파일에 나눠진 코드를 하나의 실행 가능한 오브젝트 파일로 합치는 과정을 의미한다. 스태틱 링킹의 경우 실행 가능한 오브젝트 파일을 만드는 경우에, 사용하게 되는 라이브러리 함수들을 그냥 복사해서 가지고 있도록 링킹을 한다. 다이나믹 링킹의 경우엔 사용할 함수의 주소만 미리 가지고 있다가 런타임에 주소로 가서 가져오는 방법이다. 이때, ELF 파일이 다이나믹
스태틱 링킹 시에는 자신 프로그램 내부에 복사해서 함수를 들고 있기 때문에 프로그램 밖의 주소를 참조할 필요는 없으나, 다이나믹 링킹 시에는 프로그램 외부를 참조해야 한다. 이 과정을 위해서는PLT와 GOT의 개념이 중요하다. 각각 Procedure Linkage table, Global Offset Table을 의미하며, 이 두가지 테이블을 이용해 다이나믹 링킹이 이루어진다.
구체적인 개념을 디버깅 통해 알아보고자 한다. 위 이미지는 다이나믹 링킹된 puts 함수를 사용하는 프로그램을 디스어셈블한 것이다. 코드를 보면 <puts@plt> 라는 텍스트를 볼 수 있다. puts라는 라이브러리 함수를 사용함에 따라 PLT를 참조하여 puts()를 실행하겠다는 것이다.
참조하는 위치로 가서(PLT) 디스어셈블 시, 0x22ae 주소가 가리키는 곳으로 점프하는 코드를 볼 수 있다. PLT테이블이 가리키는 장소는 GOT 테이블을 가리키는데 즉, 0x22ae가 가리키는 장소가 puts 함수의 주소가 될 것이다.
그림으로 표현하면 위 이미지와 같다. 다이나믹 링킹된 프로그램은 어떠한 함수를 사용할 때 PLT 를 참조하는데, PLT는 GOT의 특정 위치를 가리키고 GOT의 특정 위치는 함수의 위치를 가리킨다. 디버깅에서 더 자세한 과정까지는 나오지 않았는데(*0x), 중간에 dl_resolve라는 함수가 작동한다. dl_resolve 함수는 첫 호출시에 사용하는데, 처음에는 GOT가 가리키는 곳이 실제 함수의 주소가 아니다. 실제 호출이 될 때 dl_resolve 함수가 함수의 주소를 알아내고 GOT에 저장한다. 그 다음의 호출부터는 dl_resolve를 호출하지 않고 GOT 값을 바로 참조한다. 즉, 첫 함수 실행의 경우 조금 달라진다. 사용하려는 함수의 주소를 GOT에 저장해주어야 하기 때문에 과정이 조금 추가된다고 볼 수 있다.
그렇다면 GOT overwrite란 무엇인지 이제 이해할 수 있다. 이름 처럼 GOT를 overwrite, 즉 덮어쓰기 한다는 뜻인데GOT를 덮어쓰면 어떻게 되는지 이해함으로써 해당 기법을 이해할 수 있다. 또한, GOT 는 data 세그먼트에 존재하기 때문에, RELRO 같은 방어 기법과 연결지어 생각하는 것도 가능하다.
공격 시나리오
우선 GOT overwrite를 하기 위해 어떤 취약점을 악용할 것인지에 대해 결정할 필요성이 있다. 버퍼 오버플로우와 ROP를 이용하거나, 포맷 스트링 취약점을 이용하는 두 가지 방법이 있을 수 있다. 이 과제에서는 포맷 스트링 취약점을 이용하기로 했는데, 그 이유는 일단 메모리 특정 위치에 원하는 값을 쓸 수 있게 되는 취약점이기에 GOT 값을 덮어쓰기에 어떻게 해당 취약점이 악용되는지 이해하기가 비교적 간편했다..
우선, 어떤 입력을 받고 출력하고, 다시 어떤 입력을 받고 출력해주는 프로그램이 있다고 가정할 수 있다. 이때, 출력이 printf(buffer)와 같은 방식으로 이루어진다고 가정할 수 있다. 그렇다면 이제 포맷 스트링 취약점을 이용할 수 있다.
이런 방식으로 입력을 넣을 경우, printf("%p %p %p %p %p") 가 되는데, 어디서 값을 뽑아줄지 지정하지 않은 상태가 되기 때문에 스택에서 값을 뽑아오게 된다. 하지만 만약 %n이라는 값을 쓰게 될 경우, 굉장히 위험한 결과로 이어질 수 있다. %n은 특정 메모리 위치에 출력 값만큼 쓰기를 진행한다. 그리고, 따로 지정되지 않았으면 스택에서 어디에 쓸지 뽑아온다. 중요한 것은, %p로 우선 어디부터 입력하는 인자가 들어가는지 오프셋을 구하고, 해당 오프셋을 기준으로 계산하여 %n으로 쓸 주소를 뽑아오게 만드는 것이다.
이렇게 %p를 이용해 스택에 들어갈 인자의 오프셋을 구하고, %n을 통해 메모리의 특정 위치에 값을 쓸 수 있음을 알았다. 그렇다면 공격용 페이로드는 다음과 같이 작성할 수 있을 것이다. [ 쓰려는바이트$쓰일주소오프셋%n아무값\x??\x??\x??\x?? ] 과 같은 형태를 가지게 될 것이다. 쓰일 주소의 오프셋은 \x??\x??\x??\x이 스택에 존재하는 위치를 가리켜야 하며, 그 위치에 쓰려는 바이트가 작성된다. $20%n같은 형태로 적혀있으면 스택에서 20번째 인자에 적힌 위치에 써지게 된다.
페이로드를 [ 쓰려는바이트$쓰일주소오프셋%n아무값\x??\x??\x??\x?? ]같은 형태로 작성할 수도 있겠지만, python의 pwntools에 스택에 인자가 들어가는 오프셋만 알아내면 포맷 스트링 취약점의 페이로드를 만들어내는 라이브러리 함수가 존재한다. (fmtstr_payload()) 이 과제에서는 포맷 스트링 취약점에 취약한 프로그램 작성하고, 페이로드를 작성하여 printf를 system으로 바꿔치기 할 것이다.
이를 위해서는 공격을 위해 넣는 입력이 스택에 어디에 들어가는지 오프셋을 알아야 한다. 이를 위해 예시 코드를 작성했다.
입력을 받은 다음 printf로 %s 형식을 지정하지 않고 그대로 출력을 하는 포맷 스트링 취약점이 존재하는 코드이다. 이 코드를 32비트로 컴파일했다.
그 다음 위와 같은 입력을 주었다. %p는 주소를 출력하는데, printf("%p")와 같은 형식은 인자가 없으니 대신 스택에서 값을 뽑아오게 된다. 즉, 스택의 내부 내용이 유출되며, 넣었던 입력값이 존재하는 위치 또한 알 수 있게 된다. 위 예시에서는 7번째 인자(최근 들어온게 1번)에서 0x25207025 라는 16진수가 등장한다. 여기서 0x70은 아스키코드로 p를 뜻한다. 즉, 7번째에 입력이 들어간다. %7$p 라는 입력은 오프셋 7 기준으로 스택에서 값을 뽑아오게 되는데 실제로 값이 나타나는 걸 볼 수 있다.
그림으로 나타내면 위 이미지와 같다. 스택에서 가장 최근에 들어간 값이 1이라 했을 때, 7번째 인자에 p가 등장하니 buffer의 시작 위치는 주소 하나의 크기 기준(4바이트) 7만큼의 오프셋을 가진다고 볼 수 있다.
공격
실습 환경
가상화 툴: OrbStack
* macOS용 컨테이너 환경
리눅스 버전: Ubunbtu 24.10 (Oracular Oriole)
컴파일러: gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0
ASLR : 완전히 꺼짐
컴파일 옵션
-z norelro : RELRO 방어기법 해제
-m32 : 32비트로 컴파일
-no-pie : PIE 기법 해제
취약한 예제 코드
지역 변수에 버퍼(buffer)가 존재하여 스택에 위치한다. 해당 버퍼에 표준 입력으로부터 입력을 받아, printf(buffer)와 같은 방식으로 포맷 스트링 취약점을 의도적으로 만들었다. 총 두번의 입력이 있으며, 각각에 대해 두번의 출력이 있다. 첫번째 입력에서 페이로드를 넣고, 첫번째 출력에서 GOT Overwrite가 발생한다. 여기서 printf의 PLT를 따라가서 GOT 엔트리의 주소를 찾아내어 system의 주소로 덮어쓸 수 있다. 그리고, 두번째 입력에서 /bin/sh을 입력하면 print("/bin/sh") 대신 system("/bin/sh")이 실행되어 쉘이 실행될 것이다
공격 코드
우선 공격 코드를 먼저 올리고 설명할 수 있다. 공격 코드는 파이썬(Python 3.12.3) 및 pwntools 모듈을 사용했다. ELF 포맷의 파일인 ./vul을 읽어 프로세스를 실행시키고, 페이로드를 만들어 입력한 다음, /bin/sh 입력을 주어 쉘을 실행시키는 코드이다.
여기서 중요한 것은 fmtstr_payload(offset, {덮어써질 주소: 덮을 값}) 함수이다. 덮어써질 주소는 당연히 printf의 GOT 엔트리 주소이며, 덮을 값은 system의 주소이다. 그리고 오프셋은 해당 페이로드가 들어갈 스택의 오프셋을 의미한다. 정확히는, 스택에 몇 번째에 해당 값이 들어가느냐 이다.
a) printf의 GOT 엔트리 주소 구하기
우선 gdb를 켜고 disas main을 하면 0x8049040이라는 값이 나오는데, 이는 printf에 해당하는 PLT 값이다.
해당 값을 따라가면 0x804b218이라는 값이 나온다. 해당 값이 GOT 엔트리의 주소이다.
b) system
일단 시작을 안한 상태에서는 system의 주소를 알아낼 수 없다.
main을 브레이크 포인트로 잡고 run 한 다음 p system을 하면 system의 주소를 알아낼 수 있다. 그 결과 0xf7dd78f0 라는 값을 얻을 수 있었다.
c) 스택 오프셋 구하기
printf(buffer)에서 포맷 스트링 취약점이 존재한다. 그렇기에 입력을 조절하여 스택의 값을 뽑아올 수 있다. 입력으로 %p %p %p %p %p %p %p %p %p를 주면 스택에서 순서대로 값을 꺼내서 보여준다. 출력을 보면 8번째 값에서 70 이라는 값, 즉 p가 등장한다.
재확인 시에도 8번째에 인자가 들어가 있음을 확인할 수 있다.
이를 구조로 표현 시 다음과 같다.
gdb 상에서 확인 시 다음과 같다.
중간 과정 등에 의해 다른 두 값이 앞에 존재하긴 하지만, 0x0000012c부터 8번째의 자리에 있는 것을 확인할 수 있다.
그렇게 만든 페이로드를 바이너리 파일(payload.bin)로 만들어 xxd로 확인하면 다음과 같다.
* print로 출력 결과
1) %120c%20$hhn%101c%21$hhn%19c%22$hhn%7c%23$hhnaaa
2) \x19\xb2\x04\x08\x1a\xb2\x04\x08\x18\xb2\x04\x08\x1b\xb2\x04\x08
3) printf GOT 엔트리의 주소: 0x0804b218
4) system의 주소: 0xf7dd78f0
10진수 | 16진수 | 덮어쓰는 주소 |
120 | 0x78 | 0x0804b219 |
221 | 0xdd | 0x0804b21a |
240 | 0xf0 | 0x0804b218 |
247 | 0xf7 | 0x0806b21b |
우선 첫번째로 1) 번을 참고하면 %120c%20$hhn 이라는 부분을 확인할 수 있다. 출력은 %120c에 의해서 120만큼의 출력이 있다. %20$hhn은 스택의 20번째 값이 가리키는 주소에 1바이트 만큼 (half half n) 출력된 값만큼 작성되게 된다. 즉 0x0804b219에 0x78이라는 값이 새겨지게 된다.
두번째로는 %120c%20$hhn%101c%21까지 확장해서 볼 수 있다. 앞에서 120만큼의 출력이 있었고 그 후 %101c 만큼 출력되니 221의 출력이 발생한다. 그리고 스택의 21번째 값이 가리키는, 0x0804b21a에 221의 16진수 값 0xdd가 새겨진다.
세번째로는 %19c%22$hhn라는 값을 확인할 수 있다. 앞에서 221의 출력이 있었기에 현재 총 출력은 240이 되고, 16진수로 0xf0이다. 이 값을 스택의 22번째 값이 가리키는 0x0804b21a에 0xf0가 작성된다.
네번째로는 %7c%23$hhnaaa를 확인할 수 있다. 앞에서 240 출력이 있었으니 총 출력이 247이 되고, 이는 16진수로 0xf7이다. 앞에서의 과정들과 같이, 스택의 23번째 값이 가리키는 0x0806b21b에 0xf7이 작성된다. aaa는 오프셋을 맞추기 위한 쓰레기 값이다.
추가적으로 확인이 필요한 것은, 어째서 %20$hhn처럼 스택의 위치를 계산할 수 있냐는 것이다. 이를 알기 위해서 페이로드가 들어가는 buffer의 스택 offset이 필요했다. 맨 처음, 00000000을 스택의 8번째라 보면, 4바이트 단위로 offset을 하나씩 증가시켜 보면 덮어쓰려는 주소가 스택의 어디에 들어가는지 계산이 가능하다
4바이트 단위로 나눠서 표시하면 위 이미지와 같다. 첫번째 인자를 8로 시작한 경우, 20번째, 21번째, 22번째, 23번쨰 값이 각각 덮어써야 할 주소를 나타내게 된다. 총 4바이트를 %hhn으로 1바이트씩 덮어쓰기 때문에 총 네 개의 덮어쓸 주소가 존재하며, aaa의 값은 오프셋을 맞추기 위해 들어간 값임을 확인 가능하다.
그렇게 작성된 값이, 예제로 만든 취약한 프로세스로 전달되고, 두번째 입력으로 /bin/sh을 보내면 system("bin/sh")이 실행되어 쉘을 탈취할 수 있다. p.interactive의 경우엔 만든 프로세스와 상호작용하기 위해 쓰는 함수인데, 쉘을 탈취하고 해당 쉘과 상호작용 하기 위해 필요하다.
공격 코드(exploit_format.py) 실행 후 쉘 탈취에 성공한 것을 확인할 수 있었다.
방어 기법
GOT 오버라이트는 다른 취약점(ex. 포맷 스트링 취약점, 버퍼 오버플로우 취약점)과 결합하는 방식으로 쉘이 탈취되는 등 굉장히 위험한 결과를 초래할 수 있다. 그렇기에 막을 수 있는 방식이 다양하게 존재하며, 대표적인 방법들이 바로 ASLR, PIE, RELRO 세가지이다. 세가지 모두 이번 실습에서는 일부러 꺼둔 채로 진행하였다.
첫번쨰로, ASLR이다. 공격 코드를 보면 메모리 주소를 랜덤으로 배치하기 때문에 덮어써야 하는 위치나, 덮어쓸 주소같은 요소들이 랜덤화 되기 때문에 이번에 만든 공격 코드와 같은 방식으로 공격하기는 굉장히 어려워진다.
두번째로는 PIE이다. PIE는 Position Independent Executable인데, 메모리의 어떠한, 특정한 주소에 고정되어(종속되어) 실행되지 않도록 하는 기법이다. 이때문에 코드 위치가 매번 바뀌는게 가능해지므로, 공격자가 어느 주소를 공격해야 하는지 찾을 수 없어진다.
세번째는 RELRO이다. ELF 파일에서 GOT 오버라이트를 막기 위해 매우 중요한 기법인데, 메모리의 특정 부분을 읽기만 가능하도록 하고 쓰지 못하도록 막기 위해 사용된다. 이 옵션이 켜져 있으면 GOT overwrite가 불가능해진다. 두가지 옵션이 존재하는데 Partial RELRO, Full RELRO가 있는데, GOT 영역을 완전히 읽기 전용으로 하고 싶다면 Full RELRO를 적용해주어야 한다.
가장 중요한 점 중 하나는, 애초에 코드를 안전하게 작성하는 것이다. 이번 예제 코드에서처럼 printf(buffer)처럼 포맷 스트링 취약점이 있거나, 혹은 버퍼 크기를 제대로 검증하지 않아서 버퍼 오버플로우 취약점이 존재하는 문제가 발생하지 않도록, 위의 방어기법들을 사용하는 것과 더불어, 처음 코드를 작성할 때 부터 안전하게 코드를 작성하는 것이 중요하다.
예제 코드로 테스트를 해 보면, ASLR, RELRO, PIE 셋 중 하나라도 적용되어 있는 경우엔 공격이 실패하는 것을 확인할 수 있다. 또한, printf("%s", buffer)처럼 포맷 스트링 취약점에 취약하지 않은 형태로 코드를 작성해도 공격이 실패한다. 위에서 언급된 방어 방법들이 모두 굉장히 효과적임을 알 수 있다.
'운영체제보안' 카테고리의 다른 글
Access Control (0) | 2024.12.09 |
---|---|
Dirty COW #4 완화법 (0) | 2024.12.06 |
Dirty COW #3 Exploit (0) | 2024.12.06 |
Dirty COW #2 mmap(), madvise() (0) | 2024.12.06 |
Dirty COW #1 Copy-on-Write (0) | 2024.12.06 |