1. 해킹이란
특정 문제를 풀거나 비효율적인 시스템을 개선하는 해결책이라는 의미인 핵(hack)에서 유래
컴퓨터의 하드웨어나 소프트웨어, 네트워크, 웹사이트 등 각종 정보 체계에서 주어진 권한 이상을 얻거 나 의도하지 않은 동작을 일으키는 행위. 즉, 규칙을 창조적으로 활용해 의도치 않은 결과를 얻어내는 일련의 행위를 의미
대표적으로 웹 사이트를 공격하는 웹 해킹이나, 컴퓨터와 컴퓨터가 연결되는 네트워크를 공격하는 네트워크 해킹, 프로그램의 약점을 공격하는 시스템 해킹 등이 존재
2. 소프트웨어 취약점
공격자가 주어진 권한 이상의 권한을 획득하거나 프로그래머가 의도하지 않은 동작을 수행할 수 있도록 하는 소프트웨어 버그
버그 중에는 소프트웨어의 동작에 거의 영향을 미치지 않는 무해한 버그들도 있지만, 보안을 심각하게 위협하는 위험한 버그들도 존재함
여러 가지 요인으로 인해 발생하는데 가장 대표적인 원인으로는 프로그래머의 실수로 인해 일어나거나 항상 올바른 입력이 들어올 것이라는 잘못된 개발자의 잘못된 가정으로 인해 일어나기도 함
모든 소프트웨어 취약점은 소프트웨어와 공격자가 상호 작용하는 곳(Attack Vector), 즉 사용자의 입력에서부터 발생
*Attack Vector (공격 벡터)들의 집합을 Attack Surface라고 함
만약 unreachable code 영역에 있는 코드가 어디에서도 사용되지 않고 공격자가 이를 실행할 방법이 없다면 이는 보안 취약점이라고는 할 수 없으나 이러한 코드도 잠재적으로 악용될 수 있는 코드이기 때문에 존재해서는 안 됨
취약점은 공격 방법에 따라 C/C++과 같은 저수준 언어에서 메모리를 조작해 공격하는 메모리 커럽션 취약점과 메모리를 조작할 필요 없이 공격할 수 있는 로지컬 취약점으로 나눌 수 있음
3. 취약점의 분류

1. 소프트웨어 버그(Bug) - 프로그래머가 의도하지 않은 동작을 수행
2. 소프트웨어 취약점(Vulnerability) - 소프트웨어 버그 중 보안에 영향을 미칠 수 있는 버그
3. 익스플로잇 가능한 취약점(Exploitable Vulnerability) -소프트웨어 취약점 중 이를 이용해 공격자가 의도한 동작을 수행할 수 있는 버그
4. 안정적으로 익스플로잇 가능한 취약점(Reliably Exploitable Vulnerability)
- 익스플로잇이 가능한 취약점 중 매우 높은 확률로 공격에 성공할 수 있는 버그
* 익스플로잇: 사전적으로는 악용이라는 뜻을 가지고 있는 단어로, 취약점을 이용해 공격자가 의도한 동작을 수행하게 하는 코드 혹은 이를 이용한 공격 행위를 의미
4. 메모리 커럽션 취약점의 대표적 예시들
4-1. Buffer Overflow (BOF)
프로그래머가 할당한 크기의 버퍼보다 더 큰 데이터를 입력 받아 메모리의 다른 영역을 오염시킬 수 있는 취약점
발표된 지 30년에 가까운 시간이 흘렀지만, 아직도 공격에 자주 사용되는 취약점임
Buffer overflow공격 기법을 이해하기 위해서는 무엇보다 컴퓨터에서 실행되는 프로세스의 구조와 자료 저장 방식, 함수 호출 과정 및 리턴 과정, 함수 실행 과정에 대한 정확한 이해가 필요함
이렇게 복잡하고 어려운 구조를 알아야 하는 이유는 우리가 buffer overflow 공격을 하는데 있어 적절한 padding 사용과 return address의 정확한 위치를 찾고 필요한 assembly 코드를 추출하고 이해하는데 필요하기 때문
아래에서 간단히 Buffer Overflow가 무엇인지 알아본 후 8086시스템에 대해 서술할 것임
C언어에서 버퍼란 지정된 크기의 메모리 공간이라는 뜻
8 바이트의 버퍼 A와 8 바이트 데이터 버퍼 B가 메모리에 선형적으로 할당되었다고 생각할 때 여기서 버퍼 A에 16 바이트의 데이터를 복사한다면 이 데이터의 뒷부분은 버퍼 A를 넘어 뒤에 있는 데이터 영역인 B에 쓰여 지게 됨

이때 우리는 버퍼 오버플로우가 발생했다고 하고, 이는 프로그램의 Undefined Behavior을 이끌어냄
만약 데이터 영역 B에 나중에 호출될 함수 포인터를 저장하고 있다면 이 값을 "AAAAAAAA"와 같은 데이터로 덮었을 때 Segmentation Fault를 발생시킬 수 있음
버퍼 오버플로우 취약점은 프로그래머가 버퍼의 길이에 대한 가정을 올바르지 않게 하여 발생. 이는 보통 길이 제한이 없는 API 함수들을 사용하거나 버퍼의 크기보다 입력받는 데이터의 길이가 더 크게 될 때 자주 일어나는 실수임
인접한 메모리를 오염시키는 취약점이기 때문에 어떤 메모리를 오염시킬 수 있는지에 따라 공격 방법이 달라지므로 발생하는 위치에 따라 스택 버퍼 오버플로우, 힙 오버플로우와 같이 나눠서 부름
스택 버퍼 오버플로우, 힙 오버플로우는 8086시스템에 대해 다룬 후 예시와 함께 더 자세하게 서술할 예정
4-1-A. 8086 Memory Architecture

시스템이 초기화 되기 시작하면 시스템은 커널을 메모리에 적재시키고 가용 메모리 영역을 확인

운영체제는 하나의 프로세스를 실행시키면 이 프로세스를 segment라는 단위로 묶어서 가용 메모리 영역에 저장시킴
하나의 프로세스는 최대 2^32byte의 크기를 가질 수 있음
오늘날의 시스템은 멀티 테스킹(multi-tasking)이 가능하므로 메모리에는 여러 개의 프로세스가 저장되어 병렬적으로 작업을 수행함

segment가 실제로 위치하고 있는 메모리상의 주소를 0x80010000이라고 가정
code segment 내에 들어 있는 하나의 instruction IS 1를 가리키 는 주소는 0x00000100
이것은 logical address이고 이 instruction의 실제 메모리 상의 주소는 segment offset인 0x80010000과 segment내의 주소 0x00000100을 더한 0x80010100 이 됨
따라서 이 segment가 메모리상의 어느 위치에 있더라도 segment selector가 segment의 offset을 알아내어 해당 instruction의 정확한 위치를 찾아낼 수 있음
segment 구조1 - code segment : instruction가 들어 있음
instruction은 많은 분기 과정과 점프, 시스템 호출 등을 수행하게 되는데 분기와 점프의 경우 메모리 상의 특정 위치에 있는 명령을 지정해 주어야 하지만 segment는 자신이 현재 메모리 상에 어느 위치에 저장될지 컴파일 과정에서는 알 수 없기 때문에 정확한 주소를 지정 할 수없으므로 logical adress를 사용함
즉 segment selector에 의해서 자신의 시작 위치(offset)를 찾을 수 있고 자신의 시작 위치로 부터의 위치(logical adress)에 있는 명령을 수행할 지를 결정하게 됨
실제 메모리 주소 phyical address는 offset + logical address라고 할 수 있음
segment 구조2 - data segment : 프로그램이 실행시에 사용되는 데이터(전역 변수)가 들어감
현재 모듈의 data structure, 상위 레벨로부터 받아들이는 데이터 모듈, 동적 생성 데이터, 다른 프로그램과 공유하는 공유 데이터로 나뉨
segment 구조3 - stack segment : 지역 변수들이 자리 잡는 공간
현재 수행되고 있는 handler, task , program이 저장하는 데이터 영역
프로그램이 사용하는 multiple 스텍을 생성할 수 읶고 각 스텍들간의 switch가 가능
처음 생성될 때 그 필요한 크기만큼 만들어지고 프로세스의 명령에 의해 데이터를 저장해 나가는 과정을 거침
데이터를 저장하고 읽어 들이는 과정은 PUSH와 POP instruction에 의해서 수행됨
4-1-B. 8086 CPU 레지스터 구조

4-1-B-1. 범용 레지스터(General-Purpose register)

각 레지스터별로 목적이 있으며 목적대로 사용하는 것이 유용
16bits 에서는 AX, BX, CX, DX로 불리며 32bits 에서는 EAX, EBX, ECX, EDX로 불림
X는 H와 L로 나뉘며 레지스터의 상위 부분이 H
ESP - SS 레지스터가 가리키는 stack segment의 맨 꼭대기를 가리키는 포인터
EBP - SS 레지스터가 가리키는 스택상의 한 데이터를 가리키는 포인터
논리 연산, 수리 연산에 사용되는 피연산자, 주소를 계산하는데 사용되 는 피연산자, 그리고 메모리 포인터가 저장되는 레지스터
프로그래머가 임의로 조작할 수 있게 허용되어 있음
4-1-B-2. 세그먼트 레지스터(segment register)


code segment, data segment, stack segment를 가리키는 주소가 들어 가 있는 레지스터
프로세스의 특정 세그먼트를 가리키는 포인터 역할을 하며 세그먼트 레지스터가 가리키는 위치를 바탕으로 우리는 원하는 segment안의 특정 데이터, 명령어들 정확하게 끄집어 낼 수가 있음
4-1-B-3. 플래그 레지스터(Program status and control register)

프로그램의 현재 상태나 조건 등을 검사하는데 사용되는 플래그들이 있는 레지스터
4-1-B-4. 인스트럭션 포인터 (instruction pointer)
다음 수행해야 하는 명령(instruction)이 있는 메모리 상의 주소가 들어가 있는 레지스터
다음 실행할 명령어가 있는 현재 code segment의 offset 값을 가짐
하나의 명령어 범위에서 선형 명령 집합의 다음 위치를 가리킬 수 있음
JMP, Jcc, CALL, RET와 IRET instruction이 있는 주소값을 가진다.
EIP 레지스터 는 소프트웨어에 의해 바로 엑세스 할 수 없고 control-transfer instruction (JMP, Jcc, CALL, RET)이나 interrupt와 exception에 의해서 제어됨
EIP 레지스터를 읽을 수 있는 방법은 CALL instruction을 수행하고 나서 프로시저 스텍(procedure stack) 으로부터 리턴하는 instruction의 address를 읽는 것
프로시저 스텍의 return instruction pointer의 값을 수정 하고 return instruction(RET, IRET)을 수행함으로 해서 EIP 레지스터의 값을 간접적으로 지정 해 줄 수 있음
4-1-C. 프로그램 구동 시 Segment에서 일어나는 일

$gcc –S –o simple.asm simple.c 옵션으로 컴파일하며 이렇게 만들어지는 어셈블리 코드는 컴파일러의 버전에 따라 다르게 생성됨



logical address는 0x08000000 부터 시작하지만 실제 프로그램이 컴파일, 링크되는 과정에서 다른 라이브러리들 필요
전역 변수가 없으므로 data segment에는 링크된 라이브러리의 전역변수 값만 들어 있을 것

프로그램이 시작되면 CPU가 수행할 명령이 있는 EIP 레지스터는 main함수가 시작되는 코드를 가리키고 있음
system architecture에 따라 PUSH, POP 할 위치가 달라짐
ebp를 저장하는 이유는 이전에 수행하던 함수의 데이터를 보존하기 위해서이고 이것을 base pointer라고도 부름
함수 프롤로그 과정: 함수가 시작될 때에는 이렇게 stack pointer와 base pointer를 새로 지정하는 과정

push % ebp
stack pointer는 4바이트 아래인 0xbffffa78을 가리킴
mov %esp, %ebp
ESP 값을 EBP에 복사함으로써 base pointer와 stack pointer가 같은 곳을 가리킴
sub $0x8, %esp
ESP에서 8을 뺴는 명령으로 ESP는 8바이트 아래 지점을 가리키며 스택에 8바이트의 공간이 생김
현재 ESP: 0xbffffa70
and $0xfffffff0, %esp
ESP의 주소 값 맨 뒤 4bit를 0으로 만들기 위해 ESP와 11111111 11111111 11111111 11110000 와 AND 연산을 함
sub %eax, %esp
stack pointer를 EAX만큼 확장시키기 위해 ESP에 들어있는 값에서 EAX에 들어있는 값을 뺌- 0이 들어있어서 의미없는 명령임
sub $0x4, %esp
스택을 4바이트 확장
현재 ESP: 0xbffffa6c

push $0x03 / push $0x02 / push $0x01
function(1,2,3)을 수행하기 위해 인자값을 입력한 것으로 스택에서 나올 때는 반대이기 때문에 거꾸로 되어있음
call 0x80482f4
함수 실행이 끝난 다음 이후 명령을 실행하도록 이 후 명령이 있는 주소를 스택에 넣은 다음 EIP에 함수의 시작 지점의 주소를 넣음
add $0x10, %esp
함수 수행이 끝나고 나면 이제 어디에 있는 명령을 수행해야 하는가를 스택에서 POP하여 알 수 있게 되며 이것이 BOF에서 가장 중요한 return address임
현재 EIP: function 함수가 있는 0x80482f4

push %ebp / mov %esp, %ebp
함수 프롤로그 수행
main 함수에서 사용하던 base pointer가 저장되고 stack pointer를 function함수의 base pointer로 삼음

sub $0x28, %esp
fucntion함수의 프롤로그가 끝나고 만난 명령
지역 변수 buffer1[15]와 buffer2[10]을 선언하여 총 40바이트만큼의 스택을 확장함
gcc2.96 미만에서는 word(4byte)단위로 나뉘지만 이후부터는 16배수로 할당되어 16+16+8(dummy)byte가 할당됨

mov $0x41, [%esp -4] / mov $0x42, [%esp -8]
이런 형식으로 ESP를 기준으로 스택의 특정 지점에 데이터를 복사해 넣는 방식으로 동작


leave
leave instruction: 함수 프롤로그 작업을 되돌림
mov %ebp %esp / pop %ebp
leave명령은 위의 2가지 명령과 같은 명령으로
stack pointer를 이전의 base pointer로 잡아 function함수에서 화장했던 스택 공간을 없애며 PUSH해서 저장해 둔 main함수의 base pointer를 복원함
POP을 하여 stack pointer는 1 word 위로 올라갔으며 stack pointer는 return address와 같은 지점을 가리킴

ret
ret instruction: 이전 함수로 return하라는 의미
EIP 레지스터에 return address를 POP하여 집어 넣는 역할
starck pointer는 1 word 위로 올라감
$0x10, %esp
스택을 16바이트 줄임
stack pointer는 0x804830c에 있는 명령을 수행하기 이전의 위치로 돌아감
leave / ret
각 레지스터 값은 main 함수 프롤로그 과정을 되돌리고 main함수 이전으로 돌아감
이제 자세하게 스텍 버퍼 오버플로우와 힙 오버플로우를 다뤄보도록 하자
스택 버퍼 오버플로우
가장 초기에 연구되었던 형태의 버퍼 오버플로우로, 지역 변수가 할당되는 스택 메모리에서 오버플로우가 발생하는 경우
예시1) stack-1.c

gets함수
사용자가 개행을 입력하기 전까지 입력했던 모든 내용을 첫 번째 인자로 전달된 버퍼에 저장하는 함수
그러나 gets 함수에는 별도의 길이 제한이 없기 때문에 16 바이트가 넘는 데이터를 입력한다면 스택 버퍼 오버플로우가 발생

sfp는 4Byte 크기로 스택 주소값을 계산할 때 현재 스택값의 바닥, 즉 기준을 잡을 때 필요한 프레임 포인터 값을 저장하는 곳
ret은 Return Adress의 약자로 돌아갈 코드영역의 위치를 뜻함
예시2) stack-2.c


char *
포인터 변수를 할당하여 주소 값을 가지고 있음
strncpy 함수
헤더 파일 : <string.h>
함수 원형: char* strncpy(char* dest, const char* origin, size_t n) - origin에 있는 문자열을 dest로 n만큼만 복사하는 함수
->간단하게 strncpy(dest,origin,n)으로 사용가능
기본적으로 \0를 상관하지 않고 n의 길이만큼만 복사함->적절한 위피에 \0을 넣어주어야 하는지 살펴보아야함
*주의 : n<=sizeof(origin) (휴먼에서 발생할 수 있음), n<=sizeof(dest) (런타임 에러)
strlen함수
헤더파일: <string.h>
함수 원형: size_t strlen(const char* str); - const char*타입의 문자열을 받아서 문자열의 길이를 반환하는 함수
char*가 가리키는 주소에서 부터 시작해서 \0이 문자가 나올때 까지의 문자들의 개수를 세서 최종 길이를 반환
strcmp함수
헤더파일: <string.h>
함수 원형: int strcmp(const char* str1, const char* str2)
strcmp 함수에 비교할 문자열을 넣어주면 결과를 정수로 반환(문자열을 비교할 때 대소문자를 구분함).
- -1: ASCII 코드 기준으로 문자열2(s2)가 클 때
- 0: ASCII 코드 기준으로 두 문자열이 같을 때
- 1: ASCII 코드 기준으로 문자열1(s1)이 클 때
https://dojang.io/mod/page/view.php?id=346
C 언어 코딩 도장: 41.2 문자열 비교하기
strcmp 함수를 사용하면 두 문자열이 같은지 비교할 수 있으며 함수 이름은 문자열을 비교하다(string compare)에서 따왔습니다(string.h 헤더 파일에 선언되어 있습니다). strcmp(문자열1, 문자열2); int strcmp(const *_Str1, char const *_Str2); 문자열 비교 결과를 반환 다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요. string_compare.c #include #include //
dojang.io
핵심이 되는 check 함수에서는 16 바이트 크기의 temp 버퍼에 입력받은 패스워드를 복사한 후 "SECRET_PASSWORD" 문자열과 비교 후 문자열이 같다면 auth 변수를 1로 설정하고 auth를 리턴함
line 10에서 strncpy 함수를 통해 temp 버퍼를 복사할 때, temp의 크기인 16 바이트가 아닌 인자로 전달된 password 문자열의 길이만큼을 복사하므로 argv[1]에 16 바이트가 넘는 문자열을 전달한다면 길이 제한 없이 문자열이 복사되어 스택 버퍼 오버플로우가 발생하게 됨

예시3) stack-3.c

strncmp함수
헤더파일: <string.h>
함수 원형: int strcmp(const char* str1, const char* str2, size_t n)
n에는 0보다 큰 값이 들어가야함, str1,str2 문자열보다 큰 값을 넣게되면 알아서 문자열의 전체를 비교
-
-1: ASCII 코드 기준으로 문자열2(s2)가 클 때
-
0: ASCII 코드 기준으로 두 문자열이 같을 때
-
1: ASCII 코드 기준으로 문자열1(s1)이 클 때

stack-1.c에서는 길이 검증이 없는 함수를 사용해 스택 버퍼 오버플로우가 발생했고, 이번에는 고정된 크기의 버퍼보다 더 긴 데이터를 입력받아 스택 버퍼 오버플로우가 발생함
예시4) stack-4.c

read 함수에서 받는 입력이 32바이트를 넘진 않지만, sprintf 함수를 통해 버퍼에 값을 쓸 때 "Your Input is: "문자열을 추가하며 만약 buf에 31바이트를 꽉 채운다면 "Your Input is: " 문자열이 앞에 붙어 총 길이가 32바이트를 넘게 됨

힙 오버플로우

read 함수를 통해 입력받는 길이인 100바이트가 input 버퍼의 크기인 40바이트보다 크기 때문에 힙 오버플로우가 발생

input 영역에서 버퍼 오버플로우가 발생해 hello의 메모리 영역까지 침범할 경우, line 16에서 hello 메모리를 출력할 때 "HI!" 문자열이 아니라 공격자에게 오염된 데이터가 출력됨
4-2. Out-Of-Boundary (OOB)
버퍼의 길이 범위를 벗어나는 곳의 데이터에 접근할 수 있는 취약점
올바르지 않은 값이 버퍼의 인덱스로 사용될 경우 발생
브라우저와 같은 대규모 최신 소프트웨어에서도 자주 발견되는 취약점
예시1) oob-1.c

buf의 길이는 10이므로 buf의 인덱스로 사용될 수 있는 올바른 값은 0 이상 10 미만의 정수임
하지만 line 10에서 입력받은 idx 값을 인덱스로 사용할 때 해당 값이 올바른 범위에 속해 있는지 검사하지 않음
C언어는 인덱스를 이용해 버퍼에 접근할 때 인덱스의 범위에 대한 별도의 경계 검사가 존재하지 않기 때문에 올바르지 않은 값을 사용한다면 buf의 영역 밖에 있는 값에 접근 가능

예시2) oob-2.c : oob-1.c에 코드 idx = idx % 10 추가
OOB의 발생 여부를 판단:
버퍼의 인덱스로 사용할 수 있는 올바른 값의 범위와 버퍼의 인덱스가 될 수 있는 모든 값의 범위를 비교
인덱스가 될 수 있는 값의 범위가 사용할 수 있는 올바른 값의 범위의 부분집합이면 안전
C언어에서는 피연산자가 음수라면 나머지 연산의 결과도 음수 가능

buf의 인덱스로 써야 하는 값의 범위: 0~9
buf의 인덱스로 쓸 수 있는 값의 범위: int 형의 범위 % 10
buf의 인덱스로 쓸 수 있는 값의 범위는 -9 ~ 9이므로 나머지가 음수가 되게 한다면 OOB를 발생

예시3) oob-3.c - idx가 음수일 경우 이를 양수로 바꿔주는 코드 추가
C언어에서 int형으로 표현 가능한 정수의 범위: -pow(2, 31) ~ pow(2, 31) - 1
int형은 32비트이기 때문에 총 pow(2, 32)개의 수 표현 가능
int형은 0을 포함하기 때문에 표현할 수 있는 음의 정수의 갯수와 양의 정수의 갯수가 다름
int 형에서 -pow(2,31)은 표현 가능하지만 pow(2,31)은 표현 불가

pow(2,31)은 표현 가능한 최대 정수보다 하나 더 크기 때문에 이는 -pow(2,31)과 같은 값
idx에 -pow(2, 31)을 넣었을 경우 line 14에서 절대값을 구하는 연산을 수행한 후에도 -2^31이 그대로 저장
line 15에서 idx = idx % 10을 할 때 idx에 음수가 저장되고, 이는 buf 배열의 올바른 인덱스 범위를 벗어나기 때문에 OOB가 발생

이를 근본적으로 막기 위해서는 idx를 int형이 아닌 unsigned int형으로 선언하거나,
인덱스를 입력받은 이후에 if (idx < 0 || idx >= 10)과 같은 경계 검사 구문을 추가해야 함
4-3. Off-by-one
버퍼의 경계 계산 혹은 잘못된 반복문의 연산자를 사용하는 등의 인덱스를 고려하지 않을 때 발생하는 취약점
반복문을 순회할 때 잘못된 비교 연산자를 사용(버퍼의 경계 계산 혹은 반복문의 횟수 계산 시 < 대신 <=을 사용하는 경우)하거나 인덱스가 0부터 시작하는 것을 고려하지 못했을 때 자주 발생하는 취약점
예시1) off-by-one-1.c

buf에 16바이트 문자열을 입력받은 후 buf와 sizeof(buf)의 값을 copy_buf 함수의 인자로 전달
copy_buf함수에서는 임시 버퍼 temp를 할당하고 반복문을 통해 buf의 데이터를 복사
반복문에서 i가 0일 때부터 sz일 때까지 총 sz + 1번 반복하여 sz + 1만큼 데이터가 복사되고, off-by-one 취약점이 발생
4-4. Format String Bug (FSB)
printf나 sprintf와 같은 함수에서 포맷 스트링 문자열을 올바르게 사용하지 못해 발생하는 취약점
"%x"나 "%s"와 같이 프로그래머가 지정한 문자열이 아닌 사용자의 입력이 포맷 스트링으로 전달될 때 발생

포맷 스트링을 사용하는 함수의 인자만 잘 검토하면 되기 때문에 다른 취약점들에 비해 막기 쉬움
최신 컴파일러에서는 포맷 스트링으로 전달되는 인자가 문자열 리터럴이 아닐 경우 경고 메시지를 출력하기 때문에 요즘에는 잘 발생하지 않는 취약점
그러나 프로그램에 큰 영향을 줄 수 있는 취약점이기 때문에 항상 염두해 두어야 함
표준 C 라이브러리에서 포맷 스트링을 사용하는 대표적인 함수들
printf, sprintf / snprintf, fprintf, vprintf / vfprintf, vsprintf / vsnprintf
예제1) fsb-1.c

"%x %d"와 같은 포맷 스트링을 문자열로 입력한다면, printf(buf)는 printf("%x %d")가 됨
printf("%x %d")에는 두 번째 인자와 세 번째 인자가 전달되지 않기 때문에 쓰레기 값을 인자로 취급해 출력
예제2) fsb-2.c

line 12에서는 포맷 스트링이 위치할 곳에 사용자의 버퍼가 위치하므로 포맷 스트링 버그가 발생.
이번에도 fsb-1.c 와 마찬가지로 "%x"나 "%d"와 같은 포맷 스트링을 입력하면 의도치 않은 값이 파일에 저장됨
예제3) fsb-3.c

4-5. Double Free와 Use-After-Free(UAF)
동적 할당된 메모리를 정확히 관리하지 못했을 때 발생하는 취약점
Double Free: 이미 해제된 메모리를 다시 한 번 해제하는 취약점
예제1) df-1.c

line 21에서 해제하는 메모리 a는 line 18에서 이미 해제된 메모리 포인터이나 이 프로그램을 Ubuntu 18.04 환경에서 실행해보면 정상적으로 종료됨
따라서 line 12에서 이미 free된 메모리 a에 대해 다시 free를 호출하는 일이 어떤 일을 발생시키는지는 정확히 모르지만, 해제된 메모리를 다시 해제하는 것이 불가능하지 않다는 사실을 알 수 있음
line 6에서 메모리를 할당했을 때 a가 저장하고 있는 값은 특정 힙 메모리의 주소임
그러므로 a를 free했을 때 시스템에 해당하는 힙 메모리 할당자의 구현에 따라 메모리가 해제됨
그러나 이 때 같은 포인터를 두 번 해제하는 것과 같은 비정상적인 일이 발생하면 공격자가 프로그램을 예상치 못한 실행 흐름으로 만들 수 있음
Use-After-Free(UAF): 해제된 메모리에 접근해서 값을 쓸 수 있는 취약점
예제1) uaf1.c

100 바이트 크기의 메모리 a 를 할당한 후 "Hello World!" 문자열을 복사
그 다음 메모리 a를 해제하고 새로운 100 바이트 크기의 메모리 b 를 할당
새로 할당된 메모리에는 strcpy 함수를 통해 메모리 b 에 "Hello Pwnable!" 문자열을 복사함
주의!
포인터 a에 저장된 메모리 주소 값은 바뀌지 않았다는 것과 메모리 a와 메모리 b가 같은 주소를 가리키고 있음
- 이미 해제되었던 메모리 a가 메모리 할당자로 들어가고, 새로운 메모리 영역을 할당할 때 메모리를 효율적으로 관리하기 위해 기존에 해제되었던 메모리가 그대로 반환되어 일어나는 일. 그러므로 이미 해제된 메모리 a에 접근하면 메모리 b가 같이 영향을 받기 때문에 프로그래머가 의도하지 않은 일이 발생할 수 있음

line 18에서 "Hello World!" 문자열을 복사하는 포인터는 b 가 아닌, 해제된 메모리 포인터인 a
이미 해제된 포인터 a와 새로이 할당한 포인터 b가 같은 메모리 영역을 가리키고 있기 때문에, 포인터 a에 "Hello World!" 문자열을 복사하고 포인터 b의 내용을 출력하면 "Hello World!" 문자열이 출력됨
4-6.초기화되지 않은 메모리
예제1) uninit1.c

main함수에서는 Person의 인스턴스를 선언한 후 name의 길이를 name_len 변수에 입력 받음
문제1. name에 할당된 메모리를 초기화하지 않음
read 함수는 입력받을 때 널 바이트와 같은 별도의 구분자를 붙이지 않으므로 이후 name 을 출력하는 부분에서 초기화되지 않은 다른 메모리가 출력 될 수 있음
만약 길이가 100보다 작으면 malloc 함수를 통해 메모리를 할당한 후 name_len만큼 입력받음
문제2. name_len 변수의 값이 100보다 크거나 같은 경우에 대한 예외 처리가 없음
이 경우 p.name 은 malloc 으로 할당된 값이 아니라 쓰레기 값이 되며 만약 공격자가 이 값을 조작할 수 있다면, line 16에서 read함수를 통해 데이터를 입력받을 때 원하는 메모리 주소에 원하는 값을 쓸 수 있게 됨
4-7.Integer issues
정수의 범위
정수의 범위에 대한 정확한 이해 없이 작성된 코드는 자주 문제를 일으키는데, 이는 때로 치명적인 취약점을 발생시킬 수 있음

size_t와 long 자료형은 아키텍쳐에 따라 표현할 수 있는 수의 범위가 달라짐
long 자료형은 32비트인 경우 int와 동일하고, 64비트인 경우 long long과 동일
size_t 자료형은 32비트일 때 unsigned int와 동일하며, 64비트일 때는 unsigned long과 동일
묵시적 형 변환
연산 시 연산의 피연산자로 오는 데이터들의 자료형이 서로 다를 경우, 다양한 종류의 형 변환이 일어나게 되는데 이 때 프로그래머가 자료형을 직접 명시해주지 않는다면 묵시적으로 형 변환이 발생함
묵시적 형 변환에 대한 규칙들
1. 대입 연산의 경우 대입 연산자의 좌변과 우변의 자료형이 다를 경우 묵시적 형 변환이 일어나게 되며 작은 정수 자료형에 큰 정수를 저장하는 경우, 작은 정수의 크기에 맞춰서 상위 바이트가 소멸됨
2. 컴퓨터가 int형을 기반으로 연산하기 때문에 정수 승격은 char이나 short같은 자료형이 연산될 때 일어남
3. 피연산자가 불일치할 경우 형 변환 이 일어남. 이 경우 int< long< long long < float<double< long double 순으로 변환되며, 작은 바이트에서 큰 바이트로, 정수에서 실수로 형 변환이 일어나게 됨
Integer Issues
예제 1) int-1.c

코드에서는 len 값을 사용자에게 입력받은 후 이후 len + 1 만큼 메모리를 할당받고 그 포인터를 buf에 저장 그리고 read 함수를 통해 buf에 데이터를 len만큼 입력받음
<len 값으로 -1을 넣었을 때 프로그램의 흐름>
len = -1이므로 line 12에서는 buf = malloc(0)이 호출되고, 리눅스에서는 malloc의 인자가 0이라면 정상적인 힙 메모리가 반환됨
이후 line 19에서 read(0, buf, -1)이 호출되는데 인자로 전달된 값은 int형 값 -1이고, read 함수의 세 번째 인자는 size_t 형이므로 묵시적 형 변환이 일어남
따라서 read 함수를 호출할 때, 32비트 아키텍처라고 가정하면 read(0, buf, pow(2, 32) - 1)이 호출되므로 지정된 크기의 버퍼를 넘는 데이터를 넣을 수 있어 힙 오버플로우가 발생함
예제 2)int-2.c

create_tbl 함수는 width, height 값과 초기화 데이터인 row 포인터를 인자로 받고 테이블을 초기화함
line 8에서 width * height 크기의 테이블을 할당한 후 각 행에 init_row 데이터를 복사하게 되나 width, height, n이 전부 unsigned int형의 변수이기 때문에 width * height가 pow(2, 32)를 넘어가면 의도하지 않은 값이 들어가게 됨
width가 65536이고 height가 65537이라고 가정한 경우 width * height의 값은 65536 * 65537 = pow(2, 32) + 65536이므로 실제로 저장되는 값은 65536 * 65537이 아닌 65536이 되나 memcpy 함수에서는 반복문을 순회하면서 메모리를 복사하기 때문에 버퍼 오버플로우가 발생하게 됩니다.
퀴즈 풀이-취약 line 찾기

line 8
if문을 보면, length < 0 || length + 1 > = MAX_SIZE인 경우 길이 검사를 통과
length는 int형 변수이므로 length에 int형의 최대 값인 0x7FFFFFFF를 넣을 경우 length < 0은 거짓이고 length + 1은 0x80000000이 되므로 음수로 취급되어 length + 1 >= MAX_SIZE의 검사도 거짓이 됨
따라서 line 13에서 read(fd, buf, 0x7FFFFFFF)가 호출되어 힙 오버플로우가 발생하게 됨
5. 로지컬 버그 중 대표적 예시들
5-1. Command Injection
사용자의 입력을 셸에 전달해 실행할 때 정확한 검사를 실행하지 않아 발생하는 취약점
5-2. 레이스 컨디션 (Race Condition)
여러 스레드나 프로세스의 자원 관리를 정확히 수행하지 못해 데이터가 오염되는 취약점
발생 원인과 공격 방법에 따라 메모리 커럽션 취약점으로도, 로지컬 취약점으로도 분류 가능
5-3. Path Traversal
프로그래머가 가정한 디렉토리를 벗어나 외부에 존재하는 파일에 접근할 수 있는 취약점
주로 소스 코드에서 "../"와 같은 경로 문자를 검사하지 않아 발생
6. 미티게이션
취약점의 공격을 어렵게 함
취약점의 존재 여부와는 무관하게 프로그램을 보호하는 방법에 대한 고민이 이루어지며 등장

Ex) Stack Smashing Protector (SSP):
스택 버퍼 오버플로우의 미티게이션 중 하나로 버퍼 오버플로우를 방지하기 위해 버퍼의 뒤에 랜덤한 값을 넣어두고 이를 특정 시점에 검사해 버퍼가
오염되었는지 확인
'Hacking Study > 포너블 스터디(2020-1) 과제' 카테고리의 다른 글
포너블 5주차 과제 (0) | 2020.05.24 |
---|---|
포너블 4주차 과제 (0) | 2020.05.17 |
포너블 3주차 과제 (0) | 2020.04.17 |
2주차 보충 - Format String Bug (0) | 2020.04.12 |
포너블 스터디 1주차 과제 (0) | 2020.04.03 |