이번에 해볼 공격은 overflow를 이용한 것인데, 기본적으로 메모리, 스택 등 OS적인 요소에 대한 선험적인 지식을 필요로 한다.
간단히 설명해 보자면,
스택 : 지역변수, 함수가 작동하기 위해 사용되는 메모리. 임시로 데이터를 저장해서 연산처리 하는 영역
힙 : 동적으로 할당되는 영역 ex)malloc
bss : 초기화되지 않은 전역변수
text : 함수
data : 초기화된 전역변수
이다.
각 카드들 중 원하는 문제를 누르면, 지시사항을 준다. 여기서는 이 2개의 파일을 wget으로 받은 후, 아래를 실행시켜 원하는 모양새로 만들면 통과이다.
간단하게 분석해 보자면, run하면서 func을 실행한다. 이 때 key에는 deadbeef가 들어간다.
여기서 gets 는 scanf와 동일하다고 보면 되는데, bof에 취약하다.
만약 key값이 cafebabe면 shell을 실행한다.
현재, overflowme는 32byte를 할당받은 상태이다. 만약 여기에 A라는 40byte를 집어 넣는다면??
그 위 8byte 배열에 A가 덮어씌어질 것이다. 즉, 다른 메모리 값을 변조시킬 수 있다.
이 공격으로 지역변수나 return값을 악의적으로 덮어씌워 공격할 수 있다.
현재 int key는 지역변수이다. 따라서 gets라는 취약 함수를 통해 overflow를 이용해 값을 변조해서 key값을 수정하는 걸 목표로 하자.
근데 이 코드를 직접 컴파일해선 안 된다.
BOF를 막기 위한 3가지 방어 기법이 있다.
1. Nx(non excutable) -> 스택에서 코드를 exe할 수 없게 하는 방어 기법이다. bof로 return을 악의적으로 수정할 수 있기 때문이다.
2. 카나리 -> 랜덤한 값을 buffer에 넣어서 해당 값이 변조됐으면 bof가 일어났다고 판단하고 프로그램을 종료한다.
3. ASLR -> 메모리(스택, 힙)의 주소를 random하게 부여해서 주소를 알기 힘들게 만든다.
이러한 방어 기법들이 있는데, gcc -o bof.c bof 이런 식으로 컴파일하면 자동으로 이런 방어기법을 적용해서 컴파일한다.
이제 bof.c로 다시 넘어오자. 우선, 우리는 int key의 위치가 어디에 있는지 알 수 없다.
해당 그림 처럼, stack pointer 4byte, return addr 4byte, overflowme 32byte를 넘어가면 key 영역이므로, 40 + 0xcafebabe를 입력하게 하면 해결할 수 있는 거 아냐? 라고 할 수도 있다. 그러나, 컴파일 과정에서 최적화 등 이유로 메모리 값을 바꾸거나 그럴 수도 있기 때문에, 코드만 보고 이론적으로 접근하는 것은 매우 힘들다.
따라서 바이너리 값을 보고 바꾸는 과정이 필요하다. 따라서 gdb라는 disassemble 파일을 보고 까보자.
컴파일은 고급 언어를 한 번에 보고 실행파일(exe, elf)로 바꾸어 준다. 그런데, 고급언어 -> 어셈블러 -> (opcode)기계어 이런 과정으로 진행된다. 실행파일(binary file)을 다시 원래대로 바꿔주는 것을 disassemble이라고 하고 대표적인 툴이 gdb이다. 이 파일을 잘 보고 공격을 해보자.
여기서 몇가지 참고해야할 점들이 있다.
1. return addr는 disassemble할 때 생략된다.
2. 스택에선 함수마다 구역을 차지한다.
3. 그 구역은 ebp로 시작해서 ret로 끝난다.
4. 함수를 call로 실행하기 전에, return 주소와 스택 포인터를 저장해 놓는다.
4. return되고 나면 해당 메모리 영역을 재사용하기 위해 해당 메모리를 지워버린다.
다시 main으로 돌아가서, 위에서 설명한 대로 +3부터 +16은 func를 실행하기 위해 준비하는 과정이다.
그 바로 밑 mov eax, 0x0은, eax에 0을 넣는 과정인데, return 값에 0이 들어가서 함수가 종료된다고 생각하면 된다.
마찬가지로, func도 +3에서 +87이 함수의 내용이라고 볼 수 있다.
여기서 보면, +24, +35, +56, +70, +87에서 call하고 있다. 소스코드에선 4번인데, call을 5번하는 이유는 +75 아래 부분과 +3, +6 부분에선 카나리를 위한 부분이다.
따라서, 흰색 부분이 코드 내용이라 볼 수 있다. 여기서 call은 printf, gets, printf, system이라고 할 수 있다. cmp는 compare인데, cafebabe와 같은지 검사하는 부분이다.
이제, 우리가 예상한 대로 overflowme가 32byte를 그대로 할당할 것이냐?? 를 보자.
그 전에 또 알아야 할 게 있다.
ebp에서 bp는 base pointer을 의미하는데, 현재 스택 프레임에 대한 주소를 표시한다.
esp에서 sp는 stack pointer를 의미하는데, 현재 스택 주소를 표시한다.
e는 현재가 32bit 환경이라는 것을 의미한다.
따라서 +1에 mov ebp, esp는 스택 프레임 주소에 현재 스택 주소를 넣고 시작한다는 의미이다.
그 다음 usb esp, 0x48은 esp에 0x48만큼 빼겠다는 의미이다. 이는 십진수로 표현하면 72이다. esp가 0x48만큼 메모리를 할당하겠다는 의미이다. 즉, 32byte가 아니라는 의미이다. 더 클 수도 있다. 스택은 아래로 자라니까 빼는 것이 할당하는 것이라고 생각하면 된다.
즉, 이제 76byte + 0xcafebabe로 값을 변조시킬 수 있다.
이제 해결법을 알았으니 수행해보자.
b *func로 breakpoint를 걸고 r로 실행해보자.
n으로 한 줄씩 넘어가면서 메모리 상태를 확인할 수 있다.
넘어가다 보면, eax에 값을 세팅한 다음에 call을 한다. 여기서 세팅하는 값은 인자(deadbeef)이다.
만약 인자가 2개다? eax, ebx를 쓴다. 3개다? eax, ebx, ecx를 쓴다. 이런 식으로. 4개다? edx까지. 5개 이상이다? stack에 저장해 놓고 함수를 호출한다. -> 함수 호출할 때, 일시적으로 저장할 수 있는 공간을 필요로 하기 때문.
그리고, ebp - 0x2c가 배열의 시작 주소라는 것도 알 수 있다. 여기서 다시 조교님의 그림을 보자.
만약에 stp(= ebp)가 0x100이라면, 이 밑으로 0x2c만큼 내려간다. 이 때, 크기는 44byte인데, 여기서 12는 컴퓨터가 알아서 배정해 준 메모리이다. 여기다가 stp, ret (4, 4) byte씩을 더하면 52byte가 나온다. 이제, 52 + 0xcafebabe를 입력하면 이제 exploit code를 짤 수 있을 것이다!!
이제 마지막이다.
"cafebabe"는 문자당 1byte이므로 총 8byte이다.
그런데, 0xcafebabe는 2개당 1byte라고 칠 수 있어서 4byte라고 볼 수 있다. 여기서 32bit는 4byte이므로,
컴퓨터가 한 번에 연산할 수 있는 수는(32bit이므로) 4byte라고 할 수 있다.
또, key는 int이므로 4byte이다.
따라서, 그냥 cafebabe라고 입력하면 문자열로 인식하므로, 인코딩을 통해 0xcafebabe로 만들어줘야 한다.
추가로, 리틀 엔디안으로 넣어야 하므로, 0xbebafeca로 입력해야 한다.
인코딩할 땐, python을 이용해서 넣어주자. python -c 'print"\xbe\xba\xfe\ca"'로 0xcafebabe를 넣어줄 수 있다!
이제 거의 다 왔다. 52만큼 의미없는 값을 넣어준 후, \xbe\xba\xfe\ca도 함께 넣어 실행해주자. 그런데, 이렇게 shell을 실행해 줄 때, shell은 입력값이 없으면 자동으로 종료되므로 shell을 열어놓기 위해 입력스트림을 열어놓아야 한다.
cat을 아무 인자도 안 넣고 실행해 주면, 입력스트림을 열고 입력값을 기다리므로 cat을 넣어주자.