Calling a function in C without naming it
이름을 쓰지 않고 C 함수 호출하기
학교의 자동 채점 환경에서 허용되지 않은 C 함수를 직접 호출하는 과정을 설명합니다. ASan이 바꿔치기한 함수와 libc 함수의 메모리 배치를 이용해 주소를 계산하고, 실행 권한이 있는 메모리에서 시스템 호출을 수행하는 방식입니다.
- 주제
AI 요약
글쓴이와 친구는 학교의 자동 채점 환경에서 모든 과제를 통과할 수 있는 단일 해법을 찾다가, 허용 목록에 없는 함수를 호출하는 방법을 실험합니다. 채점기는 코드에 금지된 함수가 있는지 검사하며, 단순히 소스에서 이름을 검색하는 방식은 아닌 듯합니다. 글쓴이는 clang의 전처리 결과나 추상 구문 트리(AST)를 검사할 가능성을 추측합니다.
ASLR 환경에서 함수 주소 계산하기
처음에는 execve 주소를 알아내 직접 호출하려 합니다. 하지만 ASLR(Address Space Layout Randomization)이 실행할 때마다 프로그램과 라이브러리의 메모리 위치를 바꿉니다. PIE(Position-Independent Executable)도 실행 파일의 주소를 고정하지 않으며, execve는 실행 파일이 아닌 동적 라이브러리에서 불러옵니다.
ASLR은 메모리 구간의 위치를 바꾸지만 구간 안 함수 사이의 상대적 배치는 유지합니다. 따라서 두 함수의 주소 차이를 알면 한 함수의 주소에서 다른 함수의 주소를 계산할 수 있습니다. 과제 하나에서 두 함수 이름을 모두 사용할 수 있다면, 실행 환경에서 둘 사이의 오프셋을 먼저 구할 수 있습니다.
ASan이 바꾸는 함수와 남는 함수
글쓴이는 printf와 execve의 주소 차이를 구하려 했지만 실행할 때마다 값이 달랐습니다. 바이너리를 살펴보니 printf가 libc의 함수가 아니라 ASan(AddressSanitizer)이 제공하는 인터셉터 함수였습니다. ASan은 메모리 오류를 검사하려고 printf, malloc 같은 함수를 자체 구현으로 바꿉니다. 자동 채점기도 ASan을 켜므로 같은 상황이 발생합니다.
그래서 글쓴이는 ASan이 바꾸지 않는 함수 가운데 쓸 수 있는 mmap을 선택합니다. 허용된 함수인 printf와 mmap의 오프셋을 다른 과제에서 구하고, 함수 포인터와 정수 값을 겹쳐 쓰는 C의 union을 이용해 mmap 주소를 계산합니다. 함수 포인터 간 변환은 채점 환경의 -pedantic 컴파일 옵션에 막히므로 이 방법을 택했다고 설명합니다.
실행 가능한 메모리에서 시스템 호출하기
mmap으로 읽기·쓰기·실행 권한을 가진 메모리 페이지를 만들고, 여기에 x86-64 Linux 시스템 호출을 수행하는 짧은 기계어 루틴을 복사합니다. 이 루틴은 C 함수 호출 규약의 인자를 Linux 시스템 호출 규약에 맞게 옮긴 뒤 syscall 명령을 실행합니다. 이후 execve 시스템 호출로 셸을 실행하는 개념 증명을 채점 VM에서 확인했고, 화면에 문자열이 출력됐다고 합니다.
W^X(Write XOR Execute) 정책은 쓰기 권한과 실행 권한을 한 메모리 구간에 함께 주지 않는 방식으로 코드 변조를 막습니다. 그러나 이 사례에서는 mmap이 두 권한을 모두 요청하는 데 쓰였습니다. 글쓴이는 문제를 학교에 알렸지만, 글에서 추가 조사나 후속 조치 내용은 다루지 않습니다. ASLR만으로는 함수 사이의 상대 오프셋을 숨기지 못하며, 라이브러리 함수 순서를 실행마다 바꾸는 방안을 언급합니다. 다만 ASan이 함수를 대체하는 상황은 그 방안만으로 해결되지 않는다고 덧붙입니다.
Lobsters 반응
- @nathell — 오래전 학교 수업에서 C 코딩 규칙을 지켜야 했습니다. 규칙은 모든 파일에서 탭을 금지했지만, 제출물마다 Makefile도 요구했습니다. 이를 문자 그대로 따르면 Makefile에서도 탭을 쓸 수 없어 사실상 작업이 어려워집니다. 그래서 탭 없는 Makefile을 만드는 변환기를 작성했습니다. 실제 규칙은 아무것도 하지 않는 Makefile 하나를 두고, 즉시 평가되는 더미 변수가 GNUmakefile을 생성한 뒤 make를 다시 실행하게 만드는 방식이었습니다. GNU make가 GNUmakefile을 makefile과 Makefile보다 먼저 읽는 점을 이용했습니다. 추가 점수는 받지 못했지만 재미있는 이야깃거리가 생겼습니다.
- @zg — 탭을 금지한 이유가 궁금합니다. 그 이유를 물어보셨나요?
- @classichasclass — 학교에서 이런 시도를 보고 뭐라고 했나요? 이야기가 끝나지 않은 것 같습니다.
- @accelbread — 허용된 표준 라이브러리 함수만 가져오도록 제한한 채 코드를 WebAssembly로 컴파일하면 이런 문제를 처리할 수 있을 것 같습니다.
- @jaculabilis — 임의의 코드에서 시스템 호출을 막으려는 일은 쉽지 않아 보입니다. 학교가 실행 환경을 통제한다면, 문제가 되는 시스템 호출을 차단한 상태로 strace를 거쳐 프로그램을 실행하는 편이 훨씬 쉬울 것 같습니다.
- @treykeown — 이런 느슨한 샌드박싱에서 벌어지는 공방이 재미있습니다. dlsym(RTLD_NEXT, "execve")로도 될 것 같습니다.
- @invlpg — dlsym이 허용된 심볼일 때만 가능합니다.
원문: Wiro / 번역·요약: Trawling