main 함수에서 welcome 함수와 login 함수를 실행하고 있고, wlecome 함수에서는 이름을 입력받아 출력하고 있고, login함수에서는 passcode1과 passcode2를 입력받고, 두 값이 모두 조건과 일치하면 시스템 함수로 cat flag를 실행한다. 그러나 두 값을 입력받는 곳을 보면 주소 연산자를 사용하고 있지 않아서 입력받아도 passcode1과 passcode2에는 입력받은 값이 들어갈 수 없다.
또한 login 함수에 있는 fflush(stdin)은 입력 버퍼를 지우는 코드이다. 그러나 리눅스에서 해당 코드는 실행이 안된다고 한다.
welcome함수와 login함수를 gdb로 보면 다음과 같다.
스택의 구조를 생각해보면 welcome 함수에서 name을 입력받기 전 0x70만큼의 공간을 확보하고 있고, login함수에서는 passcode를 입력받기 전 0x10만큼의 공간을 확보하고 있다. name의 크기는 100바이트 지만 0x70-0x10=0x60은 10진수로 96이므로 4바이트가 남고, int형 정수인 passcode1의 값을 변경시킬 수 있다. fflush의 got을 flag를 출력하는 시스템 함수의 주소로 바꾸면 flag를 출력할 수 있을 것 같다.
fflush plt에 들어가 fflush의 got주소를 구할 수 있다.
fflush의 got주소는 0x0804a004이다. 시스템 함수의 시작점은 0x080485e3이지만 scanf함수에서 정수로 받기 때문에 10진수로 변환하면 134514147 이다.
CTF에서 포너블 문제의 형식을 보면 바이너리가 주어지고, 문제 서버가 보통 nc서버로 주어진다. 바이너리를 분석하여 nc 서버에 payload를 보내서 문제를 해결하는 방식이다. 서버에 접속하면 바이너리가 바로 실행되는 형태가 많다. 이와 같은 상태의 서버를 구축하기 위해 xinetd를 설치해야 한다. xinetd는 네트워크에서 들어오는 요청을 받아 그 요청에 적절한 서비스를 실행한다. 요청은 포트번호를 식별자로 사용하여 구분한다.
$ sudo apt-get install xinetd
설치가 다 되었으면 /etc/xinetd.d 경로에 서비스 이름으로 설정 파일을 만들어야 한다. 테스트용 서비스로 hello를 만들것 이기 때문에 hello 파일을 만들었다. hello 파일에 다음과 같은 내용을 작성한다.
마지막 줄 server에 서버에 접속했을 때 실행시킬 바이너리 파일의 경로를 적어주면 된다.
이후 /etc/services 파일의 하단 # Local services 아래에 실행시킬 서비스와 포트 번호를 추가해준다.
이후 지정한 포트 번호로 nc 접속하면 서비스가 제대로 동작함을 확인할 수 있다.
테스트용 바이너리의 소스코드는 다음과 같다.
#include <stdio.h>
int main()
{
printf("hello\n");
return 0;
}
문제 서버에 접속하면 다음과 같이 flag 파일이 있고, set uid가 걸려있는 파일이 있다.
flag 파일은 fd 계정으로는 읽을 권한이 없어서 setuid가 걸린 fd를 통해 flag파일을 확인할 수 있을 것 같다. fd의 소스코드로 보이는 fd.c의 내용은 다음과 같다.
fd를 실행할 때 인자가 없으면 pass로 시작하는 문자열을 출력하고 프로그램을 종료한다.
fd를 argv[1] - 0x1234로 초기화하고 len에 read함수로 buf에서 32바이트를 읽어오고 있다. read함수의 첫번째 인자는 fd(파일 디스크립터)라고 한다. 파일 디스크립터는 0은 표준 입력, 1은 표준 출력, 2는 표준 에러 이다. 따라서 위 소스에서 fd가 0이 되면 입력 받을 수 있고, 입력받은 내용이 buf에 들어간다. 그리고 buf가 LETMEWIN이라면 시스템 함수로 flag를 출력한다.
fd가 0이되려면 0x1234는 4660이므로 인자로 4660을 넣으면 fd가 0이되서 표준 입력으로 read함수를 실행하기 때문에 LETMEWIN을 입력하면 조건문을 만족시켜 플래그가 출력된다.
setuid 함수로 level 13 계정의 uid로 바꿔주고 있고, gets 함수로 입력을 받는다. 입력을 받는 str의 크기는 256바이트 이지만, 입력값에서 제한을 하고 있지 않기 때문에 bof 취약점이 발생할 수 있다. 정상적인 입력값을 받는다면 문자열을 입력받고, 입력값을 그대로 출력해주는 프로그램이다.
파일 목록을 보면 attackme라는 파일이 있었고, 그 파일을 tmp 디렉토리로 복사하여(권한문제 때문) gdb로 main 함수를 확인하며 다음과 같다.
str의 크기는 256이지만 264 만큼의 크기를 확보하고 있기에 str 256바이트에 dummy 8바이트가 들어가는 것 같다. str의 시작점에서 ret까지의 거리는 str의 크기 256 + dummy(8) + SFP(4) = 268이다. 전 문제 처럼 환경변수에 쉘 코드를 넣고, RET에 환경변수의 주소를 넣어 level 13 계정의 쉘을 딸 수 있을 것 같다.
find 명령어의 size 옵션을 활용해서 2700 용량을 가진 파일을 찾을 수 있었다. 크기 뒤에 b, c, k, w를 붙여 확인해본 결과 2700은 바이트 단위임을 알 수 있었다.
찾은 found.txt 파일을 열어보니 다음과 같이 level9의 shadow 파일임을 알 수 있었다.
파일의 내용을 존 더 리퍼라는 패스워드 크랙 툴을 이용하여 크랙하면 다음과 같은 비밀번호를 얻을 수 있다.
Level 9
문제의 hint파일의 내용은 다음과 같다.
bof파일을 통해 level10의 권한을 얻어야 하는것 같고, buf2의 앞 두글자가 go와 같으면 시스템 함수를 실행시켜 주는 것 같다. 입력값은 buf로 받고 있는데, buf의 크기는 10인데, 입력값 제한을 40으로 하고 있어서 bof가 발생한다.
gdb로 main함수를 보면 다음과 같다.
입력값은 ebp - 40에 들어가고 있고, strncmp로 비교하는 값은 ebp-24 에 들어가고 있다. ebp - 40이 buf이고, ebp-24가 buf2인 것이다. 메모리 공간이 16만큼 차이가 나므로 프로그램을 실행하고 입력할 때 16개만큼 다른 값으로 채우고, go를 넣으면 buf2에 go가 들어가서 조건문을 만족시킬 수 있다.
Level 10
시스템에서 프로세스의 메모리는 그 프로세스만 접근이 가능하지만, 공유 메모리를 사용하게 되면 다른 프로세스에서도 메모리에 접근할 수 있다. 공유메모리의 key_t값이 주어졌기 때문에 이 키 값의 공유메모리를 사용하는 프로그램을 만들어서 대화 내용이 있는 메모리에 접근할 수 있을 것 같다.
다음과 같이 C코드를 작성하여 위 힌트의 대화방과 같은 키값을 사용하여 메모리를 공유하는 프로그램을 만들어 줬다.
공유메모리를 생성하려면 sys/shm.h 헤더파일과 sys/ipc.h헤더파일을 사용해야 한다. shmget 함수로 7530 키를 가진 공유 메모리를 생성할 수 있고 shmat 함수로 공유메모리 프로세스에 접근할 수 있다. 메모리 주소를 반환하며 SHM_RDONLY옵션을 사용하면 읽기 전용이다. 메모리에 저장된 값을 출력하면 다음과 같은 내용이 나와 level11의 비밀번호를 알 수 있다.
Level 11
hint파일의 내용은 다음과 같다.
파일 목록을 보면 attackme 라는 파일이 있는 것을 확인할 수 있다.
힌트 파일의 내용이 attackme 파일의 소스코드 인 것 같다. 인자로 문자열을 받고 있고 인자로 받은 문자열을 str에 저장하고, str을 출력하는 프로그램이다.
문제 파일을 gdb로 실행하면 권한에 의한 오류가 발생하기 때문에 파일을 복사하여 복사한 파일을 분석해야 한다.
gdb로 main 함수를 보면 다음과 같다.
ebp - 264를 사용하는 것으로 보아 256크기의 str 변수에 8바이트의 dummy가 들어가는 것 같다. 쉘 코드를 str에 넣고, str에서 bof를 발생시켜 ret까지 덮은 후에 ret에 str의 주소를 넣어 쉘 코드를 실행시킬 수 있을것 같아서 시도해보니 str의 주소가 프로그램을 시작할때마다 무작위로 바뀌는 aslr 보호 기법이 적용되어 있어서 불가능할 것 같다.
다른 방법을 찾아보다 환경변수는 리눅스 시스템에 저장되어 있기 때문에 한번 설정하면 주소가 바뀌지 않아서 환경 변수에 쉘 코드를 넣고 ret에 환경변수의 주소를 덮으면 쉘을 딸 수 있다고 한다.
hint 파일을 읽어보면 "level2 권한의 setuid가 걸린 파일을 찾는다." 라고 쓰여 있다.
파일을 찾는것은 find 명령어를 통해 찾을 수 있으며 -user 옵션을 통해 파일 소유자를, -perm 옵션은 권한을 의미하며 4000 권한은 setuid가 설정되어 있음을 의미한다. 따라서 다음과 같은 명령어로 힌트로 주어진 파일을 찾을 수 있다.
$ find / -user level2 -perm -4000 2>/dev/null
2>/dev/null은 표준에러(2)가 발생하면 /dev/null(리눅스의 휴지통)으로 넣어준다는 의미이다.
명령어를 실행하면 다음과 같은 결과가 나온다.
/bin/ExecuteMe 파일을 실행하면 level2 권한으로 명령어를 실행시켜준다고 나오고, /bin/bash 명령을 입력하면 level2의 쉘을 딸 수 있다.
Level 2
hint 파일을 읽어보면 텍스트 파일 편집 중 쉘의 명령을 실행시킬 수 있다고 한다.
level3 권한으로 setuid가 걸려있는 파일을 확인해보면 다음과 같다.
editor을 실행시키면 vim 텍스트 에디터가 실행된다.
vim 텍스트 에디터에서는 ! 뒤에 명령어를 사용하면 쉘 명령어를 사용할 수 있다.
! id로 level3 사용자 권한으로 명령어를 실행한다는 것을 알 수 있다.
! my-pass로 level3의 비밀번호를 확인할 수 있고 ! /bin/bash로 쉘을 딸 수 있다.
Level 3
hint 파일을 읽어보면 다음과 같다.
C 코드를 읽어보면 인자의 갯수가 2가 아니면 if문이 실행되면서 printf함수로 출력하고 프로그램을 종료한다. cmd라는 배열에 dig @와 argv[1] 인자로 준 값, 이후 문자열이 들어가고 있고 시스템 함수로 cmd 배열의 문자열을 실행한다. autodig 프로그램을 실행하면서 인자로 명령어를 넣어줘야 하는데 이미 앞에서 dig 명령어가 사용되고 있기 때문에 동시에 여러 명령어를 사용하려면 세미클론을 사용하여 인자를 전달하면 된다.
autodig 프로그램은 find명령어로 찾으면 다음과 같은 곳에 있다.
autodig를 실행하고 인자로 ";/bin/bash;"를 넣어주면 level4의 쉘을 딸 수 있고, my-pass 명령어로 비밀번호를 확인할 수 있다.
Level 4
hint 파일을 보면 "누군가 /etc/xinetd.d/에 백도어를 심어놓았다.!" 라고 한다.
해당 경로의 파일 목록을 보면 다음과 같이 backdoor파일을 확인할 수 있다.
backdoor파일의 내용을 보면 다음과 같다.
finger 이라는 서비스를 통해 level5의 권한으로 /home/level4/tmp/backdoor이라는 서버 파일을 실행한다.
finger 명령어는 리눅스에서 사용자의 계정정보를 확인하는 명령어이다. finger을 실행하면 backdoor 파일의 내용으로 인해 /home/level4/tmp/backdoor 파일이 실행될 것 같다.
tmp 폴더로 이동하여 backdoor 파일을 만들어야 한다.
시스템함수로 my-pass 명령을 실행하는 c 코드를 작성하고 gcc로 컴파일 하고 finger명령어를 실행하면 비밀번호를 알 수 있다.
Level 5
hint 파일을 읽어보면 다음과 같다.
힌트의 level5 파일은 level6 계정 권한으로 setuid가 걸려있음을 확인할 수 있다.
level5를 실행하고 /tmp 디렉토리를 확인해보면 level5.tmp라는 임시파일이 없다. 임시파일을 만들고 삭제시켜주는것 같다.
생성되는 임시파일을 이용하여 권한상승을 하는 레이스 컨디션 공격 기법이 있다고 한다.
레이스 컨디션(Race Condition)은 한정된 자원을 동시에 이용하려는 여러 프로세스가 자원의 이용을 위해 경쟁을 벌이는 현상이라고 한다. 레이스 컨디션을 이용한 공격은 취약 프로그램이 생성하는 임시 파일의 이름을 파악하고, 그 임시파일과 같은 이름의 파일을 생성히고 이 파일에 심볼릭 링크를 생성한 후 원본 파일을 지운다. 원본 파일이 지워진 상태에서 취약한 프로그램이 심볼릭 링크를 건 파일과 같은 파일을 생성할 때를 기다렸다가 심볼릭 링크를 이용해 파일의 내용을 변경한다.
이 문제의 경우 취약한 파일은 level5 이고, 임시파일의 이름은 level5.tmp가 된다.
tmp 디렉토리에 level5.tmp에 심볼릭 링크로 연결할 파일을 하나 만들고 심볼릭 링크로 연결했다.
이후 level5를 실행하면 심볼릭링크로 연결된 level5에 level5.tmp의 내용이 저장되고 /tmp/level5파일의 내용을 확인하면 다음 레벨의 비밀번호를 확인할 수 있다.
Level 6
level6 계정으로 접속하면 다음과 같은 힌트가 나오고, 텔넷 접속 서비스가 나온다.
번호를 입력하면 ssh 연결이 끊기고, ctrl + c로 현재 프로그램을 종료하려고 하면 ctrl + c를 사용할 수 없다고 한다.
다시 level6 계정으로 접속해서 힌트만 보이는 화면에서 ctrl + c를 누르면 현재 프로그램이 강제 종료되고 프롬프트가 나온다. 파일 목록을 확인해보면 password 파일이 있고 password파일을 확인해보면 level7계정의 비밀번호를 알아낼 수 있다.
Level 7
hint 파일을 보면 다음과 같은 내용이 나온다.
level7 을 실행해보면 wrong.txt가 존재하지 않는다는 메시지가 나오면서 문제를 더이상 진행할 수 없다.