system 함수로 bash 셸 스크립트로 shock_me를 출력하고 있다. 문제 설명에도 나와 있듯이 bash에 관한 취약점인것 같아서 bash 취약점을 키워드로 검색해보니 bash shellshock 취약점을 찾을 수 있었다. 환경변수와 함께 선언되는 함수 뒤에 원하는 명령어를 삽입할 수 있는 취약점이다. 환경변수를 설정할 때 (){ return; };처럼 함수를 설정하고 ; 뒤에 실행할 명령어를 입력하면 된다.
pwd 명령어를 실행하도록 입력하였더니 shellshock 에서 bash명령을 실행할 때 등록된 환경변수를 읽어와서 pwd명령을 실행한 결과를 보여준다. 따라서 다음과 같이 flag를 출력하도록 하고 ./shellshock를 실행하면 shellshock_pwn 계정의 권한으로 flag를 읽은 결과를 출력해준다.
//argv로 주석이 되어 있는 부분의 코드를 보면 인자의 크기가 100이 아니면, 인자의 'A'인덱스 즉 65번째 인덱스가 "\x00"가 아니고, 인자의 'B' 인덱스 즉 66번째 인덱스가 "\x20\x0a\x0d"가 아니면 프로그램을 종료하고, 위 조건들을 만족시키는 경우 Stage 1 clear을 출력한다.
//stdio로 주석이 되어 있는 부분의 코드를 보면 read 함수로 buf를 읽어오는데, 0(stdin)이 "\x00\x0a\x00\xff"이 아닐 경우, 2(stderr)이 "\x00\x0a\x02\xff"가 아닐 경우 조건을 프로그램을 종료하고, 조건들을 만족시킬 경우 Stage 2 clear을 출력한다.
//env로 주석이 되어 있는 부분의 코드를 보면 getenv()함수로 환경변수 "\xde\xad\xbe\xef"의 값이 "\xca\xfe\xba\xbe"가 아닐 경우 프로그램을 종료하고, 조건을 만족하면 Stage 3 clear을 출력한다.
//file로 주석이 되어 있는 부분의 코드를 보면 fopen()함수로 "\x0a"라는 이름의 파일을 열고 파일을 읽어 파일의 내용이 "\x00\x00\x00\x00"가 아니면 프로그램을 종료하고, 조건을 만족하면 Stage 4 clear을 출력한다.
//network로 주석이 되어 있는 부분의 코드를 보면 sockaddr_in 구조체를 사용하고 있고, 소켓 생성에 실패(socket 함수 -1 반환)하면 socket error을 출력하고 프로그램을 종료한다. saddr의 포트 정보에 인자의 'C' 인덱스 즉 67번째 인덱스의 값을 넣고, bind 함수로 소켓 통신할 준비를 해준다. 이 과정에서 에러가 발생할 경우 bind error use another port를 출력하고 프로그램을 종료한다. accept()함수로 소켓을 연결하고, 에러가 발생할 경우(return -1) accept error tell admin을 출력하고 프로그램을 종료한다. 이후 소켓에서 4바이트를 받아와서 받아온 값이 "\xde\xad\xbe\xef"가 맞다면 Stage 5 clear을 출력한다.
위 5가지의 조건을 모두 만족시키면 플래그를 읽을 수 있다.
문제를 해결하기 위해 pwntools를 이용하여 파이썬 코드로 작성하였다.
첫번째 조건을 만족시키기 위해 인자값을 조건에서 주어진 대로 설정하고, 인자를 전달하려면 process 함수의 argv 옵션을 이용하여 전달할 수 있다.
두 번째 조건을 만족시키기 위해 stdin으로는 "\x00\x0a\x00\xff"를 전달하였고, stderr일 경우 전달하는 파일을 만들어 "\x00\x0a\x02\xff"을 저장하여 process함수의 stderr옵션을 이용하여 전달하였다.
세 번째 조건을 만족시키기 위해 process함수의 env 옵션을 이용하여 환경변수를 설정하였다.
네 번째 조건을 만족시키기 위해 조건에 주어진 이름의 파일을 만들고, 파일 내용도 조건에 주어진 대로 지정해 주었다.
다섯 번째 조건을 만족시키기 위해 인자의 67번째('C')인덱스에 포트를 지정하고 지정한 포트로 nc 연결을 하여 조건에서 주어진 값을 전달하였다.
따라서 코드로 작성하면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import\\\*
argvs = [str(i) for i inrange(100)]
argvs[65] ='\x00'
argvs[66] ='\x20\x0a\x0d'
argvs[67] ='3000'
p = process(executable='/home/input2/input', argv=argvs, stderr=open('./stderr'), env={'\xde\xad\xbe\xef':'\xca\xfe\xba\xbe'})
xor 함수에서는 문자열과 길이를 인자로 받아 길이만큼 반복하며 문자열의 각 문자를 1씩 xor 연산을 한다.
main 함수의 첫번째 if문을 보면 할당연산자보다 비교연산자가 우선순위가 높아서 비교연산을 먼저 수행하면 open함수의 리턴값은 파일을 잘 읽어와서 fd 값을 반환하고, fd값은 음이 아닌 정수이기 때문에 비교 연산의 결과는 거짓이므로 fd에는 flase(0)값이 들어가고, 조건문은 수행하지 않는다.
PW_LEN+1 크기의 pw_buf를 선언하고, if 함수의 조건식에서 read함수를 실행한다. fd값이 위에서 0으로 저장되었기 때문에 fd 0 은 stdin 이라 PW_LEN만큼 pw_buf에 입력을 받는다. 입력을 받으면 크기는 0보다 커지기 때문에 not 연산으로 조건식은 거짓이 되어 조건문은 실행하지 않는다.
PW_LEN + 1 크기의 pw_buf2를 선언하고, 입력받는다. 이후 xor함수로 xor연산을 진행한다. 이후 조건문에서 xor한 pw_buf2와 pw_buf가 같다면 시스템 함수로 플래그를 출력할 수 있다.
xor 함수는 각 자리를 1로 xor 연산 하므로 1 xor 1 = 0 이 되므로 첫번째 입력값에는 0을 10개, 2번째 입력에서는 1을 10개 입력하면 플래그를 확인할 수 있다.
rand 함수를 이용해 변수 random에 값을 저장하는데, rand()함수만 사용하면 프로그램이 생성될 때 값이 정해지기 때문에 계속 실행해도 같은 값이 저장되어 있다. key를 입력받아서 key ^ random 값이 0xdeadbeef면 시스템 함수로 flag를 읽을 수 있다. random 값을 알아야 하기 때문에 gdb로 프로그램을 실행시켜 main 함수를 보면 다음과 같다.
rand 함수가 실행되고 난 후(main+18)에 breakpoint를 걸고 실행한 후에 현재 레지스터를 확인해주면 다음과 같다.
산술연산 및 함수 반환값은 rax에 들어가기 때문에 0x6b8b4567이 rand()함수로 반환된 값이다.
0x6b8b4567과 0xdeadbeef를 xor한 값이 passcode에 key로 입력되면 if 조건문을 만족시킬 수 있을 것 같다.
두 값을 xor 연산 후 10진수로 변환하면 3039230856이 나온다. random 을 실행하고 10진수로 변환한 값을 넣으면 플래그를 획득할 수 있다.
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 이다.
문제 서버에 접속하면 다음과 같이 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을 입력하면 조건문을 만족시켜 플래그가 출력된다.