CIDY
[System_Hacking] Logical Bug 본문
프로그램의 취약점에는 크게 두 가지가 있다. 1) 메모리 커럽션, 2) 로지컬 버그.
메모리 커럽션이 프로그램 자체의 메모리 관리 실수로 인해 발생하는 취약점인 반면, 로지컬 버그는 프로그램상의 논리적 오류로 인해 발생한다. 로지컬 버그의 경우 프로그램의 부정확한 동작을 초래하지만 크래쉬를 일으키지는 않는다.
프로그램을 구현할 때 음수나 범위를 넘은 인덱스 등에 대해 예외처리를 해 주지 않아 발생하는 오류 등이 대표적인 로지컬 버그이다.
Command Injection
인젝션은 사용자의 입력을 검증하지 않고 쉘 커멘드나 쿼리의 일부로 처리하여 공격자로 하여금 실행 흐름을 조작할 수 있도록 하는 취약점이다. 쉘에서 존재하는 다양한 메타문자들을 이용해 커맨드 인젝션을 트리거할 수 있다.
$ : 쉘 환경변수
&& : 이전 명령어 실행 후 다음 명령어 실행
; : 명령어 구분자 (다음 명령어 실행)
| : 명령어 파이핑
* : 와일드 카드(0개 이상의 모든 문자를 의미)
` : 명령어 치환
그 외에도 다양한 명령어가 존재한다.
예를 들어, 내 입력 ip로 받아 쉘에서 ping해주는 프로그램이 있다고 하면, 127.0.0.1 ; /bin/sh 를 입력해 쉘을 얻을 수 있는 것이다.
Race Condition
레이스 컨디션은 프로세스나 스레드 간 자원 관리 실수로 인해 발생하는 상태이다. 예를 들어 여러 스레드에서 뮤텍스가 걸려 있지 않아 공유 메모리에 잘못된 접근이 가능해질 경우, 프로그램이 의도와는 다르게 동작할 수 있다.
위 그림의 스레드1에서 len을 0으로 초기화한 뒤, 20을 더한다. 그런데 if문을 검사한 이후 스레드2가 수행된다면 len이 40인 상태로 read를 진행하게 되니 overflow가 발생하는 것이다.
이처럼 단일 스레드로만 보면 취약점이 없을 수 있지만, 두 개 이상의 스레드가 하나의 자원을 공유할 경우 취약해질 수 있다.
// gcc -o race1 race1.c -pthread
#include <stdio.h>
#include <pthread.h>
#include <time.h>
int count = 0;
void* counting() {
for(int i=0;i<10000000;i++) {
count += i;
}
}
int main(int argc, char* argv[]) {
pthread_t thread_id[3] = {0,};
pthread_create(&thread_id[0], NULL, counting, (void*)NULL);
pthread_create(&thread_id[1], NULL, counting, (void*)NULL);
pthread_create(&thread_id[2], NULL, counting, (void*)NULL);
sleep(1);
printf("%d\n", count);
return 0;
}
위 코드는 전역 변수인 count를 여러 스레드에서 참조하여 덧셈을 수행하는 코드이다. counting함수는 천만까지 덧셈을 진행하기 때문에 코드가 모두 실행되는데 시간이 걸리게 된다. 따라서 for문이 끝나기 전에 다른 스레드가 count변수를 참조해 연산을 진행하게 되므로 매 실행마다 다른 결과를 보이게 된다.
// gcc -o race2 race2.c -fno-stack-protector -lpthread -m32
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int len;
void giveshell() {
system("/bin/sh");
}
void * t_function() {
int i = 0;
while (i < 10000000) {
len++;
i++;
sleep(1);
}
}
int main() {
char buf[4];
int gogo;
int idx;
pthread_t p_thread;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while (1) {
printf("1. thread create\n");
printf("2. read buffer\n");
printf("> ");
scanf("%d", &idx);
switch (idx) {
case 1:
pthread_create( &p_thread, NULL, t_function, (void * ) NULL);
break;
case 2:
printf("len: ");
scanf("%d", &len);
if(len > sizeof(buf)) {
exit(0);
}
sleep(4);
printf("Data: ");
read(0, buf, len);
printf("Len: %d\n", len);
printf("buf: %s\n", buf);
break;
case 3:
if (gogo == 0x41414141) {
giveshell();
}
}
}
return 0;
}
case1: t_function함수를 스레드로 수행함
case2: len에 정수 입력받고 사이즈 검사 후 len만큼 buf에 read함 (언더플로우 발생)
case3: gogo가 알맞게 덮힌 경우 쉘을 줌
위 코드에서 t_function함수는 len이 천만이 될 때 까지 계속 증가시킨다. len변수에 뮤텍스가 걸려 있지 않기 때문에 4이하의 값을 입력하여 if문 검사를 통과한 뒤, sleep(4)를 수행하는 동안 다른 스레드에서 t_function을 수행하여 len이 증가하도록 할 수 있다.
사실 len이 signed int이므로 레이스 컨디션이 아니라 언더플로우 취약점을 이용해 오버플로우를 발생시킬 수도 있을 것 같다.
sudo apt-get --reinstall install libc6 libc6-dev
sudo apt install gcc-multilib
32비트로 컴파일 시 pthread.h헤더파일을 include하는데서 오류가 발생한다면 위 라이브러리를 설치해서 해결할 수 있다.
위와 같이 len에 4를 입력하였으나 12로 출력되고, overflow가 발생해서 gogo변수가 정상적으로 조작되어 쉘이 따이는 것을 볼 수 있다.
Path Traversal
path traversal은 프로그래머가 가정한 디렉토리 외부에 존재하는 파일에 접근할 수 있는 취약점이다.
예를 들어 /tmp/문자열 하위에 사용자가 입력한 문자를 덧붙여 cd명령어를 수행하는 프로그램이 있다고 가정하자. 이 때 ../과 같은 사용자 입력을 검증하지 않으면 프로그램의 의도에서 벗어나 /tmp보다 상위의 디렉토리로 이동할 수도 있게 되는 것이다.
Environment attack
환경 변수란 프로세스가 동작하는 방식에 영향을 미칠 수 있는 동적인 값들의 집합이다.
예를 들어, 사용자의 이름을 담고 있는 USER라는 환경 변수는 사용자가 바뀔 때 마다 변경되어야 한다.
만약 A라는 파일을 실행하기 위해 쉘에 A를 입력하여도 현재 디렉토리에 A가 없다면 A는 실행되지 않는다. A를 실행하기 위해서는 A의 절대 경로를 입력하여야 한다.
리눅스 명령어인 cat, sh등도 사실은 /bin/cat, /bin/sh라는 바이너리를 수행하는 것이다. 하지만 명령어를 실행할 때 마다 절대 경로를 입력하는 것은 매우 귀찮은 일이므로, 리눅스에서는 PATH라는 환경 변수를 제공한다. PATH에 지정된 경로에 있는 파일은 현재 디렉토리에 있는 파일처럼 실행할 수 있다. (리눅스 쉘 명령어의 작동 원리이다.)
// gcc -o environ1 environ1.c
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("Screen Cleaner\n");
system("clear");
return 0;
}
위 코드는 system("clear"); 를 수행하는 코드이다. clear명령어 역시 PATH 환경변수를 기반으로 실행되는 것이다.
export PATH=""
그럼 만약 위와 같이 PATH환경 변수를 지우고 위 코드를 수행하면 어떻게 될까? 당연히 clear명령어 바이너리를 찾지 못해 에러를 출력하게 된다.
ln -s /bin/sh ./clear
export PATH=""
만약 여기서 위 명령어를 통해 /bin/sh바이너리를 현재 디렉토리에 clear이름으로 심볼릭 링크를 걸고 위와 같이 환경변수를 아예 비워버리면 위 코드를 실행했을 때 ./clear가 /bin/sh에 링크되어 있기 때문에 쉘을 얻을 수 있게 된다.
따라서 프로그램 내부에서 특정 명령어를 사용해야 할 경우에는 system("/usr/bin/clear"); 와 같이 절대 경로를 사용하는 것이 안전하다.
LD_PRELOAD환경 변수를 통해 프로세스에 로드할 라이브러리 파일을 지정할 수 있다. (해당 프로세스 실행 시 LD_PRELOAD로 로드된 라이브러리가 먼저 참조된다.)
// gcc -o libc.so libc.c -fPIC -shared
#include <stdlib.h>
void read() {
execve("/bin/sh", 0, 0);
}
위 바이너리를 주어진 컴파일 옵션으로 컴파일하면 libc.so라는 라이브러리가 생성된다. 이 라이브러리를 LD_PRELOAD환경 변수에 걸어두면 모든 파일 실행 시 앞서 생성한 libc.so를 먼저 참조하게 된다.
위 라이브러리에서 쉘을 얻는 코드를 read라는 이름으로 정의해 두었으니, export LD_PRELOAD="./libc.so"를 수행하면 프로그램에서 read를 수행할 때 기존 라이브러리의 read함수가 아닌, 위에서 정의한 read함수가 수행되어 쉘이 얻어지는 것이다.
'Hack > DreamHack' 카테고리의 다른 글
[System_Hacking] Tasks (0) | 2023.05.20 |
---|---|
[System_Hacking] Linux Exploitation (0) | 2023.05.16 |
[System_Hacking] STB-lsExecutor (0) | 2023.04.09 |
[System_Hacking] Chatbot (0) | 2023.04.06 |
[System_Hacking] heapstack (0) | 2023.04.04 |