PromleeBlog
sitemap
aboutMe

posting thumbnail
정보보안기사 - 1.7 레이스 컨디션과 포맷 스트링 공격 원리
Race Condition and Format String Attack Principles - InfoSec Engineer 1.7

📅

들어가기 전에 🔗

지난 1.6편에서는 버퍼 오버플로우를 통해 메모리를 넘치게 하여 시스템을 장악하는 방법을 알아보았습니다.
이번 시간에는 메모리 길이의 문제가 아니라,
논리적 결함
타이밍
을 이용한 두 가지 고급 공격 기법을 다룹니다.

하나는 "검사하는 시점"과 "사용하는 시점" 사이의 찰나의 틈을 노리는
레이스 컨디션
(Race Condition)이고, 다른 하나는 개발자가 무심코 사용한 printf 함수 하나로 메모리 내용을 훔쳐보는
포맷 스트링 버그
(Format String Bug)입니다.
이 두 공격은 코드가 겉보기에는 정상적으로 보여도 발생할 수 있어 더욱 위험합니다.

레이스 컨디션 [Race Condition] 🔗

레이스 컨디션
이란 두 개 이상의 프로세스가 공유 자원(파일, 메모리 등)을 동시에 접근하려 할 때, 접근 순서에 따라 실행 결과가 달라지는 현상을 말합니다.
보안에서는 이 타이밍을 조작하여 시스템을 공격하는 기법을 의미합니다.

TOCTOU [Time of Check to Time of Use] 🔗

레이스 컨디션 공격의 핵심 원리입니다.
시스템이 자원을
검사하는 시점
(Check)과 실제로 자원을
사용하는 시점
(Use) 사이에 시간 차이가 존재한다는 점을 악용합니다.
  1. Check
    : 프로그램이 파일에 쓸 권한이 있는지 확인합니다. (통과)
  2. (공격 시점)
    : 공격자가 재빨리 원본 파일을
    심볼릭 링크
    (Symbolic Link)로 바꿔치기합니다.
  3. Use
    : 프로그램은 아까 검사가 끝났으니 안전하다고 믿고 데이터를 씁니다.
결과적으로 프로그램은 엉뚱한 파일(예: /etc/passwd)에 데이터를 덮어쓰게 됩니다.

공격 시나리오: 임시 파일과 심볼릭 링크 🔗

SetUID가 설정된 프로그램이 /tmp 디렉토리에 임시 파일을 생성할 때 주로 발생합니다.
vulnerable_race.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
 
int main() {
    char *filename = "/tmp/temp_file";
    char *data = "root_data";
 
    // [Check] 파일이 존재하지 않는지(접근 가능한지) 확인
    if (access(filename, F_OK) != 0) {
        
        // [공격 포인트] Check와 Use 사이의 틈 (Race Condition)
        // 공격자가 이때 /tmp/temp_file을 /etc/passwd로 심볼릭 링크를 건다면?
        sleep(1); 
 
        // [Use] 파일 생성 및 쓰기
        FILE *fp = fopen(filename, "w");
        if (fp) {
            fprintf(fp, "%s", data);
            fclose(fp);
        }
    }
    return 0;
}
공격자가 access 함수와 fopen 함수 사이의 틈을 타서 /tmp/temp_file/etc/passwd를 가리키는 심볼릭 링크로 만들어 버리면, 이 프로그램은 관리자 권한으로 /etc/passwd를 덮어쓰게 됩니다.

대응 방안 🔗


포맷 스트링 버그 [FSB] 🔗

포맷 스트링 버그
(Format String Bug)는 C언어의 printf, fprintf, sprintf 등 포맷 스트링을 사용하는 함수에서 입력값 검증을 제대로 하지 않을 때 발생합니다.
개발자의 사소한 코딩 습관 차이가 치명적인 결과를 낳습니다.

취약한 코드 식별 🔗

fsb_example.c
#include <stdio.h>
 
int main(int argc, char *argv[]) {
    if (argc < 2) return 1;
 
    // [취약] 사용자의 입력을 포맷 인자 없이 그대로 출력
    // 사용자가 "%x" 같은 문자를 입력하면 메모리 내용이 출력됨
    printf(argv[1]); 
 
    // [안전] 포맷 스트링을 명시적으로 지정
    // 사용자가 "%x"를 입력해도 단순 문자열로 출력됨
    printf("%s", argv[1]); 
    
    return 0;
}
위의 취약한 코드(printf(argv[1]))에 사용자가 %x%n 같은 포맷 문자를 입력하면, 프로그램은 이를 문자가 아닌
명령어
로 해석해 버립니다.

주요 포맷 스트링 인자 분석 🔗

공격자는 다음과 같은 인자를 입력값으로 주입하여 메모리를 조작합니다.

공격 원리 예시 🔗

공격자가 입력으로 AAAA%x를 주입했다고 가정해 봅시다.
  1. 프로그램은 printf("AAAA%x")를 실행합니다.
  2. 화면에는 AAAA가 출력됩니다.
  3. 그 뒤에 %x를 만나면, printf 함수는 스택에서 다음 4바이트 값을 가져와 16진수로 출력합니다.
  4. 결과적으로 AAAA61ff0c 처럼 메모리 값이 노출됩니다.
만약 %n을 사용한다면, 공격자가 원하는 주소(예:
RET
주소)에 특정 값을 기록하여 쉘코드를 실행하게 만들 수 있습니다.

보안 대책 🔗

FSB와 레이스 컨디션은 코딩 습관을 바꾸는 것으로 예방할 수 있습니다.

1. 포맷 스트링 명시 (FSB 대응) 🔗

사용자 입력값을 출력할 때는 반드시 포맷 스트링(%s)을 명시해야 합니다.
secure_coding.c
// [X] 위험: 사용자가 포맷 문자를 넣으면 그대로 실행됨
printf(user_input);
 
// [O] 안전: 사용자가 입력한 내용을 단순 문자열로 취급함
printf("%s", user_input);

2. 가능성 제거 (Race Condition 대응) 🔗

레이스 컨디션은 파일 접근 권한 검사와 실제 사용을 분리하지 않는 것이 핵심입니다.
가능하면 임시 파일을 사용하지 않거나, 파일 디스크립터(fd)를 통해 열려 있는 상태를 유지하며 작업해야 합니다.

3. 컴파일러 보호 기법 🔗

최신 컴파일러(GCC 등)는 포맷 스트링이 상수가 아닌 변수로 올 경우 경고를 띄우거나, %n 사용을 제한하는 옵션을 제공하기도 합니다.
하지만 가장 확실한 것은 시큐어 코딩입니다.

결론 🔗

이번 시간에는 시스템의 허점을 파고드는 두 가지 공격 기법을 알아보았습니다.
다음 시간에는 시스템 공격의 결정체인 악성코드를 다루는
1.8 악성코드 분석: 웜, 바이러스, 랜섬웨어 비교
편을 진행하겠습니다.
비슷해 보이지만 확연히 다른 악성코드들의 전파 방식과 특징을 명확히 구분해 보겠습니다.

참고 🔗