-
pwnable.kr 1번 fd정보보안/포너블 2017. 12. 26. 20:12
pwnable.kr 1번 문제 fd
리눅스의 파일 서술자에 대한 문제다.
'표준 입력(stdin)'은 0, '표준 출력(stdout)'은 1, 또 더 있나 싶어서 위키백과에서 찾아보니 '표준 오류(stderr)'의 2도 있다고 한다.
12345678910111213141516171819202122232425262728293031323334353637383940414243root@goorm:/workspace/JunhoYeo# ssh fd@pwnable.kr -p2222fd@pwnable.kr's password:____ __ __ ____ ____ ____ _ ___ __ _ ____| \| |__| || \ / || \ | | / _] | |/ ]| \| o ) | | || _ || o || o )| | / [_ | ' / | D )| _/| | | || | || || || |___ | _] | \ | /| | | ` ' || | || _ || O || || [_ __ | \| \| | \ / | | || | || || || || || . || . \|__| \_/\_/ |__|__||__|__||_____||_____||_____||__||__|\_||__|\_|- Site admin : daehee87.kr@gmail.com- IRC : irc.netgarage.org:6667 / #pwnable.kr- Simply type "irssi" command to join IRC now- files under /tmp can be erased anytime. make your directory under /tmp- to use peda, issue `source /usr/share/peda/peda.py` in gdb terminalLast login: Tue Dec 26 00:40:17 2017 from 13.125.15.101fd@ubuntu:~$ lsfd fd.c flagfd@ubuntu:~$ cat flagcat: flag: Permission deniedfd@ubuntu:~$ cat fd.c#include <stdio.h>#include <stdlib.h>#include <string.h>char buf[32];int main(int argc, char* argv[], char* envp[]){if(argc<2){printf("pass argv[1] a number\n");return 0;}int fd = atoi( argv[1] ) - 0x1234;int len = 0;len = read(fd, buf, 32);if(!strcmp("LETMEWIN\n", buf)){printf("good job :)\n");system("/bin/cat flag");exit(0);}printf("learn about Linux file IO\n");return 0;}fd@ubuntu:~$cs 지금 노트북의 OS를 Windows 10으로 바꿨기 때문에 goorm.io의 Terminal을 사용해서 ssh로 해당 문제 서버에 접속했다.
현재 디렉토리의 파일을 ls 명령으로 확인해 보았더니, flag, fd, fd.c의 세 파일이 있었다.
flag 파일의 내용에 Flag가 있을 것 같아서 cat 명령으로 내용 출력을 시도했지만 이게 될 리가 없지.
fd.c 파일은 확장자가 .c인 것을 보아 C언어 소스코드 파일이고, fd는 fd.c가 컴파일된 프로그램인 것 같다.
cat 명령으로 fd.c 파일 내용 출력이 가능해서 확인해보니 그렇다.
fd는 file 명령으로 file type을 확인해 볼 수 있겠지만 그럴 필요성을 못 느꼈기에 pass...
코드를 읽어보니 argv[1]-0x1234가 파일 서술자 부분에 들어가는 것 같다.
즉, 표준 출력의 정수값이 0이니까 argv[1]-0x1234를 0으로 만들어줘야 한다.
argv[1]은 프로그램의 1번째 명령어 인자(command line argument)다.
그렇다면 argv[0]은? 문제에는 안 나오지만 해당 프로그램의 실행파일 경로가 들어 있다.
해당 파일 서술자를 가지고 입력을 받아 buf에 저장하고 buf과 "LETMEWIN\n"을 비교하여 일치하면 플래그를 출력하는 것이다.
자 그러면 어떤 값을 프로그램에 전달해 줘야 argv[1]-0x1234가 0이 될까?
0x1234는 16진수 hex값이니까 그걸 10진수로 고치면 되겠다.
보다시피 0x1234는 10진수로 4660이다.
따라서 argv[1]-0x1234의 값이 0이 될려면 4660-4660=0, argv[1]은 4660이 되어야 한다.
그런데 사실 본인은 그렇게 안 풀었다.
1fd@ubuntu:~$ for i in $(seq 1 10000); do echo Trying $i; ./fd $i; donecs 그냥 저렇게 for문을 이용하여 브포를 때려줬다.
1234567891011121314(...)learn about Linux file IOTrying 4656learn about Linux file IOTrying 4657learn about Linux file IOTrying 4658learn about Linux file IOTrying 4659learn about Linux file IOTrying 4660LETMEWINgood job :)mommy! I think I know what a file descriptor is!!cs 그럼 위처럼 결과가 나오다가 4660때 키보드 입력을 받기 위해서 멈춘다.
이때 LETMEWIN을 입력하면 Flag값이 나온다.
123456789101112Trying 4660LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Trying 4661LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Trying 4662LETMEWINgood job :)mommy! I think I know what a file descriptor is!!cs 어라? 그런데 4661때도 멈추네? 4662때도 멈춘다!
4660일 때 파일 서술자는 4660-0x1234=4660-4660=0이다.
4661일 때 파일 서술자는 4660-0x1234=4661-4660=1이다.
4662일 때 파일 서술자는 4660-0x1234=4662-4660=2이다.
0, 1, 2는 리눅스의 세 파일 서술자이다.
즉, 표준 출력을 뜻하는 0 말고 표준 입력과 표준 오류를 뜻하는 1, 2 역시 키보드의 입력을 받아올 수 있다는 것이다.
왜 그런 거지? 원래 1, 2일 때도 받아올 수 있는 것일까..?
구글에 pwnable.kr 1번 "4661", pwnable.kr 1번 "4662"로 검색해봤다. 역시 답 없을 땐 구글링이 최고다.
외국 Write-Up의 경우 4661로 푸신 분들도 꽤 있었다.
pwnable.kr 1번 "4662"로 검색한 결과 중 http://chaneyoon.tistory.com/179의 설명을 읽어 봤는데,
"File Descriptor 는 시스템 상의 파일을 가리키기 좋게 하기 위해서 시스템이 우리들에게 건네주는 숫자라고 할 수 있다."
출처: http://chaneyoon.tistory.com/179 [자만하지 말자]
라고 나왔다. '가리키기 좋은' 용도니까 1이나 2 역시 키보드 입력을 받아올 수 있는 것 같다.
123456789101112131415fd@ubuntu:~$ ./fd 4660LETMEWINgood job :)mommy! I think I know what a file descriptor is!!fd@ubuntu:~$ ./fd 4661LETMEWINgood job :)mommy! I think I know what a file descriptor is!!fd@ubuntu:~$ ./fd 4662LETMEWINlearn about Linux file IOfd@ubuntu:~$ ./fd 4662LETMEWINgood job :)mommy! I think I know what a file descriptor is!!cs 뭐튼 브포한 결과를 바탕으로 직접 실행해 봤더니 역시 제대로 나왔다.
그런데 가끔씩 인수를 4662로 했을 때 키보드 입력을 받은 뒤 에러가 나오는 경우도 있었다.
저건 또 왜 그런 거지? LETMEWIN도 제대로 입력했는데...
한번 4662로 10번 실행해보고 저런 에러가 흔한가 알아봐야겠다(점점 문제풀이가 산으로 가는 것 같은데...)
물론 일일히 실행해보기에는 내 인내심은 좀 딸리니, 이번에도 for문을 사용해 보자.
1for i in $(seq 1 10); do echo Try no.$i; ./fd 4662; donecs 요렇게 넣어보면 될 것이다!
...코드는 작동했지만, LETMEWIN을 자동으로 입력하는 것을 빼먹어서 일일히 손으로 10번을 쳐야 했다.1234567891011121314151617181920212223242526272829303132333435363738394041fd@ubuntu:~$ for i in $(seq 1 10); do echo Try no.$i; ./fd 4662; doneTry no.1LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Try no.2LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Try no.3LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Try no.4LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Try no.5LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Try no.6LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Try no.7LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Try no.8LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Try no.9LETMEWINgood job :)mommy! I think I know what a file descriptor is!!Try no.10LETMEWINgood job :)mommy! I think I know what a file descriptor is!!cs 게다가 알아보려고 했던 에러는 없었다.
이쯤 되면 그만 쓰려고 했는데, 에러를 다시 찾는 것은 포기하고 마지막으로 LETMEWIN을 입력하는 코드를 짜보자.
키보드로 입력하는 것처럼 화면에 출력하면 되지 않을까?
1for i in $(seq 1 1); do echo Try no.$i; ./fd 4662; echo LETMEWIN; donecs 혹시라도 실패할 수 있으니까 1번만 반복하도록 테스트 해봐야겠다.
이번에도 LETMEWIN을 10번 입력하는 일은 끔찍하니까... 저렇게 명령어 seq의 범위를 1~1로 해두면 된다.
12345fd@ubuntu:~$ for i in $(seq 1 1); do echo Try no.$i; ./fd 4662; echo LETMEWIN; doneTry no.1LETMEWINlearn about Linux file IOLETMEWINcs 엌... 실패다. 프로그램의 실행이 종료된 뒤에야 echo LETMEWIN명령이 실행된다.
그렇다면 어떻게 해야 할까... 일단 나는 아직 bash 쉘에 대한 지식도 부족하고 파이썬 쉘 스크립트 같은 것도 못 다룬다.
~라면서 현실 파악을 하고 있다가 갑자기 perl이 생각났다.
물론 perl을 자유자재로 다룰 수 있는 것은 아니지만 지금 내 앞에 노트북과 함께 있는 <해킹: 공격의 예술>이라는 책에 perl의 기본적인 사용방법이 나와 있기 때문에 그걸 응용하면 될 것 같다.
1234fd@ubuntu:~$ ./fd 4660 $ perl -e 'print "LETMEWIN\n"'LETMEWINgood job :)mommy! I think I know what a file descriptor is!!cs ~를 야심차게 시도하였으나 fail. 결국 손으로 LETMEWIN을 입력해야 Flag가 나온다.
다시한번 잘 생각해보니 책의 예제는 scanf()로 입력을 받는 반면 내가 perl script를 적용한 fd에서는 read()로 입력받기 때문인 것 같은데...
결국 실패했고 잘 모르겠지만 언젠가 이를 이해하고 할 수 있었으면 좋겠다 :) 또 안 됬던 이유도 궁금하다.
+) 내용추가(2017.12.27)
123fd@ubuntu:~$ (perl -e 'print "LETMEWIN\n"') | ./fd 4660good job :)mommy! I think I know what a file descriptor is!!cs 지금은 pwnable.kr의 3번 문제, bof를 풀고 왔는데 요렇게 파이프(|)를 이용하면 되더라!
12345fd@ubuntu:~$ (python -c 'print "LETMEWIN\n"') | ./fd 4660learn about Linux file IOfd@ubuntu:~$ (python -c 'print "LETMEWIN"') | ./fd 4660good job :)mommy! I think I know what a file descriptor is!!cs python을 사용할 때에는 저렇게 개행 문자(\n)을 제거하고 실행하면 잘 나온다!
여기서 마치면 재미없으니까 하나 썰을 더 풀어 보자면, read() 함수에 대해 알고자 구글링했을 때 추천검색어로 'read 함수 취약점'이 떴다.
1size_t read(int fd, void *buf, size_t count);cs read() 함수의 원형은 위와 같다.
여기서 fd는 파일 서술자 번호, buf는 읽은 데이터를 저장할 공간, count는 읽을 데이터의 크기다.
read() 함수를 사용할 때, 버퍼의 크기보다 count의 값을 더 크게 지정하면 BOF(버퍼오버플로우) 취약점이 발생한다고 한다.
또한 데이터를 read() 함수를 통해 여러 번 입력받을 때는 버퍼를 초기화시켜 줘야 한다.
read() 함수는 버퍼를 초기화하지 않고 입력받은 크기만큼 덮어쓰기 때문이다.
즉, 원래 10바이트의 데이터가 있던 초기화되지 않은 상태의 버퍼에 read() 함수로 7바이트의 데이터가 들어오면 이전의 3바이트는 그대로 남아 있게 된다는 것이다.
그런데 사실 scanf() 함수가 버퍼오버플로우에 더 취약하다.
scanf() 함수는 입력받을 버퍼의 크기를 지정해주지 않기 때문이다. 그래서 이를 개선하기 위해서 scanf_s() 함수를 사용하기도 한다.
또 gets() 함수 역시 입력받을 버퍼의 크기를 지정해주지 않는 버퍼오버플로우 취약점이 있어서 요즘은 fgets() 함수를 사용한다고 한다.
플래그는 mommy! I think I know what a file descriptor is!!이다.
위 스크린샷은 문제 풀고 나오는 알람을 놓쳐서 다시 인증한 것이다.
그냥 문제만 푸는 것보다 여러가지 쪽으로 탐구하는 게 더 의미 있고 공부가 된 것 같다 :)
'정보보안 > 포너블' 카테고리의 다른 글
pwnable.kr 3번 bof (0) 2017.12.27 pwnable.kr 2번 collision (0) 2017.12.26 해커스쿨 FTZ level4 (0) 2017.12.26 해커스쿨 FTZ level3 (0) 2017.12.26 해커스쿨 FTZ level2 (0) 2017.12.26