2P by GN⁺ | ★ favorite | 댓글 1개
  • Windows의 Best-Fit 문자 변환은 UTF-16 문자열을 ANSI 코드 페이지로 바꾸는 과정에서 비슷해 보이는 문자로 치환하며, 이 동작이 Path Traversal, Argument Injection, RCE로 이어지는 WorstFit 공격 표면이 됨
  • 문제는 ANSI API, C/C++ 런타임, 컴파일러가 삽입하는 시작 코드, 개발자의 비-wide 문자 API 사용이 겹치는 구조에서 발생하며, GetCommandLineA, GetEnvironmentVariableA, getenv, int main() 경로가 영향을 받음
  • CVE-2024-4577은 Chinese/Japanese 코드 페이지에서 U+00AD가 -로 바뀌며 PHP-CGI 패치를 우회했고, Filename Smuggling은 ¥, , fullwidth slash가 / 또는 \로 바뀌며 경로 혼동을 만듦
  • Argument Splitting은 fullwidth double quote나 Yen/Won sign이 명령행 파싱 문자를 만들어 wget.exe, tar.exe, openssl.exe, java.exe 같은 CLI 도구에 인자를 주입할 수 있으며, PHP·Python·Node.js·Rust의 일반적 인자 이스케이프만으로는 막기 어려움
  • 완화하려면 Windows의 UTF-8 옵션을 켜거나 개발자가 Wide Character API_wgetcwd, _wgetenv, wmain() 같은 wide 문자 경로를 써야 하며, Microsoft가 모든 Windows 에디션에서 UTF-8을 기본값으로 켜기 전까지 유사 문제가 반복될 수 있음

Windows 인코딩 구조와 Best-Fit

  • Windows는 초기에 ANSI 코드 페이지를 사용했고, 언어권에 따라 1252, 932, 936, 949, 950 같은 코드 페이지가 달랐음
    • ACP(ANSI Code Page)는 파일 작업, 환경 변수 같은 대부분의 애플리케이션과 시스템 설정에 쓰임
    • OEMCP(Original Equipment Manufacturer Code Page)는 주로 콘솔 읽기/쓰기 같은 장치 통신에 쓰임
    • chcp는 ACP가 아니라 OEMCP를 표시하므로, 이 연구의 중심인 ACP 확인 수단이 아님
  • Windows는 1990년대 중반 Unicode로 전환했고, 현재 핵심 API는 UTF-16 기반 wide character를 사용함
    • 파일 시스템, 시스템 정보, 텍스트 처리 같은 핵심 API가 wide 문자 API로 전환됨
    • UTF-8 기능은 존재하지만 대부분 언어에서 기본으로 켜져 있지 않으며, 글에서는 beta 단계로 표현됨
  • 하위 호환성 때문에 Windows API는 ANSI 버전과 Unicode 버전을 함께 제공함
    • ANSI API는 GetEnvironmentVariableA처럼 A 접미사를 가짐
    • Unicode API는 GetEnvironmentVariableW처럼 W 접미사를 가짐
    • ANSI API가 호출되면 Windows는 내부 UTF-16 문자열을 RtlUnicodeStringToAnsiString 또는 WideCharToMultiByte로 ANSI 문자열로 변환함

Best-Fit이 WorstFit이 되는 방식

  • Best-Fit은 UTF-16 문자를 대상 ANSI 코드 페이지에 정확히 표현할 수 없을 때 비슷해 보이거나 비슷하게 느껴지는 문자로 매핑하는 동작임
    • 예를 들어 Windows-1252에서 U+221E는 8로 매핑됨
    • √π⁷≤∞가 ANSI API를 거치면 "vp7=8"처럼 바뀔 수 있음
  • 매핑은 코드 페이지마다 다르게 동작함
    • ¥ U+00A5는 Japanese 932 코드 페이지에서 \로 매핑됨
    • Central European 1250 코드 페이지에서는 Y로 매핑됨
    • 대부분의 다른 코드 페이지에서는 변경되지 않음
  • Windows API 직접 호출뿐 아니라 CRT 함수와 일반적인 main 함수 경로에서도 같은 변환이 발생함
    • getenv 같은 non-wide CRT 함수에서 Best-Fit 변환이 적용됨
    • int main(int argc, char* argv[], char* envp[]) 형태로 인자와 환경 변수를 받는 경우에도 변환이 개입함
    • 컴파일러가 삽입하는 CRT 시작 코드와 ANSI Windows API 사용이 결합되기 때문임
  • 매핑 확인에는 Best-fit Mapping GrepperUnicode.org의 WindowsBestFit 원시 매핑 데이터를 참고할 수 있음

첫 WorstFit 사례: PHP-CGI CVE-2024-4577

  • CVE-2024-4577은 Chinese/Japanese 코드 페이지로 설정된 PHP-CGI 서버를 ?%ADs 요청만으로 침해할 수 있게 한 WorstFit 공격 사례임
    • 영향 코드 페이지는 932(Japanese), 936(Simplified Chinese), 950(Traditional Chinese)
    • 위협 문자는 ­ U+00AD
  • 2012년 PHP-CGI 취약점은 Apache가 쿼리 문자열을 CGI 프로그램의 첫 번째 인자로 자동 처리하면서 발생한 argument injection이었음
    • ?-s를 붙이면 페이지 소스 코드 유출과 RCE가 가능했음
    • PHP 패치는 쿼리 문자열이 dash로 시작하면 인자 파싱을 중단하는 방식이었음
  • Best-Fit 때문에 U+00AD soft hyphen이 Chinese/Japanese 코드 페이지에서 -로 변환되며 기존 패치가 우회됨
    • ?%ADs는 PHP-CGI 입장에서 -s처럼 동작할 수 있음
    • 이 사례를 계기로 연구팀은 Best-Fit이라는 용어를 처음 마주함

Filename Smuggling: 경로 문자가 변환되는 문제

  • Filename Smuggling은 파일명에 포함된 Unicode 문자가 ANSI API 경로에서 / 또는 \로 바뀌며 경로 탐색을 만들 수 있는 공격임
    • 관련 API는 GetCurrentDirectoryA, getcwd, FindFirstFileA, findfirst*, GetFullPathNameA
    • 영향 코드 페이지는 874, 125x, 932(JP), 949(KR)
    • 위협 문자는 U+FF0F, U+FF3C, ¥ U+00A5(JP), U+20A9(KR)
  • Chrome V8의 Developer Shell인 d8.exe는 내부 구현에서 GetCurrentDirectoryA()를 사용해 현재 작업 디렉터리를 얻음
    • 악성 Unicode 문자가 들어간 작업 디렉터리를 만들 수 있으면 ANSI API 접근 시 경로 탐색 페이로드로 바뀜
    • 예시로 의도치 않은 C:/windows/win.ini 접근이 가능함
  • mruby의 Dir.getwd() Windows 구현은 ANSI CRT 함수 _getcwd()에 의존함
    • 반환값이 오염될 수 있고, Path Traversal로 이어질 수 있음

Cuckoo Sandbox: Path Traversal에서 RCE까지

  • Python의 Windows 파일 시스템 접근은 문자열이 wide인지 narrow인지에 따라 wide API 또는 ANSI API를 쓸 수 있었음
    • PEP 529 이후 Windows 파일 시스템 인코딩은 UTF-8로 표준화됨
    • Python 2와 Python 3.6 이전 Python 3은 WorstFit 공격에 취약한 상태로 남아 있었음
  • Cuckoo Sandbox는 자동화된 악성코드 분석 플랫폼이며, 최신 공식 버전이 Python 2.7에 의존함
    • Cuckoo는 Cuckoo Host와 VM Cluster로 구성됨
    • 업로드된 샘플은 VM에서 격리 실행되고, 네트워크 패킷·드롭 파일·로그를 자체 메커니즘으로 동기화함
  • 악성코드가 Unicode 파일명을 가진 드롭 파일을 만들면 Cuckoo Host의 Python 경로 처리에서 Path Traversal이 발생할 수 있음
    • 예시 PoC는 AAAA\u00a5..\u00a5..\u00a5..\u00a5..\u00a5..\u00a5conf\u00a5cuckoo.conf 경로를 생성함
    • 분석 종료 후 사용자가 웹 인터페이스에서 다운로드 버튼을 누르면 Python 파일 작업이 트리거됨
    • Cuckoo Host는 변환된 ../ 포함 경로를 처리해 공격자에게 민감 데이터를 보낼 수 있음
  • 공격자는 cuckoo.conf를 다운로드하고 Flask PIN 계산에 필요한 민감 정보를 수집해 Sandbox Host에서 RCE를 달성할 수 있음
    • 데모 영상은 Video 11로 제공됨

Argument Splitting: 명령행 파싱을 바꾸는 Best-Fit

  • Argument SplittingGetCommandLineA 출력 또는 non-Unicode int main() 경로에서 명령행 문자열이 바뀌어 인자가 분리되는 공격임
    • 관련 API와 경로는 GetCommandLineA, int main()
    • 영향 코드 페이지는 874, 125x, 932(JP), 949(KR)
    • 위협 문자는 U+FF02, U+FF3C, ¥ U+00A5(JP), U+20A9(KR)
  • 예시 PHP 코드는 escapeshellarg()로 URL을 안전하게 감싼 뒤 wget.exe -q를 실행하지만, 입력 " --use-askpass=calc "calc.exe 실행이 가능함
    • 같은 입력은 Node.js, Rust, Python으로 바꿔도 방어되지 않음
    • Python 최신 버전의 subprocess.run(["wget", "-q", ...]) 예시에서도 동작함
  • Windows는 새 프로세스에 전체 명령행을 하나의 문자열로 넘기며, 실행 파일이 이를 직접 파싱함
    • UNIX 계열처럼 인자 배열이 항상 전달되는 구조가 아님
    • CreateProcess API는 lpCommandLine 파라미터를 직접 받음
  • 일반적인 명령행 파싱에서 중요한 문자는 공백·탭, double quote, backslash임
    • 공백과 탭은 quote mode가 아닐 때 인자를 분리함
    • "는 quote mode를 전환함
    • \는 특정 시퀀스에서 double quote와 backslash를 escape함
  • 대부분 언어의 표준 라이브러리는 이 규칙에 맞춰 사용자 인자를 이스케이프하지만, 이스케이프는 Best-Fit 변환 이전에 끝남
    • PHP escapeshellarg는 double quote를 공백으로 바꾸고 인자를 quote로 감싸며 backslash를 처리함
    • Python subprocesslist2cmdline으로 Microsoft CRT 명령행 파싱 규칙에 맞춰 이스케이프함
    • 이후 ANSI 변환에서 U+FF02가 " U+0022로 바뀌면 원래 명령행 구문이 달라짐
  • int main()만 사용하는 프로그램도 취약할 수 있음
    • 컴파일러가 mainCRTStartup을 바이너리에 생성하고, 이 시작 함수가 CRT 라이브러리와 연결됨
    • CRT 내부가 ANSI API로 명령행을 가져와 파싱하면 Best-Fit 변환이 개입함
    • 이 동작 때문에 특정 프로그래밍 언어의 표준 라이브러리만으로는 공격을 완전히 막기 어려움

Argument Splitting 실제 사례

  • ElFinder는 PHP 백엔드 기반 오픈소스 웹 파일 관리자이며, 기본으로 Windows 서버와 압축 생성·해제를 지원함
    • 아카이브 처리는 shell command 실행으로 구현되어 있고, 인자는 escapeshellarg로 이스케이프됨
    • tar 형식 처리에는 Windows 내장 tar.exe가 사용됨
    • aaa" "--use-compress-program=calc" "bbb.tar 같은 tar 파일명으로 --use-compress-program 인자를 주입해 임의 명령 실행이 가능함
    • 데모는 English-configured Windows server, Code Page 1252 기준이며, 125x 코드 페이지와 Code Page 874에서도 동작해야 한다고 정리됨
    • 데모 영상은 Video 12로 제공됨
  • TortoiseGit에서 쓰이는 수정된 plink.exe 사례는 악성 URI를 clone 입력으로 넣으면 코드 실행을 트리거할 수 있음
  • RStudio는 SVN 버전 관리를 지원하며, 악의적으로 만든 폴더에 SVN 프로젝트가 있으면 클릭 한 번으로 계산기 실행이 가능함
  • Microsoft Excel 사례는 Argument Splitting과 Windows의 “Open-With” 기능을 결합한 CVE-2024-49026
    • Windows는 파일 확장자별 handler table을 유지하며, ftypeassoc로 확인할 수 있음
    • 파일명은 handler 프로그램의 인자 일부가 되므로, 파일명을 통해 공격을 적용할 수 있음
    • dots, slash, backslash, double quote를 fullwidth 형태로 바꾼 파일명으로 Excel.exe에 argument injection을 유발함
    • Excel 자체에는 추가 악용에 적합한 인자가 없어 NTLM Relay와 RBCD/ADCS를 함께 사용해 RCE를 달성함
    • 데모 영상은 Video 15

Environment Variable Confusion

  • Environment Variable ConfusionGetEnvironmentVariableA, GetEnvironmentStringsA, char *getenv()가 환경 변수의 Best-Fit 변환 버전을 반환하면서 발생함
    • 영향 코드 페이지와 위협 문자는 특정되지 않음
    • Apache HTTPd 사례에서는 0x00-0xFF가 관련됨
  • 이 공격이 성립하려면 환경 변수가 사용자에 의해 제어될 수 있어야 함
    • 부모 프로세스가 생성한 자식 프로세스에 정보를 넘기는 경우가 해당됨
    • CGI에서는 query string, HTTP header 등 HTTP 요청 정보 상당수가 환경 변수로 전달됨
  • WAF 우회 예시는 CGI 스크립트가 routing service처럼 동작하는 상황을 다룸
    • Apache 설정에는 /admin이 포함된 REQUEST_URI를 거부해 /cgi.pl/admin 원격 접근을 막는 규칙이 있음
    • Windows Perl의 WorstFit 동작 때문에 admin 일부를 Best-Fit equivalent로 바꾸면 우회 가능함
    • Code Page 1250에서 à U+00E0은 ANSI 변환 중 a로 바뀜
    • /cgi.pl/%E0dmin 요청은 서버 측 규칙에서는 다른 경로로 보이지만, Perl CGI 스크립트가 ANSI API로 PATH_INFO를 읽으면 /admin으로 처리됨
  • PHP-CGI on Windows에서는 특정 구성에서 파일 존재 확인 oracle과 잠재적 LFI가 확인됨
    • PATH_INFO와 다른 path 관련 환경 변수 처리 방식이 원인임
    • /index.php/foo/bar 요청은 Apache 기준 REDIRECT_URL, REQUEST_URI, PATH_INFO, PATH_TRANSLATED 같은 환경 변수로 전달됨
    • 이 정보만으로는 PHP 파일명과 추가 PATH_INFO 경계를 명확히 구분하기 어렵고, php-cgi.exe가 이를 해석함
  • Japanese 코드 페이지에서 ¥를 활용하면 웹 서버와 PHP-CGI의 경로 해석이 달라짐
    • 웹 서버는 /..¥..¥windows/win.ini/foo 전체를 추가 PATH_INFO로 처리함
    • PHP-CGI는 REQUEST_URI=/index.php/..\..\windows/win.ini/foo처럼 변환된 값을 받아 실제 PHP 파일과 PATH_INFO를 구분하는 과정에서 혼동함
    • Apache에서는 존재하지 않는 파일과 존재하는 파일의 응답 차이로 file existence oracle이 가능함
    • IIS에서 doc_root directive가 설정된 경우 /index.php/..¥..¥..¥windows/win.ini/ 같은 경로로 C:\Windows\win.ini를 include하고 읽는 LFI가 가능함
    • 포함된 파일이 실행 가능하거나 사용자가 제어할 수 있는 코드를 담는 경우 잠재적 RCE로 이어질 수 있지만, 해당 시나리오는 현실 애플리케이션에서 드문 bug에 가깝게 분류됨

공개와 수정 과정의 난점

  • 연구팀은 프로그래밍 언어, 오픈소스 프로젝트, Windows 내장 CLI 프로그램의 여러 문제를 각 upstream maintainer에게 보고함
    • 가장 많은 논쟁은 Argument Splitting에서 발생함
    • 일부 벤더는 사용자 입력을 명령행에 넘기는 것 자체가 취약점이라고 봄
  • 책임 소재가 불명확한 점도 문제였음
    • 문제가 되는 코드가 컴파일 중 자동 삽입되는 mainCRTStartup()과 MSVCRT/UCRT 내부 ANSI API 호출에 걸쳐 있음
    • 개발자가 wmain()을 쓰지 않은 문제인지, CRT가 명령행을 잘못 나눠 main()에 잘못된 인자를 넘긴 문제인지 구분하기 어려움
    • 일부 프로젝트는 소스 코드만 제공하고, Windows prebuilt executable은 인터넷상의 제3자 자원봉사자가 배포함
  • 수정은 단순히 main()을 wide-character 버전으로 바꾸는 수준이 아님
    • 함수 시그니처가 바뀌면 변수 정의와 인자 파싱 로직을 char *에서 wchar_t * 기반으로 다시 작성해야 함
    • 이 과정은 고통스럽고 오류가 나기 쉬움
  • Curl은 Windows 기능이라며 수정 계획이 없다고 답했고, Microsoft가 포팅한 Curl은 wmain()으로 entry를 수정해 Windows 내장 curl.exe는 영향받지 않음
    • Curl 공식 빌드 바이너리는 Argument Splitting 공격의 영향을 받음
    • 전체 보고는 HackerOne에 공개됨
  • OpenSSL은 OPENSSL_WIN32_UTF8 환경 변수로 인자를 wide character 형식으로 처리할 수 있음
    • 원래 목적은 UI에서 UTF-8 표시 문제를 고치는 것이었지만, Argument Splitting 공격도 완화함
    • 기본 OpenSSL 사용에서는 개발자가 이 환경 변수를 설정해야 함을 모르는 경우가 많고, -engine 인자를 이용한 임의 코드 실행이 가능함
  • Perl 공식 배포판은 Windows prebuilt executable을 제공하지 않으며, Strawberry Perl과 ActiveState Perl 같은 서드파티 설치 프로그램이 흔히 사용됨
    • 두 배포판은 Argument Splitting 공격의 영향을 받음
    • Perl maintainer와 논의한 결과 “Perl 버그보다 Microsoft 버그에 가깝다”는 결론으로 현재 미해결 상태임
  • Microsoft에는 세 건이 MSRC에 보고됐고, 모두 처음에는 심각도 기준 미달로 거절됨
    • 여러 번 재개한 뒤 Excel 사례만 세 번째 시도 후 수락됨
    • 다른 사례는 현재까지 미해결 상태임
    • MSRC는 별도 애플리케이션이 신뢰할 수 없는 입력을 명령행에 넣어 실행하는 취약점에 의존하며, 이를 악용 가능하게 하는 technique 자체는 취약점 요건을 충족하지 않는다고 답함
  • CERT/CC에도 도움을 요청했고, Microsoft는 몇 달 뒤 GetCommandLineA 문서에 보안 경고를 추가함
    • 경고는 GetCommandLineA에만 들어갔고, 주의가 필요한 ANSI API는 더 남아 있음

보고된 영향 대상과 상태

  • 공개 과정에서 확인·보고된 항목은 다음과 같음
    • 2024/05/07: PHP php-cgi.exeCVE-2024-4577
    • 2024/06/13: Curl Official BuildWon’t Fix
    • 2024/06/13: Apache Subversion svn.exeCVE-2024-45720
    • 2024/06/16: Microsoft Tar tar.exe — Won’t Fix
    • 2024/06/19: Microsoft Excel excel.exeCVE-2024-49026
    • 2024/06/19: Microsoft PhoneBook rasphone.exe — Won’t Fix
    • 2024/06/19: Oracle Java java.exe — Pending Fix
    • 2024/06/19: Perl perl.exe — Won’t Fix
    • 2024/07/15: Perforce p4.exeCVE-2024-8067
    • 2024/08/05: PostgreSQL psql.exe — Won’t Fix
    • 2024/08/08: Putty plink.exe — Fixed
    • 2024/08/19: OpenSSL openssl.exe — Other
    • 2024/08/19: wkhtmltopdf wkhtmltopdf.exe — EOL
    • 2024/08/19: GNU Wget — No Reply

완화책과 남은 공격 표면

  • WorstFit 공격은 운영체제 수준 문제이므로, Microsoft가 모든 Windows 에디션에서 UTF-8을 기본값으로 켜기 전까지 유사 이슈가 계속 다시 나타날 수 있음
  • 사용자가 할 수 있는 조치는 Windows의 UTF-8 옵션을 확인해 켜는 것임
    • 이 기능은 아직 beta 단계로 표시되며, 부작용 여부는 확실하지 않음
  • 개발자는 가능한 한 Wide Character API를 사용해야 함
    • CRT도 _wgetcwd, _wgetenv 같은 wide character 버전을 제공함
    • non-wide 경로를 계속 쓰면 내부 구현이 ANSI API를 호출할 수 있고, WorstFit 공격에 노출될 수 있음
  • Windows의 하위 호환성 때문에 ANSI API가 숨어 있는 곳은 더 있을 수 있음
    • 예시로 RegQueryValueA 같은 Windows Registry query가 영향을 받을 수 있지만, 취약 시나리오를 찾아야 함
    • 연구팀은 Active Directory에서도 Best-Fit 동작을 관찰함

댓글과 토론

Hacker News 의견들
  • 이건 꽤 까다로운 문제임. Microsoft의 “best fit” 코드 매핑은 넓은 Unicode를 ASCII로 바꾸는, 공개되어 있지만 사실상 “느낌 기반”인 매퍼이고 시스템 전반에 깔려 있음
    이 매퍼는 아주 많은 곳에서 기본으로 링크되고, Microsoft가 하위 호환성을 보는 방식상 계속 포함될 수밖에 없어 보임. 익스플로잇은 대체로 특이한 코드 포인트가 슬래시, 하이픈, 따옴표처럼 “느낌상” 매핑되는 데서 나옴. 현대 언어 안에서는 올바른 Unicode로 검사되지만, 셸 명령이나 Win32 API로 넘어가면 제어권을 넘긴 뒤에 다른 방식으로 축소 변환됨. curl 관리자의 말처럼 여기서 “curl은 피해자”인데, 범인은 누구인지가 문제임. 서버가 사용자 입력을 검증할 때와 시스템 라이브러리에 넘길 때 다르게 뭉개면 결국 문제가 생김. Win32 쪽에서 best fit 변환을 끌 수 있는 선택지가 해법일 수 있지만, Windows 전문가는 아니라 추측임. 그렇게 해도 공식 API나 아직 끄지 않은 소프트웨어와는 계속 상호작용하게 됨

    • opt-out은 Unicode Windows API를 쓰는 것, 즉 "a"가 아니라 "w"로 끝나는 함수를 쓰는 것임. 이 방식은 "\\?\" 접두사를 붙이거나 매니페스트를 올바르게 설정하면 260자 초과 경로 문제도 같이 해결하며, Windows XP 이후로 가능했고 권장되어 왔음
      비Unicode API가 아직도 이렇게 흔히 쓰이는 이유는 잘 모르겠음. Windows 98이나 Windows 2000을 지원하려는 욕구 때문이라고는 상상하기 어려움
    • Windows에는 Windows XP부터 레거시 동작을 끄는 방법인 매니페스트 파일이 있음. 매니페스트가 없으면 GetWindowsVersion조차 현재 버전을 반환하지 않았던 것으로 기억함. 여기에 opt-out을 추가하고, 언젠가 Visual Studio 기본값으로 만드는 건 크게 어렵지 않아 보임
      또 필요한 건 일종의 린팅임. 현대 애플리케이션에서 ANSI WinAPI 함수를 호출할 이유는 보통 없음. 로캘을 UTF-8로 설정하고 8비트 함수만 쓰는 방식도 있겠지만 얼마나 잘 동작하는지는 모르겠음. argv, printf, std::cout이 UTF-8로 동작하고 이상한 변환 없이 WinAPI용 UTF-8/UTF-16 변환 함수만 쓰게 만드는 설정과 헤더도 몇 가지 있는 것으로 알고 있음. 이런 절차를 Microsoft가 한곳에 문서화해야 함
    • 보안 취약점이든 아니든, Windows에서 Unicode 인자를 제대로 처리하지 못한다면 curl의 버그이기도 함
    • 코드 포인트를 문자로 느슨하게 매핑하는 방식은 Unicode에서 늘 거슬렸음
  • 이건 어느 정도 예상 가능하지만, W/A 혼란이 생기던 무렵 10년쯤 Windows 개발과 Wine API 해킹을 했던 입장에서도 새로웠음
    Windows는 카드 게임 Munchkin 같아서, 여러 기능이 우연히 맞물리면 믿기 어려울 만큼 무작위적이고 강력한 익스플로잇으로 합쳐질 수 있음. ANSI 하위 시스템을 UTF-8로 바꾸고 있다는 점은 반갑고, 이론적으로는 이런 문제를 많이 완화할 수 있음. Rust 팀이 프로세스 생성 API에 또 다른 수정을 해야 할지도 궁금함

    • Rust 표준 라이브러리는 기본적으로 ANSI API를 거의 쓰지 않음. 글에서는 Rust에 통하는 공격을 보여주지 않았고, 그런 공격이 있다면 꼭 보고하는 게 좋음
      물론 Rust가 프로세스 경계 너머에서 벌어지는 일을 통제할 수는 없음. Rust가 실행한 애플리케이션이 ANSI API를 쓰면 그쪽에 문제가 생기지만, 그건 그 애플리케이션의 책임
  • “ANSI를 단계적으로 없애고 Wide Character API 사용을 권장하는 것”은 기억이 맞다면 NT 3.5 때부터 Microsoft의 공식 입장이었음
    안타깝게도 큰 걸림돌 중 하나는 Microsoft의 C/C++ 런타임 라이브러리 msvcrt.dll 구현 방식임. _wfopen(), _wgetenv() 같은 비표준 wide 함수는 내부적으로 Win API의 W 함수를 쓰지만, fopen(), getenv() 같은 표준 narrow 함수는 wide 버전으로 변환하는 대신 A 함수를 그냥 사용함. 그리고 A 함수는 보통 Unicode 변환 실패를 보고하지 않고 best-fit 방식으로 덮어버림. C로 작성된 소프트웨어를 Windows로 이식하는 사람이 표준 함수 사용을 전부 Microsoft 비이식 함수로 바꾸고 싶어 하지는 않음. 그 시점부터는 사실상 전면 재작성임

    • 최근 2년간 Microsoft 문서를 읽고 받은 인상은 반대였음. 애플리케이션 매니페스트에서 activeCodePageUTF-8로 설정하고 “ANSI” 함수만 쓰라는 쪽이었음
    • 이식 가능한 코드에서는 Windows 빌드일 때 mainfopen 같은 표준 함수를 wide 대응 함수로 #define
      이러면 char*와 꾸밈 없는 문자열 리터럴을 그냥 쓸 수는 없어서, Linux에서는 char, Windows에서는 wchar_ttchar 타입과 문자열 리터럴용 _T() 매크로를 정의함. 대체로 별생각 없이 잘 동작함
    • 요즘 정말 짜증 나는 건 Google에서 Win32 API를 검색하면 항상 -W 변형이 아니라 -A 변형이 먼저 나온다는 점임. robots.txt에 이상한 게 있는지는 모르겠지만, 새 코드에서는 -W 변형을 쓰라고 권장하는 API가 기본적으로 레거시 API를 반환하는 건 이상함
    • Microsoft의 C/C++ 런타임 msvcrt.dllUniversal C Runtime(UCRT)[1]으로 대체되었고, UCRT는 C99를 준수함
    • Windows는 경로명을 이런 멍청한 인코딩 처리 없이 그냥 바이트열로 다루는 API를 제공했어야 함. UNC 경로를 도입할 때 그렇게 할 수도 있었을 것 같음
  • 직접 작성한 애플리케이션이나 패치한 EXE에서 “Ansi” 코드 페이지를 실제로 UTF-8로 강제하는 방법은 두 가지가 있음
    하나는 매니페스트 파일을 쓰는 것이고, Windows 10의 특정 빌드부터 동작함. 빌드 후 임의의 EXE에도 적용할 수 있어서, 프로그램에 UTF-8 지원을 억지로 넣을 수 있음. 콘솔 모드 프로그램에 특히 유용함. 다른 하나는 “App Locale”류 도구가 쓰는 해킹을 쓰는 것임. 한 방법은 NTDLL의 문서화되지 않은 함수 호출을 포함. 정확히 어떤 함수가 필요한지는 모르지만 RtlInitNlsTablesRtlResetRtlTranslations가 관련될 수도 있음

  • Microsoft가 모든 Windows 에디션에서 UTF-8을 기본으로 켤 가능성은 잘 모르겠음. 특정 코드 페이지나 문자당 1바이트를 가정하는 오래된 애플리케이션이 많아서 깨질 수 있음
    더 미묘하게는 wide 문자에서 ANSI로 변환할 때 바이트 수가 늘지 않는다고 가정해 기존 버퍼를 재사용하는 애플리케이션도 있음. UTF-8에서는 그렇지 않고, 기존 코드 페이지 대부분에서는 대체로 맞았기 때문에 새 취약점이 생길 수 있음. 차라리 Win32 xxxA API에서 Best-Fit 로직을 제거하고, 매핑 불가능한 문자는 공통 메타 의미가 없는 x 같은 문자로 바꾸는 편이 훨씬 덜 깨질 것 같음

    • 그런 애플리케이션의 예로 Adobe After Effects가 있음[0]. 적어도 예전에는 그랬고, 지금은 Windows를 쓰지 않음
      [0] https://tambre.ee/blog/adobe_after_effects_windows_utf-8/
    • 아직 없다면 OS API 버전을 도입해서, 새 API 버전이나 새 SDK를 대상으로 하는 신규/업데이트 앱은 기본적으로 UTF-8을 가정하게 할 수 있지 않을까 함. 특정 API 버전 아래는 레거시 모드로 에뮬레이션하면 됨. Windows에는 이미 여러 Windows 버전의 동작을 흉내 내는 shim 개념이 있음
    • UTF-8 이전 Windows에서도 기본 코드 페이지를 바꾸면 앱이 이상해지는 문제는 이미 있었음. 그러니 사용자에게 UTF-8 옵션을 주는 건 합리적임
      Best-Fit 매핑이 일으키는 문제를 보면 기본값으로 만드는 것도 합리적이지만, Microsoft는 사용자가 오래된 코드를 쉽게 실행할 방법을 찾도록 도와야 할 것임. 덜 합리적인 방법으로는 Best-Fit 매핑에서 “특수” ASCII 문자로 가는 매핑을 모두 없애는 것이 있는데, CRT를 정적으로 링크한 앱에는 도움이 안 됨. 취약점을 고치지도 못하므로 좋은 해결책은 아님. 때로는 보안 취약점이 하위 호환성 파괴를 밀어붙이는 동기가 됨
  • Microsoft는 적어도 1년 전부터 이 문제를 알고 있었음. CA2101[1]이라는 특수 코드 분석 규칙을 내서 best-fit 매핑 사용을 명시적으로 권장하지 않았기 때문임
    규칙 설명에는 보안 취약점을 언급했지만, 세부 사항은 의도적으로 모호하게 처리했음
    [1] https://learn.microsoft.com/en-us/dotnet/fundamentals/code-a...

  • 모든 것을 char *에서 wchar *로 바꿀 필요는 없음. 받은 wide 문자를 UTF-8로 변환하거나, 짝 없는 서로게이트 같은 잘못된 시퀀스까지 허용하려면 Rust의 WTF-8 같은 것으로 변환한 뒤 계속 char를 쓰면 됨
    물론 ANSI나 OEMCP 문자열을 UTF-8 문자열과 섞지 않도록 조심해야 하지만, 그냥 UTF-8만 쓰면 쉽다. 고전적인 https://utf8everywhere.org/ 사이트가 권장하는 접근이 이 방식임

  • 개인 Windows 컴퓨터에서 몇 년 전부터 UTF-8 모드를 켜둔 덕분에 이 버그를 우연히 피하고 있었음. 글 하단에 나온 설정임
    오래된 외국 게임들이 깨진 글자를 보여줘서 켜둔 것이었고, “Beta”라고 표시되어 있지만 버그나 부작용은 느끼지 못했음

    • 흥미롭지만, 내 경우 그 체크박스는 무작위 앱들을 너무 많이 충돌시키는 것 말고는 한 일이 없었음. 꺼져 있을 때 사용자의 기본 코드 페이지가 무엇인지에 따라 잘 동작하는지가 달라지는 듯함
    • 방금 “Beta: Use Unicode UTF-8 for worldwide language support” 옵션을 켰음. 얼마나 많은 앱이 깨질지 흥미로울 것 같음
  • 베타 체크박스가 매니페스트에서 ActiveCodePage를 UTF-8로 설정하는 것과 같은지 궁금했는데, 문서[0]를 보니 GDI는 프로세스별 코드 페이지를 따르지 않고 체크박스가 설정하는 단일 전역 코드 페이지만 따른다고 명확히 되어 있음
    자기 앱에서 *A API로 완전히 UTF-8에 opt-in할 수 없다는 건 좀 아쉬움. 그래도 글에서 강조한 문제들에 대해서는 여전히 유효한 우회책이나 방어 심층화 수단이 될 수 있다고 봄
    [0] https://learn.microsoft.com/en-us/windows/apps/design/global...

  • 세상에. Windows API가 그런 종류의 best-fit 변환을 제공한다는 건 알고 있었지만, 내 기본 코드 페이지인 949[1]에서 여러 ANSI 함수의 기본 동작이라는 건 몰랐음
    이쯤 되면 gets처럼 그냥 금지해야 함. [1] UTF-8 코드 페이지 65001이 있다는 건 알고 있음. 오랫동안 정말 쓸 수 없는 수준이었고, 지금도 호환성 문제를 겪고 있음