Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
schrodingers-toctou — 컴파일러가 생성한 메모리 로드를 탐지하여 안전한 C 코드가 TOCTOU 취약점으로 변질되는 것을 방지합니다. 자동화된 소스 감사, Unicorn 기반 바이너리 분석, 100개 이상의 프로젝트에 걸친 컴파일러/아키텍처/플래그 스윕을 포함합니다. | Kitploit
도구/GitHubGitHub/xoreaxeaxeax/schrodingers-toctou
Dynamic Analysis (Sandboxing)Static Code Analysis (SAST)Vulnerability AnalysisExploitationBinary AnalysisLearning & Education
GitHubxoreaxeaxeax/schrodingers-toctou

schrodingers-toctou

컴파일러가 생성한 메모리 로드를 탐지하여 안전한 C 코드가 TOCTOU 취약점으로 변질되는 것을 방지합니다. 자동화된 소스 감사, Unicorn 기반 바이너리 분석, 100개 이상의 프로젝트에 걸친 컴파일러/아키텍처/플래그 스윕을 포함합니다.

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
저장소 보기
944291개월 전Kitploit 검토 완료

Schrödinger's TOCTOU

"...'정상적인 컴파일러'의 정의는 점점 더 느슨해진다."

여러분이 실행하는 바이너리는 여러분이 작성한 프로그램이 아니다. 컴파일러 최적화기는 소스를 여러분이 결코 보지 못할 방식으로 다시 작성하며 — 그러한 변경 중 일부는 조용하고도 합법적으로 겉보기에는 안전한 코드를 취약한 바이너리로 바꿀 수 있다. 같은 코드 줄은 한 컴파일러에서는 안전하고 다른 컴파일러에서는 공격에 취약할 수 있으며, 소스에는 어느 쪽인지 알려 줄 것이 없다: 그 취약성은 중첩 상태에 있다가 빌드할 때에만 붕괴된다. Schrödinger's TOCTOU는 **컴파일러가 만들어 낸 로드**와 그것이 오픈소스 커널, 하이퍼바이저, 엔클레이브, 펌웨어, 및 라이브러리에서 발견되는 검사 시점-사용 시점(TOCTOU) 취약성에 미치는 광범위한 영향을 탐구한다. 우리가 살펴본 모든 곳에서 겉보기에 안전한 코드는 컴파일러의 변덕에 노출되어 있다. 그러나 그것들은 표본일 뿐 경계가 아니다; 같은 버그는 여러분의 코드에도 있을 가능성이 매우 높다.

Challenge

"간단한 것부터 시작해 보자."

이 함수는 *p를 몇 번 로드할까?```c unsigned int g(unsigned short *p) { short t = p; / copy *p into a local for safekeeping */ return (unsigned short)t - t; }

힌트: 답은 1입니다 — 소스는 `*p`를 `t`로 정확히 한 번 로드합니다.

이것을 [Compiler Explorer](https://godbolt.org/z/c5K9P4dPd) (`arm gcc 14.2.0`,
`-O2`)에 붙여넣고 `p`를 보관하는 `r0`부터의 로드 횟수를 세어 보세요.```asm
g:
        ldrh    r2, [r0]     # load *p, once
        ldrsh   r0, [r0]     # load *p, twice
        subs    r0, r2, r0
        bx      lr

소스에서 한 번의 로드, 바이너리에서는 두 번의 로드가 발생한다. 두 번째는 조작된 로드(invented load) 다 — 컴파일러가 만들어 낸, 당신이 결코 작성하지 않은 읽기다. 이는 C 추상 머신에서 합법적이며, 추상 머신은 두 번의 읽기 사이에 메모리가 변경될 수 없다고 가정한다. 하지만 그 메모리가 공격자가 쓰기 가능한 상태라면, 그 가정은 익스플로잇이 된다: 조작된 로드는 보안 검사 이후에 실행되어, 프로그래머가 닫았다고 믿었던 검사 시점-사용 시점(TOCTOU) 창을 조용히 다시 열어 버린다. 당신이 검증한 값과 당신이 사용하는 값은 더 이상 동일함이 보장되지 않는다 — 그 값을 다시 읽는 코드를 결코 작성하지 않았는데도 말이다.

공중에서 만들어진 버퍼 오버플로

이 도전 과제는 조작된 로드가 실제로 존재함을 증명한다. 그것이 어떻게 메모리 손상으로 이어지는지 살펴보자.

TOCTOU 취약점에서는 프로그램이 값이 안전한지 확인한 다음 그 값을 사용한다. 그러나 공격자가 그 두 읽기 사이의 아주 짧은 시간에 값을 변경할 수 있다면 악용의 창이 존재한다 — 무해한 값은 검사를 통과하지만 실제로 사용되는 것은 위험한 값이다:```c if (shared->len <= 20) // CHECK reads shared->len // ** attacker modifies shared->len ** memcpy(out, shared->data, shared->len); // USE reads it again: buffer overflow

교과서적인 해결책은 **스냅샷을 먼저 찍는 것**이다. 공격자가 건드릴 수 있는 데이터는 공격자가 닿을 수 없는 로컬(local)로 복사한 다음, 그 로컬 외에는 아무것도 신뢰하지 않는 것이다. 일단 `len`이 로컬에 들어가면 고정된다. 공유 메모리를 두고 경쟁하는 공격자가 더 이상 그 값을 건드릴 수 없기 때문에, 검사와 복사가 동일한 값을 보는 것이 보장된다. 아래 `receive`의 코드가 바로 이 방식으로 TOCTOU를 해결한다. 메시지를 스냅샷으로 찍고, 스냅샷을 검증한 뒤, 검증된 복사본을 `slot`에 게시하여 소비자가 전달할 수 있게 한다.```c
#include <string.h>

struct message {
    int  len;          /* payload length */
    char data[20];     /* payload        */
};

struct message slot;   /* the most recently validated message */
char out[20];          /* fixed 20-byte destination           */

void receive(struct message *shared) {
    struct message local = *shared;    /* 1. snapshot untrusted input   */
    if (local.len <= 20)               /* 2. validate the snapshot      */
        slot = local;                  /* 3. publish the validated copy */
}

void forward(void) {                   /* the time of use, later        */
    memcpy(out, slot.data, slot.len);  /* slot.len was checked <= 20 ... right? */
}

소스에 따르면 이는 정확하다. len은 정확히 한 번만 읽힌다 — 스냅샷으로 — 따라서 <= 20 검사를 통과시키는 값은 slot에 게시된 값이다. TOCTOU 창은 닫혀 있고 코드는 안전하다.

하지만 그렇지 않다. x86-64 gcc -O2에서, receive는 이 값을 원본 공유 메모리에서 두 번: 한 번은 검사를 통과시키는 스칼라로, 그리고 다시 slot에 게시되는 대량 복사의 일부로 읽는다:```nasm receive: cmp DWORD PTR [rdi], 20 ; READ #1: the CHECK reads shared->len directly movdqu xmm0, XMMWORD PTR [rdi] ; READ #2: the bulk copy re-reads it (len is byte 0) mov rax, QWORD PTR [rdi+16] ; (the bulk copy's tail: struct bytes 16-23) jg .L1 ; len > 20? skip the publish mov QWORD PTR slot[rip+16], rax ; (publish that tail) movaps XMMWORD PTR slot[rip], xmm0 ; and publish the TOCTOU-vulnerable snapshot .L1: ret forward: movsx rdx, DWORD PTR slot[rip] ; copy size = slot.len, the unchecked READ #2 value mov esi, OFFSET FLAT:slot+4 ; src = slot.data mov edi, OFFSET FLAT:out ; dst = out[20] jmp memcpy ; copies slot.len bytes into out[20]

검사는 READ #1에서 실행되고, `slot.len`에 들어가는 값은 READ #2입니다. 그 사이에 `len`을 뒤집는 공격자는 `<= 20` 검사에 안전한 값을 통과시키면서도 과대한 값을 `slot`에 게시합니다 — 그러면 `forward`는 그 많은 바이트를 `out[20]`으로 복사하는데, 이는 스냅샷이 막으려던 바로 그 오버플로가 옵티마이저에 의해 재도입된 것입니다.

이것은 [`poc/example.c`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/poc/example.c)에서 완전한 개념 증명으로 구체화됩니다. 이 코드는 정식 TOCTOU 방어 접근 방식을 사용합니다: 신뢰할 수 없는 `message` 구조체는 수정할 수 없도록 `local`로 스냅샷되고, 스냅샷의 `local.len`은 버퍼 용량에 대해 검증되며, 검증된 복사본만 `slot`에 게시됩니다. 이후 소비자가 `slot.len` 페이로드 바이트를 고정 버퍼로 복사합니다. 동시에 공격자는 `shared->len`을 레이스합니다. 컴파일러의 예기치 않은 발명된 로드가 대량 게시를 위해 `shared->len`을 다시 읽으므로, 검사가 통과했음에도 `slot.len`은 공격자의 과대한 값을 담게 됩니다 — 프로그래머가 막으려던 TOCTOU를 재도입하면서, 공중에서 튀어나온 것 같은 불가능해 보이는 버퍼 오버플로를 만들어냅니다.

## Cause

> *C가 기계 코드에 도달할 때쯤, 그것은 프론트엔드 로어링, IR 최적화, 레지스터 할당, 백엔드 코드 생성에 의해 다시 형성됩니다 — 보이지 않는 결정을 내리는 깊고 다단계 파이프라인입니다. 탓할 단 한 단계는 없습니다. 발명된 로드는 전체 파이프라인의 창발적 속성이지, 어떤 부분의 버그가 아닙니다.*

이 시점에서: 컴파일러는 *실제로* 발명된 로드를 생성할 수 있으며, 버그를 막기 위한 바로 그 관용구 — 스냅샷, 검증, 사용 — 가 그 버그를 재도입합니다. (실제로 취약한지 알기 위한) 다음 단계는 *언제* 발생하는지를 규명하는 것입니다. 알고 보니 그건 어렵습니다.

[`cat-states/`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states)에서는 그것이 실재하며 어디에나 존재함을 보여주는 개념 증명을 찾습니다:

| 메커니즘 | 툴체인 | 대상 |
|---|---|---|
| [**Rematerialization**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#rematerialization-class-1) | GCC, Clang, ICX, ICC, MSVC | x86-64, i386, m68k, VAX, MSP430 |
| [**Width-mismatch reload**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#width-mismatch-reload-class-2) | GCC | ARM, MIPS, MIPS64, RV64, s390x |
| [**Bulk-vs-scalar overlap**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#bulk-vs-scalar-overlap-class-3) | GCC, Clang, ICX, MSVC | x86-64, ARM, AArch64, AVR, Xtensa, SPARC, PPC64, s390x, MIPS64, RV64, m68k, MSP430, VAX, HPPA |
| [**Cross-class reload**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#cross-class-reload-class-4) | GCC | x86-64, s390x |
| [**CISC mem-op fold**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#cisc-alu-mem-op-fold-class-7) | GCC, Clang | m68k, MSP430, s390x, VAX, 6502 |
| [**Byte-order reload**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/main/cat-states/README.md#byte-order-divergent-reload-class-8) | GCC | s390x |
도구 다운로드