2021. 6. 14. 23:18ㆍLayer7/Layer7_System Hacking
Buffer Overflow(BOF)란
메모리 공간에 저장되는 모든 데이터가 특정한 길이를 가지고 있고, 그 크기를 기준으로 운영체제가 데이터를 판별한다.
데이터들은 계속 이어져 있으므로 입력값의 크기를 제한하지 않는 취약점을 가지고 있는 함수(scanf, gets, sprintf, strcpy 등)를 통해 우리가 입력한 데이터를 원래 저장되어야 하는 데이터보다 더 많이 입력할 수 있다. 이로써 우리가 입력한 내용은 원래 데이터가 저장되어야 하는 메모리 공간뿐만 아니라 해당 메모리 공간을 넘어서 그 이후의 영역까지도 우리의 입력이 들어가게 된다. 즉, 입력의 크기를 제한하지 않음으로써 우리의 입력으로 뒤에 오는 데이터들을 조작할 수 있게 되고, 프로그램의 실행 결과에 영향을 미칠 수 있는 요소들까지 모두 덮어씌울 수 있어 매우 위험한 취약점이다. 일반적으로 변경이 불가능하거나 접근이 제한되어 있는 메모리 영역까지도 침범할 수 있게 되어 프로그램의 결과에 예상 외의 일들이 벌어질 수 있게 된다.
버퍼가 넘쳐흐른다, 즉 운영체제에서 지정하여 할당된 메모리 영역의 크기를 벗어나 다른 데이터를 저장하는 메모리 공간까지 침범해서 데이터를 의도적으로 그리고 악의적으로 바꿀 수 있게 되는 시스템 해킹 공격이 버퍼 오버플로우 공격(Buffer Overflow, BOF)이다. 이와 같이 메모리의 값을 조작하거나 침범하는 취약점들을 통틀어서 Memory Corruption라고도 한다.
보통 BOF의 경우에는 어셈블리의 RIP 레지스터를 조작하는 경우가 많다. 그리고 함수가 종료될 때 그 함수가 실행되었던 곳으로 돌아가기 위한 Return Address를 조작할 수도 있어 원래 돌아가야 할 위치가 아닌 아예 다른 함수를 호출한다거나, 쉘 코드와 같이 시스템을 장악할 수 있는 코드들을 메모리에 미리 삽입하여 그 코드의 주소로 이동하여 실행시키게 만든다던가 등 프로그램이 실행되는 흐름을 조작할 수 있다.
HACKCTF "Basic_BOF #1" Write-Up
IDA를 사용하여 주어진 ELF 파일의 main 함수를 디컴파일한 결과이다.
프로그램의 전체적인 흐름을 보자면 먼저 char형 변수 (배열로 예상됨) s와 int형 변수 v5를 선언하였고, v5 변수를 특정 값으로 설정하였다. 그다음 fgets()라는 함수를 이용하여 s라는 변수에 문자를 입력받는다. 이때 원래 배열의 크기인 38B만큼만 입력받아야 하는데 fgets()의 인자로 45를 주어 실제로 45B만큼의 입력을 받는다. 따라서 이 과정에서 입력한 데이터가 실제로 38B보다 크면 입력을 받는 메모리 공간을 넘어서서 다른 메모리 영역까지 데이터를 덮어 씌우게 된다.
입력을 받은 뒤 12번째 줄에서 v5가 특정 값과 동일하다면 15번째 줄에서 system("/bin/dash")로 쉘을 얻게 된다.
공격 코드를 만들어보자
BOF 공격에 성공한다 해도 결국 쉘은 우리의 컴퓨터에서 얻어지면 플래그를 얻을 수 없어 의미가 없다.
따라서 지정된 IP 주소와 포트 번호에 NC로 접속하여 문제를 풀 수 있다.
pwntools를 이용하여 파이썬으로 공격 코드를 위와 같이 작성했다.
remote 함수에 지정된 도메인과 포트 번호를 인자로 줌으로써 연결을 설정한 뒤 payload에 오버플로우 코드를 작성했다. 마지막으로 r.sendline으로 payload를 서버에 보내게 된다.
Payload의 작성
프로그램은 총 45바이트를 입력받지만 그중에서 1바이트는 개행 문자(Enter)이므로 우리가 입력할 수 있는 크기는 총 44바이트이다. 44바이트 중에서 마지막 4바이트가 우리가 조작해야 하는 int형 변수인 v5이 될 것으로 예상되어 44개 중 40바이트는 임의의 문자로 채운 다음, 마지막 4바이트 크기에 -559038737를 16진수 형태로 (0xFFFFFFFFDEADBEEF) 넣는다.
따라서 실제로 입력받는 44바이트 중 40바이트의 메모리 공간은 임의의 값으로 채워지게 되고 나머지 4바이트 공간(검사하는 v5 변수의 영역)이 검사하는 수와 같아지게 만듦으로써 검사하는 조건 분기문이 참이 되어 실행된다. 즉, 쉘을 얻게 된다.
HACKCTF "Basic_BOF #2" Write-Up
v5에 sup함수의 주소를 넣어 함수 포인터로 사용한다.
char형 배열 s에 133바이트만큼 입력한 뒤 v5를 (v5가 가지고 있는 주소에 있는 함수인 sup를) 호출한다.
sup 함수는 아래와 같이 s의 주소를 출력한다.
IDA에서 현재 프로그램에서 사용되는 함수들의 이름을 살펴보았다. shell이라는 함수가 수상해 보여 열어보았다.
shell 함수가 쉘을 생성하는 것을 확인했다. 따라서 우리는 shell을 실행할 방법을 찾아야 한다.
이때 v5 함수 포인터가 사용된다.
함수 포인터란, 포인터 변수와 똑같지만 저장하는 자료형은 함수인 포인터이다. 즉, 함수의 주소를 저장하는 포인터이다. 처음에 v5라는 함수 포인터는 sup 함수의 주소로 설정되어 있지만, 우리의 입력을 통해 v5가 가지고 있는 주소를 변경시킬 수 있다.
fgets를 실행하기 전 eax에 ebp-0x8C를 넣는데, 이는 s의 주소이다. 그리고 조작해야 하는 함수 포인터가 위치한 주소는 ebp-0xC이다.
따라서 우리의 입력값은 ebp-0x8C부터 시작하여 ebp-0xC까지 오버플로우에 영향을 미치지 않아 임의의 값을 입력하는 것이고, ebp-0xC부터 4바이트(32bit 파일이므로) 간의 메모리 영역을 shell의 주소로 덮어 씌운다.
HACKCTF "x64 Buffer Overflow" Write-Up
입력받은 문자열을 그대로 출력하는 프로그램이다.
프로그램에 호출하는 함수들을 살펴보니 "callMeMaybe"라는 함수가 있었다.
이렇듯 callMeMaybe 함수는 쉘을 반환한다. 따라서 우리는 이 함수를 실행시켜야 한다.
main 함수의 스택 프레임에서 가장 낮은 위치를 가리키는 레지스터인 rbp를 기준으로 rbp 밑에 SFP가 있고, rbp+0x8에는 Return Address가 있다 (64bit 파일이므로 기본 WORD는 8바이트이다). 이는 main 함수가 종료될 때 돌아갈 위치를 지정하는 공간이므로, 이 영역을 조작하면 main이 종료된 후 우리가 원하는 함수를 실행시킬 수 있게 된다.
main에서 확인했듯이 rbp-0x110는 우리가 입력하는 값의 주소이고, 이 주소로부터 rbp+0x8까지 (main 함수의 Return Address) 메모리 영역을 임의의 데이터로 덮어 씌운다. 그다음 8바이트는 우리가 main이 종료될 때 실행시키고자 하는 함수인 callMeMaybe의 주소로 덮어씌운다.
Buffer Overflow
메인 사진 출처: iStock
© 남찬우, 2021
'Layer7 > Layer7_System Hacking' 카테고리의 다른 글
Format String Bug (0) | 2021.08.01 |
---|---|
Buffer Overflow (ROP) (0) | 2021.07.31 |
Buffer Overflow (RTL, PLT&GOT) (0) | 2021.07.14 |
Buffer Overflow (PIE) (0) | 2021.07.11 |
Buffer Overflow (Shellcode) (0) | 2021.07.10 |