본문 바로가기
System Hacking/해커스쿨 F.T.Z

해커스쿨 F.T.Z BOF(Buffer OverFlow), GDB 기초

by En_Geon 2020. 2. 15.

level 9를 하기에 앞서 Break Time을 가져야겠다는 생각을 했다. 

그 이유는 level 9에는 BOF(Buffer OverFlow)가 나오고 GDB를 통해 분석해야 하므로 간단한 설명과 정보를 주기 위함이다.

BOF를 잘 이해하기 위해서는 메모리 구조스택 프레임을 이해해야 하는데 동빈나님의 유튜브를 참고하여 이해했다.

동빈나님의 좋은 강좌가 있어 소개하자면 해커스쿨 실습과 같은 시스템 해킹 강좌다.

GDB를 통해 분석하기 위해서는 어셈블리언어를 알아야 한다. 어셈블리에 대해 간단히 설명한다.

 

 

BOF(Buffer OverFlow)

 

BOF(Buffer OverFlow)란 버퍼 오버플로라고 부르고 이는 메모리를 다루는 데에 오류가 발생하여 잘못된 동작을 하는 프로그램 취약점이다. 컴퓨터 보안과 프로그래밍에서는 프로세스가 데이터를 버퍼에 저장할 때 프로그래머가 지정한 곳 바깥에 저장하는 것을 의미한다. 벗어난 데이터는 인접 메모리에 덮어쓰게 되며 이때 다른 데이터가 포함되어 있을 수도 있는데, 손상을 받을 수 있는 데이터는 프로그램 변수와 프로그램 흐름 제어 데이터도 포함한다. 이로 인해 잘못된 프로그램 기동이 나타날 수 있으며, 메모리 접근 오류, 잘못된 결과, 프로그램 종료, 또는 시스템 보안 누설이 발생할 수 있다.

BOF에는 크게 두 가지 Stack Overflow와 Heap Overflow가 있다. 보통 BOF라고 하면 Stack Overflow를 말한다.

 

버퍼란 한 곳에서 다른 곳으로 데이터를 이동할 때 임시로 그 데이터를 저장하기 위해 사용되는 일종의 저장 공간이다.

 

인접해 있는 두 버퍼가 있다고 가정해보자.

이때 A 버퍼는 8바이트 길이의 스트링 버퍼, B 버퍼는 2바이트 정수형 버퍼다. A는 8바이트 모두 숫자 0 값만 포함하고 B는 숫자 3을 포함한다. 문자들은 1바이트 크기다.

 

A B
0 0 0 0 0 0 0 0 0 3

 

A 버퍼에 "HACKERFTZ"를 저장한다. \0(null)문자는 스트링의 끝임을 알리기 위해 따라온다.

 

A B
H A C K E R F T Z \0

 

보는 것과 같이 "HACKERFTZ\0"문자가 A 버퍼의 길이보다 길다 보니 B 버퍼에 있던 "03" 데이터를 무시하고 덮어쓰기를 한다.

 

C/C++ 컴파일러는 배열의 경계검사(Boundary Cheak)를 하지 않기 때문에 스트링의 길이를 확인하지 않고 B의 값을 덮어쓴다.

여기서 중요한 건 덮어쓴다는 것이다. B 버퍼의 내용을 옆으로 밀어서 B 버퍼의 데이터를 남겨두고 쓰는 것이 아니고, B 버퍼의 자리를 잠시 빌려 쓰는 것이 아니고 B 버퍼의 데이터 위에 덮어쓰기를 한다.

 

특정 함수를 사용할 때 입력한 문자열을 저장하기 위해 선언한 변수(배열)의 크기보다 더 길게 입력하면 다른 용도로 사용하는 영역에 값을 덮어쓴다.

BOF 공격에 취약한 함수는 "strcpy, strcat,gets, fscanf, scanf, sprintf, sscanf, vfscanf, vsprintf, vscanf, vsscanf, streadd, strecpy, strtrns"가 있는데 이 함수들의 공통적인 특징은 처리하는 문자열의 최대 크기를 정하지 않는다는 점이다

 

여기서 문제는 버퍼가 바뀌는 상황인데도 프로그램은 전혀 모르고 있는 상태라는 것이다. 버퍼 값이 바뀌어도 프로그램은 이를 전혀 사용자에게 통지하지 않는다. 하지만 요즘에는 디버깅 시 컴파일러가 버퍼 앞뒤에 오버플로 방어용 1 ~ 4바이트짜리 값을 넣어 침범되었을 경우 예외를 시키거나 코딩 단계에서 '이 함수는 버퍼 오버플로의 위험이 있음'이라고 경고를 해준다. 그렇다고 해서 BOF가 사라진 건 아니므로 프로그래머는 BOF를 대비해야 한다.

 

BOF를 사용한 해킹

 

BOF를 사용하여 해킹하기 위해서는 RET에 대해 알아야 한다. 동빈나님의 전문적인 강좌를 보고 이해하기 바란다.

BOF를 일으키면 우리가 원하는 버퍼를 넘어 원하지 않는 버퍼까지에도 덮어쓸 수 있고, 나아가 RET까지 덮어쓸 수 있게 된다.

따라서 BOF를 사용해서 RET에 자신이 원하는 명령이 들어있는 메모리의 주소로 덮어쓴다면 자신이 원하는 명령을 수행할 수 있다.

대부분 해킹의 경우는 root권한의 쉘을 받아내는 명령을 수행한다.

 

 

GDB

 

보통 GDB라고 부르는 GNU 디버거는 GNU 소프트웨어 시스템을 위한 기본 디버거다. GDB는 다양한 유닉스 기반의 시스템에서 동작하는 이식성 있는 디버거로 에이다, C, C++, 포트란 등의 여러 프로그래밍 언어를 지원한다.

 

GDB의 사용법은 구글링과 해커스쿨 실습으로 배우거나 번역본을 통해 배우도록 한다.

 

여기서 말하고 싶은 건 어셈블리다. 사용법을 익히더라도 어셈블리에 대해 알지 못한다면 무용지물이다.

본인도 어셈블리에 대해서 자세히는 모르지만, 같이 배워나가려고 한다.

 

 

어셈블리(Assembly)

 

프로그래밍 언어 중 하나이며 기계어에서 한 단계 위의 언어이며 기계어와 함께 단 둘뿐인 저급(Low Level)언어다.

여기서 저급언어란 질이 낮은 언어를 뜻하는 것이 아니라, 보다 컴퓨터에 가까운 언어를 의미한다. C언어는 추상화된 고급 언어로 분류되지만, 포인터를 이용하여 메모리에 직접 접근하는 것은 저급 언어의 특징이다.

어셈블리어는 리버스 엔지니어링을 하기 위한 가장 기초적인 도구다.

 

레지스터(Registers)

동빈나님의 시스템 해킹 강좌 4강에서 나온다.

레지스터들은 각각의 쓰임새가 정해져 있는데 강제성은 없지만 대부분 지키는 것이 좋다.

 

기본적으로 레지스터는 비트별로 나뉜다.

 

64 Bit 32 Bit 16 Bit 8 Bit
rax eax ax al
rbx ebx bx bl

 

32bit 체제에서는 "e"를 붙이고 64bit 체제에서는 "r"을 붙인다.

GDB를 실행시켜 레지스터를 보면 어떤 체제에서 프로그램을 컴파일한 것인지 알 수 있다.

 

데이터 레지스터 (범용 레지스터)

 

논리 연산, 수리 연산에 사용되는 피연산자, 주소를 계산하는데 사용되는 피연산자, 그리고 메모리 포인터가 저장되는 레지스터다.

 

rax

 

가장 많이 쓰는 변수다. 특히 사직연산에 주로 사용된다. 또한, 함수의 리턴값이나 return 100, return False 등의 코드를 사용할 때 100이나 False에 해당하는 값이 rax에 기록된다.

 

rbx

 

rsi 레지스터나 rdi 레지스터와 결합할 수 있으며, 이 rbx 레지스터는 메모리 주소를 저장하기 위한 용도로 사용된다.

 

rcx

 

여기서 C는 Count의 약자로 카운터 레지스터다. 주로 반복 명령어 사용 시 반복 카운터로 사용되는 레지스터다.

rcx 레지스터에 반복할 횟수를 지정하고 반복 작업을 수행한다.

 

rdx

 

rax와 같이 쓰이고 부호 확장 명령 등에 쓰인다. 큰 수의 곱셈 또는 나눗셈 등의 연산이 이루어질 때, rdx 레지스터가 사용되어 rax 레지스터와 함께 쓰인다

 

포인터 레지스터

 

rsi

 

데이터를 조작하거나, 복사 시에 소스 데이터의 주소가 저장된다.

 

rdi

 

rsi 레지스터와 비슷하나, rdi 레지스터에는 복사 시의 목적지의 주소가 저장된다.

 

rbp

 

스택 프레임의 시작 지점 주소(스택의 가장 윗부분, 스택의 처음)가 저장된다.

rbp 레지스터는 현재 사용되는 스택 프레임이 소멸하지 않는 이상 rbp 레지스터의 값은 변하지 않는다.

 

rsp

 

스택 프레임의 끝 지점 주소(스택의 가장 아랫부분, 스택의 마지막)가 저장된다.

push, pop 명령에 따라 rsp의 값이 4바이트씩 변한다.

 

이곳은 간단한 설명을 위한 곳이다.

어셈블리 명령어는 여느 프로그래밍 언어와 같이 아주 많고 기본적으로 많이 쓰는 건 쓰다 보면 외워지겠지만, 구글링을 통해 알아보자.

 

 

 

댓글