Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

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

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 헬퍼 바이너리를 사용한 분석.

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

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

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 :)
root@kitploit:~
`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");

root@kitploit:~
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(); }

root@kitploit:~
REV에 익숙하지 않다면, 이것은 C 언어에서 `switch` 문을 디컴파일한 것입니다. `0x539`라는 흥미로운 옵션이 하나 있습니다. 이 옵션을 통해 플레이어는 `0x10`개의 임의 바이트를 추측할 수 있습니다. 모든 바이트가 같으면 `FUN_00101504();`를 호출하는데, 이 함수는 `system("cat flag.txt");`를 실행합니다. 그렇지 않으면 프로그램이 종료됩니다.

하지만 16개의 임의 바이트를 브루트포싱하는 것은 256**16 = 340282366920938463463374607431768211456가지의 모든 가능성을 시도하는 것과 같습니다. 이건 행운을 빌어요 ㅋㅋ.

모든 가능성을 통과하더라도 프로그램은 여전히 권한이 없기 때문에 플래그를 읽을 수 없습니다. 이 `cat flag.txt` 문장은 의도적인 것으로, 경험 없는 플레이어를 속여 이 `0x539` 메뉴 옵션을 선택하게 하는 것뿐만 아니라, 플레이어가 단순히 이 함수를 호출하여 플래그를 읽지 못하게 방지하기 위해서이며, 이에 대해서는 나중에 자세히 다룰 것입니다.
***
## 추가```C
  int iVar1;
  undefined8 *puVar2;
  void *pvVar3;
  undefined8 *ptr2;
  
  if (order_cnt < 10) {
    puVar2 = (undefined8 *)malloc(0x30);
    print("Pick your bread: ");
    readline(puVar2 + 1,8);
    print("Select your spread: ");
    readline(puVar2 + 2,8);
    print("Choose your veg: ");
    readline(puVar2 + 3,8);
    print("Slam your meat & egg: ");
    readline(puVar2 + 4,9);
    iVar1 = order_cnt;
    pvVar3 = malloc(0x30);
    *(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
    *puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
    print("Any side notes for the cook? ");
    readline(*puVar2,0x30);
    puVar2[5] = 0;
    if (DAT_00105140 != (undefined8 *)0x0) {
      for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
      }
      ptr2[5] = puVar2;
      puVar2 = DAT_00105140;
    }
    DAT_00105140 = puVar2;
    order_cnt = order_cnt + 1;
  }
...

여기 있는 변수들이 눈에 잘 들어오지 않는다면, 제가 이렇게 이름을 바꾸는 데 시간을 들였기 때문입니다. 리버스 엔지니어링 중에 상황을 추적하려면 여러분도 이렇게 이름을 바꿔야 합니다. 자, 주요 포인트를 살펴보겠습니다:

  • puVar2는 크기가 0x30인 malloc 청크입니다.
  • 0번째 필드는 puVar3 포인터로, 8바이트 길이이며 현재 청크의 notes를 가리킵니다.
  • 다음 4개 필드는 각각 8바이트 길이지만, 4번째 필드에 9바이트를 읽을 수 있으므로 5번째 필드로 1바이트를 오버플로우할 수 있습니다.
  • 그러나 puVar2[5] = 0; 줄이 우리의 1바이트 오버플로우를 어쨌든 NULL로 덮어씁니다.
  • 그런 다음 전역 변수 DAT_00105140 == 0이면, 새로운 청크로 설정하기만 하므로, 이것이 리스트의 head 포인터일까요?
  • 그렇지 않으면, 현재 리스트의 마지막 청크까지 반복하여, 그 5번째 필드를 새로운 청크로 설정합니다. => 5번째 필드는 연결 리스트에서 다음 청크를 가리키는 포인터입니다.

Edit```C

... print("Enter order index: "); __isoc99_scanf(&DAT_001030c5,&local_20); getc(stdin); if ((local_20 < 0) || (order_cnt <= local_20)) { print("Invalid index!\n"); } else { local_18 = head; for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) { local_18 = (undefined8 *)local_18[5]; } print("Pick your bread: "); readline(local_18 + 1,8); print("Select your spread: "); readline(local_18 + 2,8); print("Choose your veg: "); readline(local_18 + 3,8);WE print("Slam your meat & egg: "); readline(local_18 + 4,9); print("Any side notes for the cook? "); readline(*local_18,0x30); } ...

root@kitploit:~
입력 인덱스에 대한 검사가 있으므로 임의 위치를 편집할 수 없습니다. 

**하지만, 4번째 필드에서 9바이트를 읽어 5번째 필드로 1바이트 오버플로우되는 것은 여전히 존재합니다!!! `nxt` 포인터에 1바이트를 덮어쓸 수 있습니다**
***
## Show```C
...
    if (local_18 != (undefined8 *)0x0) {
      printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
    }
...

%s는 NULL 문자가 나올 때까지 출력하므로, 4번째 필드가 8바이트 길이라면, 4번째 %s는 우리 청크의 4번째 필드와 5번째 필드의 nxt 포인터 값을 출력할 것입니다. => 힙(heap) 누수(leak)를 얻습니다!!!


Cancel

여기서 흥미로운 것은 없습니다.


Checkout```C

strcpy(local_d8,dir_name); sVar2 = strlen(dir_name); strcpy(local_d8 + sVar2,"/recipe"); creat(local_d8,0x1c0); iVar1 = open(local_d8,2); if (iVar1 == -1) { print("Error opening file f.\n"); /* WARNING: Subroutine does not return / exit(0); } chmod(local_d8,0x1f8); strcpy(local_98,dir_name); sVar2 = strlen(dir_name); strcpy(local_98 + sVar2,"/notes"); printf("%s %s\n","/notes",local_98); creat(local_98,0x1c0); __fd = open(local_98,2); if (__fd == -1) { print("Error opening file f_notes.\n"); / WARNING: Subroutine does not return */ exit(0); } chmod(local_98,0x1f8);

root@kitploit:~
그래서 입력한 이름의 디렉토리에 `recipe`와 `notes` 파일을 생성합니다. `chmod` 문은 이 파일들을 실행 가능 모드로 설정합니다: `0x1f8`과 `0x1c0`은 각각 8진수로 `0700`과 `0770`입니다.```C
...
  for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
  {
    sVar2 = strlen((char *)(local_e0 + 1));
    write(iVar1,local_e0 + 1,sVar2);
    write(iVar1,&DAT_00103127,1);
    sVar2 = strlen((char *)(local_e0 + 2));
    write(iVar1,local_e0 + 2,sVar2);
    write(iVar1,&DAT_00103127,1);
    sVar2 = strlen((char *)(local_e0 + 3));
    write(iVar1,local_e0 + 3,sVar2);
    write(iVar1,&DAT_00103127,1);
    sVar2 = strlen((char *)(local_e0 + 1));
    write(iVar1,local_e0 + 4,sVar2);
    write(iVar1,&DAT_00103127,1);
    sVar2 = strlen(*local_e0);
    write(__fd,*local_e0,sVar2);
    write(__fd,&DAT_00103127,1);
    local_e0 = (char **)local_e0[5];
  }
  iVar1 = close(iVar1);
  if (-1 < iVar1) {
    iVar1 = close(__fd);
    if (-1 < iVar1) {
      local_58 = 0x2f706d742f207063;
      local_50 = 0x732e796c6c656873;
      local_48 = 0x206f;
      local_40 = 0;
      local_38 = 0;
      local_30 = 0;
      local_28 = 0;
      local_20 = 0;
      strcpy((char *)((long)&local_48 + 2),dir_name);
      system((char *)&local_58);
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return;
    }
  }
...

이 거대하고 지저분한 코드 블록은 기본적으로 우리 청크의 내용을 이 파일들에 작성한 다음, 명령어 cp /tmp/shelly.so dir_name을 실행합니다. 여기서 dir_name은 프로그램 시작 시 우리가 제공한 이름입니다.


FUN_00101e84```C

void FUN_00101e84(void)

{ long in_FS_OFFSET; char *local_18; long local_10;

local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("HOLD UP, LET HIM COOK."); local_18 = (char *)0x0; execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array); if (local_10 != *(long )(in_FS_OFFSET + 0x28)) { / WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }

root@kitploit:~
이것은 흥미로운 함수입니다. 이 함수는 `notes` 배열 포인터를 환경 변수로 사용하여 `NULL argv`와 함께 `pkexec`를 호출하는데, 이는 우리의 CVE-2021-4034에서 취약한 명령입니다. 이것이 우리가 `notes`를 조작하고 어떻게든 실행을 이 함수로 리디렉션해야 한다는 것을 시사합니까?

***
## shelly.so
Ghidra에서 이것을 열어보면, 이는 분명히 `set-UID-root` 라이브러리이며, 이는 CVE 익스플로잇에 사용된 것들과 유사합니다.

***
# Summary
주요 사항:
1. 프로그램 시작 부분(주문 번호)에 스택 누출이 있으며, 이는 `recipe` 파일을 생성하는 데 사용되는 상수 문자열 `/recipe`의 주소를 보유합니다.
2. 청크의 0번째 필드는 `notes` 포인터입니다.
3. 청크의 5번째 필드는 연결 리스트에서 다음 청크를 가리키는 `nxt` 포인터입니다.
4. `Show`는 청크의 5번째 필드인 힙 주소를 유출할 수 있습니다.
5. `Edit`는 5번째 필드에 1바이트를 덮어쓸 수 있습니다. **이것이 유일한 임의 쓰기 버그입니다!!**
6. `FUN_00101e84`는 NULL argv와 우리가 제어하는 환경 변수(`notes` 배열)로 `pkexec`를 호출합니다.

***
# Exploitation

## Heap chunk layout
gdb에서 빠른 검사를 통해 프로그램 내 서로 다른 청크 간의 오프셋을 알 수 있습니다:```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤  search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
  0x55555555aae8 - 0x55555555aaec  →   "aaaa" 
gef➤  x/30gx 0x55555555aae8-0x18
0x55555555aad0:	0x0000000000000000	0x0000000000000041
0x55555555aae0:	0x000055555555ab20	0x0000000061616161
0x55555555aaf0:	0x0000000000000061	0x0000000000000061
0x55555555ab00:	0x0000000000000061	0x000055555555ab60
0x55555555ab10:	0x0000000000000000	0x0000000000000041
0x55555555ab20:	0x0000000030303030	0x0000000000000000
0x55555555ab30:	0x0000000000000000	0x0000000000000000
0x55555555ab40:	0x0000000000000000	0x0000000000000000
0x55555555ab50:	0x0000000000000000	0x0000000000000041
0x55555555ab60:	0x000055555555aba0	0x0000000062626262
0x55555555ab70:	0x0000000000000062	0x0000000000000062
0x55555555ab80:	0x0000000000000062	0x0000000000000000
0x55555555ab90:	0x0000000000000000	0x0000000000000041
0x55555555aba0:	0x0000000031313131	0x0000000000000000
0x55555555abb0:	0x0000000000000000	0x0000000000000000

따라서 청크의 크기는 0x40(메타데이터 0x10바이트 포함)이며, 청크들은 (현재 청크 시작부터 다음 청크 시작까지) 0x40+0x40 = 0x80의 오프셋으로 분리됩니다. 이는 notes가 청크를 할당할 때마다 할당되어 항상 연속된 청크들 사이에 위치하기 때문입니다.

임의 위치에 쓰기

익스플로잇에 들어가기 전에, 1바이트 오버플로우 버그를 악용하여 임의 주소에 쓰는 방법은 무엇일까요? 다음 전략을 사용할 수 있습니다:

  1. 청크 A, B, C 3개를 할당합니다.
  2. 두 번째 청크의 nxt 포인터를 유출합니다(요약 세션의 두 번째 지점).
    • 유출된 주소(다음 청크)는 C입니다.
    • 그러면 현재 청크의 주소는 B = C-0x80이 됩니다.
    • B의 최하위 바이트를 x라고 합시다.
  3. 현재 청크의 5번째 필드에 1바이트 x + 8을 오버플로우합니다. 결과 nxt 포인터는 B + 8이 됩니다.
    • 여기서 주의해야 합니다: 위의 메모리 스니펫을 사용하여 현재 청크를 첫 번째 청크, 즉 A = 0x55555555aae0이라고 하면, 유출된 주소는 B = 0x55555555ab60이 되고 x = 0xe8이 됩니다.
    • 만약 0x55555555ab08에 있는 A의 5번째 필드의 마지막 바이트를 x로 덮어쓰면, 결과 포인터는 이 되어 과 다릅니다. 이는 우리가 원하는 것이 아닙니다.

첫 번째 방법 - "cat flag.txt"로 점프

이 함수로 점프하는 방법이 있지만, 여기서는 다루지 않겠습니다. 하지만 시도하기 전에 Dockerfile을 살펴보세요. flag.txt에 대해 무엇인가 눈에 띄나요?``` ... chown root:root /home/ctf/flag.txt ... USER peasant CMD ["/home/ctf/start.sh"]

root@kitploit:~
***IT IS OWNED BY ROOT, WHILE THE PROGRAM IS RAN AND OWNED BY AN UNPRIVILEGED USER***.  
따라서 "cat flag.txt"를 실행하더라도 프로그램이 파일에 접근할 권한이 없기 때문에 플래그가 출력되지 않습니다. 이는 플래그를 읽기 위해 루트로 권한을 상승시켜야 함을 의미합니다.

## 2번째 방법 - CVE-2021-4034
CVE-2021-4034를 악용하려면 다음 설정이 필요합니다:
1. `GCONV_PATH=.`라는 이름의 디렉토리
2. 이 디렉토리 안에, `gconv-modules` 설정 파일이 위치할 디렉토리 이름과 동일한 실행 파일이 필요합니다. 이 예제에서는 `recipe`라고 합시다.
3. `recipe` 디렉토리 안에, 내용을 제어할 수 있는 `gconv-modules` 파일과 셸을 생성해 줄 동적 라이브러리가 필요합니다 (아마 주어진 바이너리 `shelly.so`일까요?).
4. 그런 다음 `gconv-modules`에 아래 5단계와 동일한 CHARSET과 동적 라이브러리 이름 `shelly.so`를 다음과 같이 입력합니다: `module UTF-8// SHELLY// shelly 2`.
5. 이 모든 작업 후에, NULL argv와 조작된 환경 변수 배열 `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}`로 `pkexec`를 호출합니다.

이제 이 챌린지를 악용하여 위 설정을 달성합니다.

***
### 목표 1-2
1-2단계는 쉽습니다: `GCONV_PATH=.`라는 사용자 이름으로 로그인하여 이 디렉토리를 생성하기만 하면 됩니다. 그런 다음 `checkout`을 수행하여 이 디렉토리 안에 실행 파일 `recipe`를 만듭니다. 그런 다음 첫 번째 메뉴로 돌아갑니다.
***
### 목표 3-4
`recipe`라는 사용자 이름으로 로그인하여 이 디렉토리를 생성합니다. 이제 `gconv-modules`라는 파일을 생성해야 *하지만*, `checkout`은 `recipe`와 `notes` 파일만 생성합니다.

문자열 `/notes`의 메모리 위치를 덮어써서 `/gconv-modules`로 바꾸면 어떨까요? 이 방법이 작동할 수 있지만, 해당 메모리가 쓰기 가능해야 합니다. gdb에서 확인해 보겠습니다:```gdb
gef➤  search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
  0x555555559010 - 0x555555559016  →   "/notes" 

그리고 쓰기 가능합니다!

어떻게 쓰기를 수행할까요?

  1. 위에 설명된 임의 쓰기 전략을 사용하여 유출된 스택 주소의 위치를 읽습니다. 이를 통해 /recipe가 어디에 있는지 알 수 있습니다.
  2. gdb에서 빠르게 확인하면 /recipe에서 /notes까지의 오프셋이 0x10임을 알 수 있습니다. 1단계의 /recipe 주소에서 이 오프셋을 빼면 /notes 주소를 얻을 수 있습니다.
  3. 위의 임의 쓰기 전략을 사용하여 /notes를 /gconv-modules로 덮어씁니다.

gconv-modules 파일에 올바른 내용을 쓰기 위해, checkout 시 청크의 notes 내용이 /notes(현재는 /gconv-modules)에 기록된다는 것을 알고 있습니다. 확실히 하기 위해, 모든 notes에 문자열 module UTF-8// SHELLY// shelly 2를 제공하면 됩니다.

따라서 이제 checkout을 호출할 때마다 recipe와 gconv-modules라는 두 개의 파일이 생성되고, gconv-modules 파일에 module UTF-8// SHELLY// shelly 2 (shelly를 사용하는 이유는 주어진 set-uid-root 라이브러리 .so의 이름이기 때문)와 같은 내용이 기록됩니다. 또한 같은 디렉토리에 shelly.so도 복사됩니다.

목표 5

  1. notes가 해당 환경 변수인 4개의 청크를 할당합니다. notes 포인터 배열에는 이 4개의 notes에 대한 4개의 포인터와 종료 NULL 포인터가 포함되며, 이것이 정확히 우리가 원하는 것입니다.
  2. 추가 청크를 할당하고 목표 3-4에서 사용한 동일한 전략을 사용하여 main 함수의 RIP를 비밀 함수 FUN_00101e84로 덮어씁니다.
    • RIP가 어디에 있는지 어떻게 알 수 있나요? 프로그램 시작 시 유출된 스택 주소를 기억하시나요? gdb에 들어가서 RIP까지의 오프셋을 찾은 다음, 실제 유출된 주소에 그 오프셋을 더하여 RIP를 얻습니다.
    • main이 어디에 있는지 어떻게 알 수 있나요? 목표 3-4의 2단계에서 유출된 /recipe의 주소를 기억하시나요? 여기서도 동일한 작업을 수행하세요!
  3. 셸을 즐기세요 :)

해설이 포함된 풀이 스크립트는 이 저장소에 제공되어 있습니다. 질문이 있으시면 언제든지 연락주세요 :)

도구 다운로드
nxt
0x55555555**ab**e8
0x55555555**aa**e8
  • 따라서 두 번째 최하위 바이트가 동일한 현재 청크와 다음 청크를 선택해야 합니다.
  • 이제 세 번째 청크는 C 대신 B + 8에 있습니다. 청크 B를 편집하여 첫 번째(bread) 필드에 쓰려는 target 주소를 포함시킵니다.
  • 세 번째 청크를 편집하면 B + 8부터 시작하는 내용을 덮어씁니다. 이 세 번째 청크의 notes 포인터가 target입니다! 따라서 notes에 쓰는 모든 내용이 target에 기록됩니다!