
CTF pwn 챌린지 writeup: CVE-2021-4034(pkexec)를 힙 조작을 통해 익스플로잇하며, Ghidra 기반 리버스 엔지니어링과 커스텀 shelly.so 헬퍼 바이너리를 사용한 분석.
이것은 CVE-2021-4034 취약점을 악용해야 하는 제가 C로 작성한 CTF pwn 문제입니다. 플레이어는 이 저장소의 challenge 디렉터리에 있는 2개의 바이너리를 받습니다. chal 바이너리는 CTF 문제를 구현하고, shelly.so는 도우미 바이너리입니다.
이 글을 작성할 당시 Dockerfile은 아직 완성되지 않았습니다. Dockerfile은 라이브 CTF 중에 이 문제를 배포하는 데 필요하지만 로컬에서는 필요하지 않습니다: 다음과 같이 사용자 권한을 설정하고 취약한 패키지를 설치하여 로컬에서도 이 문제를 에뮬레이션할 수 있습니다:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1를 설치합니다.root:root 소유의 flag.txt 파일을 생성합니다.challenge 폴더의 파일을 현재 디렉터리로 다운로드합니다.chal을 실행합니다.chal 바이너리를 실행하면 이 바이너리가 무엇을 하는지 대략적으로 알 수 있습니다:```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
`Add` 함수의 입력 크기와 `Cancel` 함수의 인덱스를 가지고 실험해봐도 특별한 점은 없습니다 (오버플로우나 세그멘테이션 폴트 없음). 하지만 몇 가지 흥미로운 점이 있습니다:
- 주문은 리스트(아마도 연결 리스트)에 저장되는 것으로 보이며, 0-인덱스인 것 같습니다?
- `Order number: 0x7ffde93681f0`라는 출력이 있는데, 이는 스택의 어떤 위치를 출력하는 것으로 보입니다?
- 또한, 프로그램은 우리가 입력한 이름으로 디렉토리를 생성하고, 그 안에 3개의 실행 파일을 만드는데, 그 중 하나는 우리가 받은 헬퍼 바이너리 파일입니다:```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
실행 파일에는 우리의 입력이 포함되어 있습니다.
다른 함수들을 가지고 놀면서 버그를 발견할 수도 있겠지만(그럴 가능성이 높습니다), 본론으로 들어가서 Ghidra에서 프로그램을 열어보겠습니다.
프로그램을 실행할 때 나타나는 문자열과 Ghidra에 있는 문자열을 비교하면, 일부 FUN_* 함수들을 익숙한 이름으로 바꿀 수 있습니다:```C
undefined8 main(void)
{ int iVar1; size_t sVar2; undefined2 *puVar3; long in_FS_OFFSET; int opt; int local_1c; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "/recipe"; print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n"); print( "We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n" ); while( true ) { print("1. ENTER THE HUB\n"); print("2. QUIT\n"); __isoc99_scanf(&DAT_001030c5,&opt); getc(stdin); if (opt != 1) break; printf("Order number: %p\n",&local_18); print("Enter your name: "); __isoc99_scanf(&DAT_0010334f,&DAT_00105120); sVar2 = strlen(&DAT_00105120); puVar3 = (undefined2 *)malloc(sVar2 + 2); DAT_00105100 = puVar3; *puVar3 = 0x2f2e; *(undefined *)(puVar3 + 1) = 0; strcpy((char *)(DAT_00105100 + 1),&DAT_00105120); iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060); if (iVar1 == -1) { mkdir((char *)DAT_00105100,0x1c0); } DAT_00105140 = 0; order_cnt = 0; for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) { *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0; } main_menu(); } print("Come again :)\n");
The `printf("Order number: %p\n",&local_18);` **스택에 있는 지역 변수의 위치를 출력합니다.**
gdb로 빠르게 확인해보면 누출된 주소가 상수 문자열 `/recipe`에 대한 포인터의 주소임을 알 수 있습니다:```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
우리는 mkdir 호출을 볼 수 있는데, 이는 현재 디렉토리에 입력한 이름으로 디렉토리를 생성합니다.
이는 프로그램을 실행했을 때 우리가 관찰한 것과 일치합니다. 그런 다음 main_menu 함수를 호출하기 전에 일부 변수를 초기화하며, 이 함수는 대략 다음과 같이 생겼습니다:```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN 'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...Nuh Uh!...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...Ooh Yes.. sCruMpTioUs...");
}
print("Fine... I'll give you a taste.\n");
FUN_00101504();
}
REV에 익숙하지 않다면, 이것은 C 언어에서 `switch` 문을 디컴파일한 것입니다. `0x539`라는 흥미로운 옵션이 하나 있습니다. 이 옵션을 통해 플레이어는 `0x10`개의 임의 바이트를 추측할 수 있습니다. 모든 바이트가 같으면 `FUN_00101504();`를 호출하는데, 이 함수는 `system("cat flag.txt");`를 실행합니다. 그렇지 않으면 프로그램이 종료됩니다.
하지만 16개의 임의 바이트를 브루트포싱하는 것은 256**16 = 340282366920938463463374607431768211456가지의 모든 가능성을 시도하는 것과 같습니다. 이건 행운을 빌어요 ㅋㅋ.