오랜만에 간단한 CTF 문제를 하나 풀어보겠습니다. 이번 문제는 우리나라에서 개최되는 메이저 CTF중 하나인 Codegate 문제입니다.


문제 이름은 "Secret Note" 이고 올해 예선전때 일반부 pwn 카테고리에서 첫번째 문제로 출제 됐습니다.


pwn은 pwnable, binary exploit, system hacking 등등 여러가지 이름으로 불리는 분야로


특정 프로그램의 취약점을 찾아내고 그 취약점으로 서버의 엑세스 권한을 얻은뒤 임의 코드를 실행하거나 파일을 읽거나 하는것이 목표인 분야입니다.


보통 CTF에서 나오는 pwnable은 문제 서버에서 취약한 바이너리가 서비스되고 있고 공격자는 그 바이너리를 분석해서 취약점을 기반으로 서버를 공격해서 플래그를 읽어오는 방식입니다.



이제 해당 문제 바이너리를 분석 해보겠습니다.


viewimage.php?id=2ab4c42ef0d0&no=24b0d769e1d32ca73ce882fa1bd62531c0a64993133e134c29906eec98920e7cf5607361606b758cd39206919e88b61c1f33e70f86a15aa7a310048068ad3628a26026


우선 적용된 보호기법을 확인해보면 모든 보호기법이 다 걸려있습니다.


이것만 봐도 전형적인 note 챌린지이고 heap exploit 을 해야하는 문제라고 파악할수 있습니다.



init(); while ( 1 ) { while ( 1 ) { menu(); __isoc99_scanf("%d", &v3); if ( v3 != 3 ) break; delete(); } if ( v3 <= 3 ) { if ( v3 == 1 ) { create(); } else if ( v3 == 2 ) { edit(); } } } }



main 함수를 살펴보면 역시나 create, edit, delete 로 구성된 간단한 note 챌린지 라는걸 알수 있습니다.



v3 = 0; printf("Index: "); __isoc99_scanf("%d", &v2); if ( v2 < 0 || v2 > 15 ) { LABEL_9: puts("Error"); return v6 - __readfsqword(0x28u); } if ( !chunks[v2] ) { v0 = v2; chunks[v0] = malloc(16uLL); } printf("Key: "); __isoc99_scanf("%u", &v3); if ( v3 <= 0x1000000 ) { v4 = chunks[v2]; printf("Size: "); __isoc99_scanf("%d", v4 + 8); if ( *(int *)(v4 + 8) <= 1024 ) { buf = malloc(*(int *)(v4 + 8)); if ( buf ) { printf("Data: "); read(0, buf, *(int *)(v4 + 8)); *(_QWORD *)v4 = buf; *(_DWORD *)(v4 + 12) = v3; puts("Save completed"); return v6 - __readfsqword(0x28u); } } goto LABEL_9; } printf("Error");



함수를 분석해보면 처음에 할당되는 note 청크의 구조를 짐작해볼수 있는데



struct note { char *buf; int size; unsigned int key; };



이렇게 구성됐다고 유추해볼수 있습니다.



그리고 이 문제에서는 create 함수에 2가지 취약점이 존재하는데


1. idx를 입력하면 chunks[idx]에 0x10 만큼 할당한 힙 주소를 저장하는데 key값을 0x1000000 보다 크게 입력하면 Error가 출력되면서 함수가 반환되지만 chunks[idx] 는 초기화 되지 않음.

2. 적절한 key값을 입력하더라도 size를 1024보다 크게 입력하면 *(int *)(v4 + 8) 위치에 입력된 size와 chunks[idx] 에 복사된 힙 주소가 초기화 되지 않고 반환된다.



고작 이런걸로 뭘 할수 있는거냐고 생각할수 있지만 끝까지 읽다보면 이것이 왜 취약점인지 이해하실수 있을겁니다.





우선 이 프로그램은 바이너리에 PIE가 걸려있기도 하고, 어차피 바이너리 자체에는 익스에 활용할만한 요소가 별로 없어서 libc를 활용해야 합니다.


그러므로 libc base 와 heap base 를 leak 하는것이 먼저인데 이것은 create 함수의 첫번째 취약점과 edit 함수를 활용하면 됩니다.




v2 = 0; printf("Index: "); __isoc99_scanf("%d", &v1); if ( v1 >= 0 && v1 <= 15 && (v3 = chunks[v1]) != 0 && *(_QWORD *)v3 && (printf("Key: "), __isoc99_scanf("%u", &v2), *(_DWORD *)(v3 + 12) == v2) ) { printf("Data(%d): ", *(_DWORD *)(v3 + 8)); read(0, *(void **)v3, *(int *)(v3 + 8)); puts("Edit completed"); } else { puts("Error"); }



edit 함수를 살펴보면 올바른 idx를 입력하면 chunks[idx] 에서 이전에 할당한 힙 주소를 가져오고 올바른 key값을 입력하면 size를 출력해주고 size만큼 buf를 쓰는것이 가능합니다.


이걸 어떻게 활용해야 원하는 주소를 leak 할수 있을까 고민해보면, edit 함수는 올바르지 않은 key를 입력한다고 해서 프로그램을 종료하지 않습니다.


이로인해 브포를 때리면 언젠가는 key를 알아낼수 있기도하고 위에서 유추했던 note 청크의 구조를 생각해보면 libc나 heap의 base를 leak 하는것이 가능하다는것을 알수 있습니다.




viewimage.php?id=2ab4c42ef0d0&no=24b0d769e1d32ca73ce882fa1bd62531c0a64993133e134c29906eec98920e7cf5607361606b758cd39206919e88b61c1f33e70f86a15aaaf140078033af6228c157ed




힙 청크의 구조를 생각해보면 처음 할당된 청크는 16바이트의 헤더와 요청한 사이즈 이상을 16바이트로 정렬한 크기만큼 할당된 공간으로 구성되는데


이것을 free 할 경우, 16바이트의 헤더와 각각 8바이트인 fd, bk로 구성된 freed 청크가 됩니다. 이로인해 기존에 size와 key가 위치한 곳에 bk의 주소값이 들아가게 되면서


이후 해당 freed 청크를 재할당 했을때 bk에 남아있던 주소값이 size와 key에 들어가게 됩니다.


저는 unsorted bin의 특성을 활용해서 bk에 임의의 libc 주소와 heap 주소를 남겨놨습니다. 해당 주소의 오프셋은 힙 할당 순서가 바뀌지 않는 이상 거의 일정하므로 오프셋 자체가 바뀔 걱정은 거의 안해도 됩니다.




문제는 기본적으로 size와 key는 create 함수에서 초기화를 해서 bk에 남아있던 주소가 사라진다는 것인데, create 함수의 첫번째 취약점을 활용하면 chunks 배열에 원하는 청크를 재할당 하면서 size와 key는 초기화 되지 않기 때문에 임의의 주소를 size,key에 넣을수 있게 됩니다.




.... # RUN chmod +x ./setup.sh RUN chmod +x ./run.sh # RUN ./setup.sh RUN chown -R root:ctf /home/ctf RUN chmod -R 750 /home/ctf RUN chmod 444 /home/ctf/flag EXPOSE ${PORT} CMD socat -T60 -t60 TCP-LISTEN:${PORT},reuseaddr,fork EXEC:./run.sh,stderr



그리고 size는 어차피 key를 맞추면 출력해주므로 key만 브포때리면 되지만, 4바이트면 21억이 넘는 경우의수인데 60초 제한이 걸려있는 원격서버를 상대로 이만큼의 브포를 어떻게 때리냐고 생각할수도 있지만


어차피 리틀엔디안 구조에 따라서 key는 주소값의 상위 4바이트가 위치하게 되고, 유저레벨에서의 주소값은 모두 6바이트로 구성되있으므로 2바이트만 브포 때리면 leak하는것이 가능합니다.


게다가 이마저도 1.5 바이트로 줄일수 있는데, 특이 케이스를 제외한 일반적인 경우에 libc 주소는 0x7XX... 로 시작하고 heap 주소는 0x5XX.... 로 시작하기 때문에, 상위 0.5바이트는 고정시켜두고 브포를 때려도 거의다 맞출수 있습니다.




이렇게 libc, heap base 를 얻은 다음에는 실질적인 익스를 해야하는데


이 문제서버의 운영체제는 ubuntu 22.04이고 glibc는 2.35 버전이므로 일반적으로 많이 쓰던 공격기법들은 안되는 경우가 많습니다.


그러므로 저는 house of apple 이라는 기법으로 익스를 했는데, 이건 fsop 기법이므로 이걸 사용하려면 입출력버퍼 FILE 구조체를 조작하거나


바이너리의 bss영역에 있는 입출력버퍼 포인터를 fake FILE의 주소로 조작해야 합니다.




저는 AAW를 사용해서 stdout FILE 구조체를 조작하는 방식으로 익스를 진행했는데 이건 create 함수의 두 번째 취약점을 활용하면 됩니다.


create 함수에서 size를 크게 입력하더라도 chunks[idx]를 초기화 하거나 청크를 free 시키지도 않고 size를 직접 note->size에 입력하는 방식이라서 잘못된 size가 계속 남아있습니다.


이로인해 잘못된 size값을 가진 note 청크를 edit에서 접근하는것이 가능해지면서 힙 오버플로우가 발생합니다.


이것을 활용해서 fake chunk 를 만들면 AAW가 가능해지고 house of apple 기법을 활용해서 fake stdout 을 기존 stdout 구조체에 덮어쓰면 쉘이 따이게 됩니다.





24b0d121e09c28a8699fe8b115ef046c63f12b4d