21/03/28 TL. BOF를 이용한 공격(2)
- -
공격자들은 악성코드를 심기 위해 shell을 띄우는 작업을 많이 한다.
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"; // 25byte shellcode
// “\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80” => 24byte shellcode
// “\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80” => 23byte shellcode
main()
{
printf("Shellcode Length: %d\n", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
이와 같은 C로 작성된 코드를 통해 쉘코드를 실행시키는 프로그램을 작성할 수 있다.
여기서는 예시로 25byte, 24byte, 23byte 총 세 방식이 나와있다.
shellcode의 목적들은 모두 /bin/sh를 실행시킨다는 목적은 같지만,
intel 기준에선 4byte씩 끊으므로 24byte로 놓을 수도 있고,
크기를 줄여서 23byte로 지정할 수도 있다. 즉 상황에 따라 달라질 수 있다.
여기서 선수지식으로 다음과 같은 지식을 필요로 한다.
또한 코드의 function pointer에 대해 잘 모르겠다면, 다음 내용을 참고하자.
dojang.io/mod/page/view.php?id=592
BOF 보호 기법 세가지는 저번에 3가지가 있다고 했다.
1) ASLR (Address Space Layout Randomization) : 메모리 영역 주소를 랜덤하게 배치시켜 공격하기 어렵도록 한다.
2) NX bit / DEP : 메모리 영역 내에서 실행을 시키지 못하도록 한다.
3) 카나리 :
공격자들이 BOF를 이용해 코드 삽입 등의 공격을 많이 하다 보니, 스택 내에서 실행하지 못하게 하도록 Linux 내에서 NX bit를 설정한다.
따라서, 컴파일 할 때, execstack을 인자로 주지 않으면 일종의 탐지기법에 의해 seg fault가 발생한다.
따라서,
gcc -z execstack -o shellcode shellcode.c
을 통해 컴파일 하도록 하자.
이제 shellcode(shell 실행파일)을 set-uid 프로그램으로 변경한 후 실행하면, root 권한의 shell을 획득할 수 있다!
만약, root 권한이 안 얻어진다면, /bin/sh에 set-uid 방어기법이 적용된 이후의 것일 것이다. 그럴 경우엔,
sudo ln -sf /bin/zsh /bin/sh
를 통해, set-uid 방어 기법이 걸리지 않은 zsh로 링크시켜서 실행하자.
우선, 함수 호출 시 스택 구조에 대해 알 필요가 있는데, 아래 글에 잘 정리돼있다.
잘 모른다면 참고해서 읽어보자!
추가로, eip는 현재 실행하고 있는 명령어의 주소를 담고 있는 레지스터로 쓰인다.(intel)
eip : main + <10> // func()가 실행되는 주소
main + <14> // func2()가 실행되는 주소
이런 경우, ret 주소에, func2를 호출한 주소 + 4byte(바로 다음 주소)가 담겨 있어야 한다. 왜? 돌아가서 이전에 실행하던 부분 바로 다음줄 부터 시작해야 하니까.
이걸 이용해서, return addr에 BOF를 이용해서 shellcode를 실행 할 수 있다!
만약 동적으로 할당돼있으면? library를 이용하거나, Nx bit를 피하기 위해 shellcode를 shellcode를 stack이 아니라, 실행 가능한 곳에 넣고 부를 수 있게 한다.
text영역은 메모리 주소값을 gdb로 열어볼 수가 있다. 정적으로 메모리가 저장돼있기 때문에. 따라서 공격자가 return addr로 공격할 수 있는 주소를 알 수 있다. 따라서 이를 막기 위해 ASLR이라는 방어 기법이 등장했다.
ASLR을 적용하면? buffer라는 주소 영역이 메모리 영역에 load될 때마다 무작위로 바뀌게 된다. Linux, Firefox 등 많은 곳에 적용돼있다. 사실 3가지 기법 모두가 많은 시스템에 적용된다.
이제, 해당 함수 address의 위치를 알려주는 address.c를 보자. 스택 시작 위치를 무작위로 지정해 코드가 메모리에 로드될 때마다 스택 주소를 변경하기 때문에 스택 주소, ebp 주소, 악성 코드 주소 추측이 어려워진다.
// address.c
#include<stdio.h>
#include<stdlib.h>
int main(){
char x[12];
char *y = malloc(sizeof(char)*12);
printf("Address of buffer x (on stack) : 0x%x\n", x);
printf("Address of buffer y (on heap) : 0x%x\n", y);
}
이제 ASLR 기법을 해제하고 공격해보자.
sudo sysctl -w kernel.randomize_va_space=0
다음과 같은 명령어로 ASLR을 해제할 수 있다. 참고로 여기서 설정 값이 2일 땐, 랜덤 라이브러리 / 랜덤 스택 / 랜덤 힙이고, 1일 땐, 랜덤 라이브러리 / 랜덤 스택이다.
저번 실습은 정적으로 분석이 가능했기 때문에 ASLR이 걸려있지 않아도 공격이 가능했지만, 이번 실습은 버퍼 영역에 shellcode를 넣고, return addr를 shellcode가 담긴 buffer의 주소로 변경할 것이기 때문에 주소에 대한 정보를 알아야 한다. 따라서, ASLR을 해제해야 한다.
이 밖에도 추가로 옵션을 주어야 한다. -fno-stack-protector를 통해 카나리를 끌 수 있다. 컴파일러가 버퍼에 더미 값을 추가해서 사용자의 의도대로 메모리를 +α로 불필요한 더미 바이트를 생성하지 않게, -mpreferred-stack-boundary=2도 입력해 준다.
gcc -z execstack -o bof bof.c -mpreferred-stack-boundary=2 -fno-stack-protector
이제, bof 실습을 위한 bof.c를 생성하고, set-uid로 만들어 주자.
#include <stdio.h>
void func(){
char buf[100];
printf("input:");
gets(buf);
printf("%p\n", buf);
}
int main(int argc, char* argv[]){
func();
return 0;
}
이제 func를 gdb로 까본다면,
이렇게 되는데 lea eax, [ebp-0x64]에서 0x64는 100이다. 즉, buffer에서 메모리를 100만큼 할당하는 부분에 해당한다.
참고로 ebp는 stack frame pointer(sfp)에 해당하는 주소를 담당하고 있다. leave, ret 는 스택을 제거하기 위함이다. 현재 실행중인 명령어의 주소가 담겨있는 eip에, ret을 실행하면 return 값이 들어가게 된다.
따라서 ret에 buffer의 적당한 주소 안에 shellcode를 넣고 접근하게 하면 된다. 예를 들어 24byte의 shellcode를 사용한다면, 앞에 24byte shellcode를 넣어놓고 거기로 주소를 쏴서 접근하게 하는 식으로 할 수 있겠다.
어쨌든 buffer에 실행시킬 shellcode를 넣고, return addr를 그 shell code 위치로 overflow해서 shellcode를 실행시키는 것이 이번 실습의 최종 목표이다. 따라서, 이 shellcode가 실행되는 주소를 알아야 한다. 실제로 메모리에 load되는 메모리의 시작 주소를 알아야 return addr를 조작할 수 있을 것이다.
선수지식으로 4GB 가상 메모리 기준(32bit)에선, 0에서 0xfffffff까지인데, 0xc0000000에서 0xffffffff까지(2GB)는 kernel이 사용하고, 나머지 2GB는 유저가 사용한다는 걸 알아야 한다.
또, 현재 instrunction을 나타내는 eip에 대해 알아야 하는데, 0x0804843b <+0>: push ebp를 수행할 때는 eip에 0x0804843b가 들어가서 수행하는 것이다.
sub esp, 0x64는 esp에서 스택의 바닥을 100byte 밑으로 지정하겠다는 의미이다. 그렇게 해서 buffer 영역으로 잡고, 그 다음 printf를 수행하기 위해 push를 해서 메모리를 세팅하고, printf를 호출한다.
user memory 영역 가장 하단 부분은 text, bss, data영역인데, 함수 정보(0x8048300), 문자열의 정보(0x8048500) 등이 기록된다. 즉, 이 정보들은, user memory의 영역을 알 수 있는 정보가 된다. 이제 load될 때는 buffer의 시작 주소가 0x64가 아니라, 특정 주소로 load될 것인다. 이제 프로그램을 실행해 보면서 확인해 보자.
b *func로 breakpoint를 걸고 r로 실행한다. n으로 한 줄씩 넘어간다.
현재 break point가 sub에 걸린 상태이다. ebp에 담고 있는 0xbf9ffd28의 값은 main 함수의 sfp이다. 즉, 0xbf90ffd20에는 main 함수의 stack frame pointer를 담고 있다는 소리이다. 0xbf9ffd24는 func함수가 끝난 다음, 실행될 주소를 담고있다. 즉, return addr이다. 여기선 0x08048476라고 돼있는데, 이 값을 shellcode가 담긴 buffer의 주소로 세팅하고, shellcode를 실행시키면 되는 것이다.
0x084876의 경우에는 main함수에서 func를 실행시키고 바로 다음 줄. 즉, return 0에 대한 정보를 담고 있다. 따라서 다시 main함수로 돌아간다고 보면 된다.
이제, 인자 값을 받는 <+23> 부분이 취약 부분이라고 볼 수 있다. 해당 부분에 breakpoint를 걸고 보자.
b *func+23
n으로 넘어가다가, gets에서 input으로 AABB를 넣어주었다.
이제, ebp - 0x64를 해야 한다. 그 값이 버퍼의 시작주소일 것이다. 이제 값을 확인해 보면, BBAA(리틀엔디안이므로)로 들어가 있다. 버퍼의 첫 부분(+4) 부터 시작한다.
0xbf8c4a38는 stp를 가리킨다. 따라서 이 전 까지가 buffer에 들어가는 값들이다.
따라서 0xbf8c49cc에서 104byte + return addr하면, 값을 조작할 수 있을 것이다.
생각해보니, ASLR을 종료했는데 메모리 값이 바뀌어서 확인해보니 안 꺼져 있었다. 끄고 나서 다시 확인해 봤더니, 이제 메모리가 바뀌지 않는 걸 확인할 수 있었다. 아무튼 그 다음은 python을 이용해서 100byte를 A로 채우고 실행해 봤다.
이제 해당 영역은 A 100개로 채워진 것을 확인할 수 있다.
여기서 추가로 참고할 건, 좌측은 주소고 우측은 값이다. 우측은 탭으로 구분해서, 각 c10 c14 c18 c1c에 들어있는 값. 이런 식이다.
참고로 저기 밑줄 친 부분에는 71 : ec 72 : ff 73 : bf이다. 마지막에 00이 된 이유는 string의 끝 부분을 표현하기 위해 null을 넣은 것이다.
이번에는 A를 108을 넣었다.
이제 더 수행해서 ret까지 간다면?
현재 실행하는 곳을 이상한 주소값으로 가버린다. 따라서 프로그램 자체가 seg fault를 반환하며 kill된다.
이러한 취약점을 악용할 수 있는 행위를 exploit, 그러한 코드를 exploit code 혹은 payload라고 한다.
'TL' 카테고리의 다른 글
21/04/15 TL. OPTEE (0) | 2021.04.16 |
---|---|
21/04/04 TL. FSB를 이용한 공격 (0) | 2021.04.04 |
21/03/23 TL. BOF를 이용한 공격(1) (1) | 2021.03.23 |
21/03/11 TL. Set-UID를 이용한 취약점 공격들. (0) | 2021.03.11 |
21/03/10 TL. 프로비저닝, 애자일, args[]란? (0) | 2021.03.10 |
소중한 공감 감사합니다