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

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

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

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2021-4034-CTF-writeup — CTF pwn 챌린지 writeup: CVE-2021-4034(pkexec)를 힙 조작을 통해 익스플로잇하며, Ghidra 기반 리버스 엔지니어링과 커스텀 shelly.so 헬퍼 바이너리를 사용한 분석. | Kitploit
도구/GitHubGitHub/wechicken456/cve-2021-4034-ctf-writeup
ExploitationReverse EngineeringCTFLearning & EducationBinary ExploitationLabs & Practice
GitHubwechicken456/cve-2021-4034-ctf-writeup

CVE-2021-4034-CTF-writeup

CTF pwn 챌린지 writeup: CVE-2021-4034(pkexec)를 힙 조작을 통해 익스플로잇하며, Ghidra 기반 리버스 엔지니어링과 커스텀 shelly.so 헬퍼 바이너리를 사용한 분석.

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
저장소 보기
2112년 전아직 검토되지 않음

CVE-2021-4034-CTF-writeup

이것은 CVE-2021-4034 취약점을 악용해야 하는 제가 C로 작성한 CTF pwn 문제입니다. 플레이어는 이 저장소의 challenge 디렉터리에 있는 2개의 바이너리를 받습니다. chal 바이너리는 CTF 문제를 구현하고, shelly.so는 도우미 바이너리입니다.

이 문제를 에뮬레이션하는 방법

이 글을 작성할 당시 Dockerfile은 아직 완성되지 않았습니다. Dockerfile은 라이브 CTF 중에 이 문제를 배포하는 데 필요하지만 로컬에서는 필요하지 않습니다: 다음과 같이 사용자 권한을 설정하고 취약한 패키지를 설치하여 로컬에서도 이 문제를 에뮬레이션할 수 있습니다:

  1. 이 취약점을 성공적으로 악용하려면, Linux 머신이 아닌 플레이어는 먼저 Linux VM을 설치해야 합니다. 그런 다음 취약한 커널을 설치해야 합니다. 방법은 다음을 참조하세요: [https://askubuntu.com/a/700221]
  2. 패키지 libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1를 설치합니다.
  3. 현재 디렉터리에 root:root 소유의 flag.txt 파일을 생성합니다.
  4. 권한이 없는 사용자를 생성합니다. 이 사용자로 전환합니다.
  5. challenge 폴더의 파일을 현재 디렉터리로 다운로드합니다.
  6. 권한이 없는 사용자로 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

  1. ENTER THE HUB
  2. QUIT 1 Order number: 0x7ffde93681f0 Enter your name: tin
  3. ADD NEW ORDER
  4. EDIT ORDER
  5. SHOW ORDER
  6. CANCEL ORDER
  7. CHECKOUT
  8. DONE 1 Pick your bread: aaaa Select your spread: bbbb Choose your veg: cccc Slam your meat & egg: dddc Any side notes for the cook? 0000
  9. ADD NEW ORDER
  10. EDIT ORDER
  11. SHOW ORDER
  12. CANCEL ORDER
  13. CHECKOUT
  14. DONE 1 Pick your bread: AAAA Select your spread: BBBB Choose your veg: CCCC Slam your meat & egg: DDDD Any side notes for the cook? 1111
  15. ADD NEW ORDER
  16. EDIT ORDER
  17. SHOW ORDER
  18. CANCEL ORDER
  19. CHECKOUT
  20. DONE 3 Enter order index: 0 aaaa, bbbb, cccc, dddc, 0000
  21. ADD NEW ORDER
  22. EDIT ORDER
  23. SHOW ORDER
  24. CANCEL ORDER
  25. CHECKOUT
  26. DONE 3 Enter order index: 1 AAAA, BBBB, CCCC, DDDD, 1111
  27. ADD NEW ORDER
  28. EDIT ORDER
  29. SHOW ORDER
  30. CANCEL ORDER
  31. CHECKOUT
  32. DONE 4 Enter order index: 1
  33. ADD NEW ORDER
  34. EDIT ORDER
  35. SHOW ORDER
  36. CANCEL ORDER
  37. CHECKOUT
  38. DONE 3 Enter order index: 1 Invalid index!
  39. ADD NEW ORDER
  40. EDIT ORDER
  41. SHOW ORDER
  42. CANCEL ORDER
  43. CHECKOUT
  44. DONE 5 /notes ./tin/notes
  45. ADD NEW ORDER
  46. EDIT ORDER
  47. SHOW ORDER
  48. CANCEL ORDER
  49. CHECKOUT
  50. DONE 6
  51. ENTER THE HUB
  52. QUIT 2 Come again :)
`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에서 프로그램을 열어보겠습니다.

프로그램을 실행할 때 나타나는 문자열과 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가지의 모든 가능성을 시도하는 것과 같습니다. 이건 행운을 빌어요 ㅋㅋ.
도구 다운로드