
GNU IFUNC는 CVE-2024-3094의 진짜 원인입니다.
[주황색 웹사이트][hn]의 장난꾼들이 나를 괴롭히고 있는 것 같군요. 아래에 몇 가지 엄선된 답변을 준비했지만, 먼저 도전장을 내겠습니다: ifunc 없이 이 공격을 시연할 수 있는 첫 번째 사람에게 제 돈 500달러를 보내겠습니다. 진심으로 흥미롭고, 깨달음을 얻기 위해 기꺼이 지불할 의향이 있습니다. 이 저장소를 포크하고 작동하는 PoC와 함께 PR을 제출하세요. 승리하면 개인적으로 우편 주소를 요청하겠습니다. 이제 답변입니다:
이건 잘못된 나무를 짖는 격이다.
이보게, 나는 잘못된 나무에 살고 있어서 누구에게든 짖을 수 있네.
익스플로잇에 필수적이지 않았다,
당신이 익스플로잇에 필수적이지 않아!
root로 실행되는 임의 코드에 대한 보호를 추가하고 싶다면 항상 selinux가 있다.
일단 로드되면, 이 공격은 더 이상의 syscall 경계를 넘을 필요가 없었다. 그래서 맞다, 우리는 "보너스" root 세션을 제한할 수 있었겠지만, 여전히 머신에 초대받지 않은 손님이 있었을 것이다!
이게 뭐야, 빌보의 생일 파티야?? 파티 용무 외에는 입장 금지!!
- IFUNC가 main 이전에 코드를 실행하는 유일한 방법은 결코 아니다.
하지만 그것은 메모리 보호가 설정되기 전에 코드를 실행하는 불필요한 방법이다.
- 그들이 제시하는 대안은 프로세스 수명 동안 함수 포인터가 쓰기 가능한 상태로 남기 때문에 논란의 여지가 있지만 덜 안전하다,
mprotect로 즉석에서 처리할 수 있다! 위의 Modifying LD_PRELOAD 하위 섹션 위의 마지막 문장을 보라.
그래, 이 블로그는 잘못 인도되었다.
실례지만 이 블로그는 인도되지 않았다. 이 모든 장난은 내가 직접 한 것이다! 아무도 나를 이렇게 멍청하게 만들도록 속이지 않았다.
IFUNC는 [클라이언트] 소프트웨어 자체에서 구현되어야 한다,
@CountWSS 💯 당연하지
일련의 노골적인 프로세스 실패가 Github 메인테이너로부터 ...
이것이 내가 진지하게 답변할 유일한 지점이다:
나는 이것이 xz-utils 메인테이너에게 지극히 불공평하고 커뮤니티에 상당히 위험하다고 생각한다. 이것이 그의 실수로 시작되었다고 생각하는 것은 말이다. 그것은 아무도 이 프로젝트를 유지하는 것을 돕는 데 신경 쓰지 않은 것에서 시작되었다. 공격자는 ifunc를 기술적 취약점으로 의존했고, 우리의 집단적 xz-utils 방치는 사회적 취약점이었다. 나는 Mr. Collin의 행동을 수년간의 영웅적인 커뮤니티 봉사에 대한 헌신 이외의 다른 것으로 보는 것이 부끄럽다고 생각한다.
또한 Bruce Schneier도 나와 동의한다 그러니... 미안하지만, 당신의 주장은 끝났다.
언어가 필요 이상으로 거칠었을 수 있다
내 친구들이 처음에 이걸 얼마나 순화시켰는지 믿지 못할 것이다.
Linux 배포판들은 OpenBSD가 그들의 엉망진창에 순응하고 적응하기를 기대할 만큼 자신들을 높이 평가해서는 안 된다
@debazel!!! Me gusta.
완전 헛소리.
좋아, 그 부분은 정확하다.
xz-utils를 [CVE-2024-3094][nvd]로 비난하는 것을 그만둬야 하는 이유. 또한 내 ETSA Talk도 확인해 보세요!

CVE-2024-3094, 더 흔히 "The xz-utils backdoor"로 알려진 것은 글로벌 사이버보안에 대한 아슬아슬한 위기였다. 이 공격이 [Andres Freund][freund]에 의해 때맞춰 발견되지 않았다면, 우리 행성의 대부분의 SSH 서버가 이 공격 배후의 세력에게 root 접근을 허용하기 시작했을 것이다.
불행히도, 너무 많은 분석이 [악성 코드][JiaT75]가 어떻게 xz-utils 저장소에 들어왔는지에 초점을 맞추고 있다. 대신, 나는 중요한 오픈 소스 소프트웨어의 두 가지 오래된 설계 결정이 이 공격을 가능하게 했다고 주장하고 싶다: [OpenSSH를 SystemD에 링크][biebl]하는 것, 그리고 [GNU IFUNC][sourceware]의 존재.
시작하기 전에: 이 논의의 상당 부분은 Linux의 동적 링킹의 복잡성을 다룬다.
복습이 필요하다면, dynamic_linking.md를 확인하라.
xz-utils 백도어의 고수준 세부 사항을 설명하는 훌륭한 글들이 많이 있다. Dan Goodin의 [What we know about the xz Utils backdoor that almost infected the world][goodin1]와 Sam James의 [FAQ on the xz-utils backdoor (CVE-2024-3094)][thesamesam] gist 같은 것들이다. 여기서 그 모든 것을 다시 해시할 필요는 없으므로, 이 글의 목적을 위해 매우 조악한 요약은 다음과 같다:
## Linux 배포판은 왜 OpenSSH를 수정하는가?
짧게 답하자면, 그럴 수밖에 없기 때문이다. OpenSSH는 OpenBSD 커뮤니티에 의해, OpenBSD 커뮤니티를 위해 개발되며, 그들은 Linux에 대해 전혀 신경 쓰지 않는다. [Portable OpenSSH][mindrot] 프로젝트는 OpenBSD 전용 구성 요소를 일반적인 POSIX 구성 요소로 대체하고, 해당되는 경우 일부 플랫폼별 코드를 적용하는 패치 모음으로, 최선의 노력(best-effort)으로 유지된다. 실제로 SSH의 소프트웨어 공급망은 대략 다음과 같은 모습이 된다:```mermaid
flowchart TD
subgraph OpenBSD Folks
A[OpenBSD]
B[OpenSSH]
H[improvements]
end
B-->A
A-->H
H-->B
B-->C
C[Portable OpenSSH]
subgraph Debian Folks
D[Debian SSH]
G[improvements]
end
C-->D
D-->G
G-->C
subgraph Fedora Folks
J[Fedora SSH]
K[improvements]
end
C-->J
J-->K
K-->C
OpenBSD의 OpenSSH 버전은 다른 모든 것의 업스트림이며, 대부분의 개선 사항은 OpenBSD 커뮤니티 내에서 이루어집니다. 이러한 변경 사항은 Portable OpenSSH 프로젝트로 다운스트림되어, OpenBSD에 특화되지 않은 방식으로 새로운 기능을 재구현하려고 시도합니다. 이것이 바로 SSH가 Linux, macOS, FreeBSD, 심지어 Windows와 같은 플랫폼에서 작동할 수 있게 해주는 것입니다.
하지만 거기서 끝나지 않습니다. 일부 운영 체제는 Portable OpenSSH가 제공하는 것 이상으로 추가적인 커스터마이징을 적용합니다. 예를 들어, Apple은 macOS 비밀번호 관리자와의 통합을 돕기 위해 ssh-add에 [--apple-use-keychain][keith] 플래그를 추가합니다.
CVE-2024-3094의 경우, Fedora와 Debian은 [sshd 재시작과 관련된 경쟁 조건][schmidt]을 수정하기 위해 OpenSSH 포크에 대한 자체 [SystemD 패치][biebl]를 유지 관리했습니다. 따라서 SSH의 실제 공급망은 다음과 같이 보이기 시작했습니다:```mermaid
flowchart TD
A[OpenSSH]
B[Portable OpenSSH]
C[Debian SSH]
D[Fedora SSH]
A-->B
B-->C
B-->D
C<-->|SystemD Patches|D
이 패치들은 Portable OpenSSH에 들어가지 않았습니다. Portable
OpenSSH 측이 ["libsystemd에 의존성을 두는 데 관심이 없다"][djmdjm]고
했기 때문입니다. 그리고 업스트림 OpenSSH에도 들어가지 않았습니다.
OpenBSD는 SystemD를 지원할 필요가 전혀 없기 때문입니다.
### "관심사의 분리"에 대한 우려
이것은 무해해 보이지만, 오픈 소스, 특히 Linux에서 훨씬 더 큰 문제의 한
예시입니다. 운영 체제의 핵심 구성 요소들이 서로를 모르고, 서로 대화하지
않는 사람들에 의해 개발되고 있습니다.
* SystemD를 위해 OpenSSH를 패치한 사람들은 libsystemd가 xz-utils에
의존한다는 것을 알았을까요 (혹은 신경 썼을까요)?
* SystemD 측 사람들은 xz-utils가 ifunc를 사용하기 시작했다는 것을
알았을까요 (혹은 신경 썼을까요)?
* OpenSSH 측 사람들은 ifunc라는 것이 있다는 것을 알았을까요 (혹은 신경
썼을까요)? OpenBSD에서는 확실히 그런 것은 없습니다.
어떤 의미에서 이러한 소통의 단절은 오픈 소스의 특징입니다. 나는 당신에게
귀찮게 하지 않고도 당신의 작업을 내 필요에 맞게 적용할 수 있습니다. 하지만
이는 또한 핵심적인 설계 가정(예: 전통적인 동적 링킹 과정)이 유지되는 것을
막는 어느 정도의 간접성을 초래할 수 있습니다.
[콘웨이의 법칙][conway]의 명백한 따름정리는, 당신이 당신 조직의 조직도를
출시하고 있다면, 당신 조직도의 틈새에 사는 버그들도 함께 출시하고 있다는
것입니다. 여기서 어떤 개인이나 팀도 실제로 실수를 저지르지는 않았지만,
돌이켜 보면 공격자들이 Debian/Fedora SSH의 왼손이 xz-utils의 오른손이
무엇을 하는지 알지 못한다는 것을 인지했다는 점은 분명합니다.
## GNU IFUNC는 *원래* 무엇을 하려는 것인가?
이는 런타임에 어떤 함수의 어떤 버전을 사용할지 결정할 수 있게 해줍니다.
이는 링커가 심볼을 해석하는 방식에 영향을 주기 위해 **임의의 코드**를
실행할 기회를 제공함으로써 이루어집니다.

### CPU 기능 감지
애플리케이션이 매우 다양한 x86 CPU에서 실행되어야 한다고 가정해 봅시다.
현재 CPU의 구체적인 기능에 따라, 동일한 작업에 대해 서로 다른 알고리즘을
사용하는 것이 더 나을 수 있습니다. IFUNC의 원래 아이디어는 프로그램이
함수가 처음 호출될 때 CPU 기능을 확인하고, 그 이후에는 해당 CPU에 가장
적합한 구현을 사용할 수 있게 하는 것이었습니다.
[`cpu_demo.c`](https://github.com/robertdfrench/ifuncd-up/blob/trunk/code/cpu_demo.c)를 살펴보세요:```c
void print_cpu_info() __attribute__((ifunc ("resolve_cpu_info")));
void print_avx2() { printf("AVX2 is present.\n"); }
void print_nope() { printf("AVX2 is missing.\n"); }
static void* resolve_cpu_info(void) {
__builtin_cpu_init();
if (__builtin_cpu_supports("avx2")) {
return print_avx2;
} else {
return print_nope;
}
}
int main() {
print_cpu_info();
return 0;
}
이 프로그램은 IFUNC의 가장 일반적인 사용법을 보여줍니다. CPU가 특정 기능을 지원하는지 여부를 확인하고, 지원되는 기능에 따라 함수의 다른 구현을 제공합니다. 이 경우, 우리의 함수 print_cpu_info는 CPU가 얼마나 오래되었는지에 따라 "AVX2 is present" 또는 "AVX2 is missing"을 출력하게 됩니다.
IFUNC는 CPU 기능을 탐색하기 위한 것이지만, 리졸버에서 더 복잡한 코드를 실행하는 것을 막을 수는 없습니다. 예를 들어, tty_demo.c는 STDOUT이 파일인지 터미널인지에 따라 다른 함수 구현을 로드하는 방법을 보여줍니다:```c
// Print Green text to the Terminal
void print_to_tty(const char *message) {
const char *green_start = "\033[32m";
const char *color_reset = "\033[0m";
printf("%sTTY: %s%s\n", green_start, message, color_reset);
}
// Print plain text to a file void print_to_file(const char *message) { printf("FILE: %s\n", message); }
void print_message(const char *message)
attribute((ifunc("resolve_print_function")));
void (*resolve_print_function(void))(const char *) { struct termios term;
// Ask the kernel whether stdout is a file or a tty
int result = ioctl(STDOUT_FILENO, TCGETS, &term);
if (result == 0) {
// stdout is a terminal
return print_to_tty;
} else {
// stdout is not a terminal
return print_to_file;
}
}