
```Python
x = 10 # 변수 x는 값 10을 저장하는 심볼입니다.
def square(n):
return n ** 2 # square 함수는 입력값을 제곱하여 반환하는 심볼입니다.
```
- x와 square는 프로그램에서 사용되는 심볼(변수와 함수 식별하기 위해 사용됨) ```c
void foo(int* ptr) {
// ptr과 관련된 작업 수행
}
int main() {
int* p = malloc(sizeof(int));
foo(p);
return 0;
}
```
- 함수 foo는 포인터 인자 ptr을 받지만, ptr의 입력값에 대한 사전 조사나 제약 조건 명시 X
- 이는, ptr이 under-constrained pointer이고, 함수에서 처리하는 합법적인 입력 값 집합이 과도하게 추정될 수 있음을 의미함
- main 함수는 malloc을 사용하여 포인터 p를 할당하고 foo에 인자로 전달
- foo 함수는 ptr의 입력값에 대한 사전 조건 명시 안해서, ptr에 대한 합법적인 입력 값 집합이 과도하게 추정될 수 있으며, 이는 aliasing 관계 분석에 영향 줄 수 있음 ```c
void manipulateData(int* ptr1, int* ptr2) {
*ptr1 = 10;
*ptr2 = 20;
}
int main() {
int* p1 = malloc(sizeof(int));
int* p2 = malloc(sizeof(int));
manipulateData(p1, p2);
return 0;
}
```
- manipulateData 함수는 두개의 포인터 인자 ptr1과 ptr2를 받아서, 그들이 가리키는 값 수정함
- main함수에서는 malloc 사용해 p1과 p2라는 두개의 포인터를 할당하고, manipulate함수에 인자로 전달
- 이 경우, p1과 p2 포인터는 제약 조건이 있는 포인터임
- malloc을 사용하여 각각 다른 메모리 위치에 할당되기 때문에, 포인터들이 서로 다른 메모리 위치를 가리킨다는 것 알 수 있음 ```C
DataPacket *packet;
int numHeaders;
PacketHeader *headers;
sock=AcceptSocketConnection();
ReadPacket(packet, sock);
numHeaders =packet→headers;
if (numHeaders > 100) {
ExitError("too many headers!");
}
headers = malloc(numHeaders * sizeof(PacketHeader); // 이 부분
ParsePacketHeaders(packet, headers);
```
- 코드는 패킷에 너무 많은 헤더가 포함되어있지 않은지 확인하기 위해 검사 수행
- 그러나, numHeaders는 부호있는 int로 정의되어서, 음수가 될 수도 있음
- **수신 패킷이 -3과 같은 값을 지정하면 malloc 계산은 음수를 생성함**(각 헤더가 최대 100바이트인 경우, -300)
- 이 결과, malloc()에 제공되면 먼저 size_t 유형으로 변환 (∵ void \*malloc(size_t size))
- **이 변환에서, 4,294,966,996(40억)과 같은 큰 값이 생성되어 malloc()이 실패하거나, 매우 많은 양의 메모리 할당될 수 있음**
- 따라서, 공격자가 적절한 음수를 사용하면 매우 작은 양수를 사용하도록 malloc()을 속일 수 있으며, 이는 예상보다 훨씬 작은 버퍼를 할당하여 잠재적으로 버퍼 오버플로우로 이어질 수 있음 ```C
char buf[10], cp_buf[10];
fgets(buf, 10, stdin);
strcpy(cp_buf, buf);
```
- 프로그래머는 fgets()가 반환될 때, buf에 길이 9 이하의 null-terminated한 문자열이 포함될거라 예상하지만, I/O 오류가 발생하면, fgets()는 buf를 null로 종료하지 않음.
- I/O 오류: File/Read/Write/Format Errors
- 앞 3개는 권한이나 파일 손상된 경우
- Format Errors: 입력 데이터의 형식이 예상과 다른 경우. fgets()는 개행 문자를 포함하여 지정한 최대 길이만큼의 문자열을 읽어들이는데, 이를 초과하는 문자열이 입력된 경우, 형식 오류가 발생할 수 있음
- 또한, 문자를 읽기 전에 파일 끝에 도달하면 fgets()는 buf에 아무것도 쓰지 않고 반환함
- 이 두 상황 모두에서, **fgets()는 NULL을 반환하여 비정상적인 일이 발생했다는 신호를 보내지만, 이 코드에서는 경고 감지 못함**
- **buf에 null 종결자가 없으면 strcpy()에 대한 후속 호출에서 버퍼 오버플로우 발생 가능** ```C
#include <stdio.h>
void printWrapper(char *string) {
printf(string);
}
int main(int argc, char **argv) {
char buf[5012];
memcpy(buf, argv[1], 5012);
printWrapper(argv[1]);
return (0);
}
```
- printWrapper()함수에서 printf()를 호출하기 때문에 악용될 수 있음 ```C
int count;
printf("Hello, world!%n", &count);
printf("The number of characters printed so far is: %d\n", count);
```
- *%n*은 "Hello, world!" 문자열 출력하기 전까지 출력된 문자의 수(13)를 *count*변수에 저장
- 두번째 printf문에서 count 변수의 값 출력하면 "The number of characters printed so far is: 13" 출력됨 ```C
#include <stdio.h>
int main() {
char* name = "John";
printf("Hello, %s!\n", name);
return 0;
}
```
- x86 아키텍처에서 위 코드를 컴파일하고 실행하면, printf 함수를 호출할 때 스택에 인자가 push되고 호출한 이후에, 스택 포인터가 조정됨.
- 이때, stack correction 값을 검사하면 전달된 인자의 개수를 알 수 있음
- stack correction 값=8: 인자 1개; 12: 인자 2개
- Stack Correction(스택 보정)
- 함수 호출 시, 스택 포인터(SP)를 조정하여 스택에 push한 인자들을 제거하는 과정. 스택 보정은 호출된 함수가 실행을 마치고 반환될 때, 스택 상태를 이전 호출 상태로 되돌리기 위해 필요한 작업임
- **호출 규약(call convention)**에 따라 수행됨. 호출 규약은 함수 호출 시, 인자 전달/반환값 처리/스택 관리 등의 규칙을 정의한 규약
- 대표적인 호출 규약 : cdecl 규약- 호출된 함수에서 스택에 푸시한 인자들을 호출 이후에 호출자가 제거하는 스택 보정 필요함
- 스택 보정 값 : 스택 포인터를 조정하는데 필요한 바이트 수 ```C
srand(time());
int randNum = rand();
```
- 공격자는 PRNG에 사용되는 시드 예측하여, 생성된 난수 스트림도 예측 가능
- srand(): rand함수의 시드값 초기화해주는 기능
- rand()함수는 srand()함수에 의존적임
- srand의 s는 seed라는 뜻. seed 값에 따라 rand값이 바뀌게되며, stdlib.h 헤더파일에 존재함 ```python
x = 10
y = 5
z = x + y
```
- z 변수는 x와 y 변수의 합으로 계산됨
- 컨텍스트 민감성이 낮은 분석: x와 y 변수의 값에 관계없이 z는 항상 15로 간주
- 분석이 실행되는 동안 변수의 값에 대한 상세한 정보 고려하지 않고, 단순히 코드의 구조 확인
- 컨텍스트 민감성 높은 분석: x와 y 변수의 값에 따라 z의 값이 달라질 수 있다는 것 인지
- 더 정확한 분석결과, but 분석의 복잡성과 처리 속도 증가 초래 ```python
x = 5
y = 10
if x > y:
z = x + y
else:
z = x - y
```
- 위 코드에서 abstract state를 사용하여 실행 상태를 표현할 수 있음
- abstract state는 변수의 추상화된 값을 포함하므로, x, y, z를 아래와 같이 표현할 수 있음
```python
x: [x > 5, x <= 5]
y: [y > 10, y <= 10]
z: [x + y, x - y]
```
- 위 abstract state에서 x, y 변수의 추상화된 값은 각각 x>5와 y>10임
- 이는 x가 5보다 크고, y가 10보다 크다는 제약 조건 나타냄. z 변수의 abstract state는 조건문에 따라 x+y 또는 x-y로 구체화됨 취약점의 발견의 맥락에서 그 효과에도 불구하고, 최근 바이너리 프로그램 분석 접근 방식은 정확성과 확장성의 절충안으로부터 제한되고 있다. 본 논문에서, 저자는 정적/동적 취약점 탐지 기술을 돕는 취약점 속성 셋을 식별하여, 정적분석의 정확성과 동적 분석의 확장성을 향상시키고자 한다. 정적/동적 기술을 조심스럽게 합치면서, 저자는 큰 스케일에서의 real world 프로그램에서 이러한 속성을 보여주는 취약성을 탐지한다.
저자는 바이너리 코드 분석에서 몇가지 발전을 만들며 기술을 구현했고, arbiter라는 프로토타입을 만들었다. 저자는 네가지 common vulnerability classes에서 큰 스케일의 평가로 저자의 접근방식의 효과성을 증명한다: CWE-131(버퍼 사이즈의 부정확한 계산), CWE-252(check되지 않은 반환 값), CWE-134(uncontrol된 포맷 스트링), CWE-337(난수 생성기의 예상가능한 seed). 저자는 우분투 저장소의 76516개 이상의 x86-64 바이너리들에서 이 접근방식을 평가했고, 컴파일 과정에서 프로그램에 삽입되는 결함을 포함하여 새로운 취약점을 발견했다.
정적 기법: 데이터 흐름 복구, 프로그램 슬라이싱 사용
동적 기법: 취약성 특성에 따라 선택됨
속성들
- P1
- ARBITER가 지원할 수 있는 취약성 범위 정의
- 데이터 흐름에서 복잡한 value 관계를 추론할 수 있는 DSE 사용
- P2
- 작은 프로그램 슬라이스를 실행하여 확장성 높이기 위한 사전 조건 정의
- 이는, 동적 분석에 필요한 상태 정보가 부족한 프로그램 슬라이스 문제를 발생할 수 있음
- P3
- P2의 문제를, P3가 aliasing 문제의 대부분을 무시할 수 있는 능력으로 인해 DSE 기법인 UCSE 적용함
- P3 없으면, ARBITER는 정적 분석 중에 aliasing을 계산하거나 보수적인 aliasing으로 인해 높은 false positive 수용해야했을 것임
Property-Compliant(PC) 취약점: 정적/동적-심볼릭 문맥에서 이런 특성을 따르는 취약점
- ARBITER는 이런 취약점 추론 가능
PC 취약점을 포함하는 CWE 항목
- Table 3
- ![[Pasted image 20240109222151.png]]
아래 부터는 배경지식 섹션 참고하는 게 나음
```C
int secret = 42;
printf(user_input);
```
- user_input은 사용자가 제어할 수 있는 문자열임. 사용자가 포맷 지정자를 포함한 악의적인 문자열 입력하면, 다음과 같은 문제 발생 가능
- 메모리 노출: 사용자가 `%s`나 `%x`와 같은 포맷 지정자를 사용하여 스택이나 임의의 메모리 위치에서 값을 읽을 수 있
- 메모리 손상: 사용자가 `%n`과 같은 포맷 지정자를 사용하여 메모리 주소에 값을 쓸 수 있고, 이를 통해 중요한 프로그램 변수를 수정하거나 메모리 영역을 덮어쓸 수 있음
- 서비스 거부: 사용자가 특정 포맷 지정자나 큰 너비/정밀도 값을 사용하여 프로그램이 과도한 리소스를 소모하거나 충돌을 일으킬 수 있음
srand 호출에 사용되는 시드가 시간의 반환 값과 커널의 난수 생성기에서 읽은 값과 같이 결합되어(e.g., xor연산 사용) 생성돼서 발생srand(time(NULL));specify_sources(binary): 프로그램 위치들의 세트(e.g., 함수명 또는 주소) 및 변수 사양들(e.g., 리턴 값, 함수에 대한 특정 인수) 반환해야함. 이 위치들은 ARBITER의 정적 분석에서 잠재적으로 취약한 흐름들 찾기 위한 source로 사용됨specify_sinks(binary): source들과 유사하게, 잠재적인 취약한 흐름들 찾기 위한 sink로서 사용될 변수들의 집합 지정apply_constraint(state, sources, sink): 잠재적으로 취약한 흐름을 감지하고 상징적으로 분석할 때 이 함수 이용해서 심볼릭 취약점 조건에 대한 흐름 확인 ```python
def specify_sources(binary):
return {} # defaults to function arguments
def specify_sinks(binary):
return { "malloc": 0 } # first argument to malloc
def apply_constraint(state, sources, sink):
for source in sources:
if source.length < sink.length: #equalize bit length of source and sink
source = source.zero_extend(sink.length-source.length)
state.solver.add(sink < source)
``` ```python
def specify_sources(binary): # specify return values (index 0 in arbiter) of security-relevant system calls
return {
'access': 0, 'chdir': 0, 'chown': 0,
'chroot': 0, 'mmap': 0, 'prctl': 0,
'setrlimit': 0, 'stat': 0, 'setuid': 0,
'setgid': 0, 'setsid': 0, 'setpgid': 0,
'setreuid': 0, 'setregid': 0, 'setresuid': 0,
'setresgid': 0
}
def specify_sinks(binary): # arbiter shorthand for the return value of the _caller_ of these functions
return [
'access', 'chdir', 'chown',
'chroot', 'mmap', 'prctl',
'setrlimit', 'stat', 'setuid',
'setgid', 'setsid', 'setpgid',
'setreuid', 'setregid', 'setresuid',
'setresgid'
]
def apply_constraint(state, sources, sink):
if state.satisfiable(
extra_constraints=[sources[0] == -1]):
# target function allows both negative and positive values (indicating absence of checks)
state.solver.add(sources[0] == 0)
else: # reject the state by making it unsatisfiable
state.solver.add(False)
``` ```python
def specify_sources(binary):
return {} # defaults to function arguments
def specify_sinks(binary):
# the format argument of string formatters
return {
'printf': 0, 'fprintf':1, 'dprtinf': 1,
'sprintf': 1, 'vasprintf': 1, 'snprintf': 2,
'fprintf_chk': 2, 'dprintf_chk': 2,
'sprintf_chk': 3, 'vasprintf_chk': 2,
'asprintf_chk': 2, 'snprintf_chk': 4
}
def apply_constraint(state, sources, sink):
# check for the format string in the ELF
addr = state.solver.eval(sink, cast_to=int)
elf_address = \
state. project.loader.find_section_containing(addr)
if elf_address is not None:
state.solver.add(False)
``` ```python
def specify_sources(binary):
# return value of time()
return {"time": 0}
def specify_sinks(binary):
# first argument of srand()
return {"srand": 0}
def apply_constraint(state, sources, sink):
# no constraints --- purely a data flow problem
pass
```