2021. 2. 23. 04:26ㆍFTZ
[ Level 18 ]
level18 로그인
ID : level18
PW : why did you do it
힌트에 나온 코드를 보면, shellout이라는 함수에서 /bin/sh를 실행하는 것으로 보아,
check변수의 값을 0xdeadbeef로 만들어주어 shellout함수를 실행시키도록 해야 하는 것 같다.
shellout함수를 실행시키기 전 코드를 분석한다.
변수 string, check, x, count, fds를 선언하고,
※ fd_set 구조체 File Descriptor를 지정하는 구조체이다. 여러개의 fd(file descriptor)를 select함수에서 읽기/쓰기/오류에 대한 event발생 여부 체크 모록을 관리 (간단하게 배열로 여기면 편하다.) |
fflush로 출력 버퍼를 비운 다음,
※ fflush함수 fflush 함수가 버퍼를 비운다는 뜻은 버퍼에 남아있는 데이터를 완전히 지운다는 뜻이 아닌 버퍼에 남아있는 데이터를 출력하고자 하는 목적지로 전송한다는 뜻이다. C언어로 프로그래밍을 하다 보면 입출력 버퍼에 데이터가 남게 되어 정상적인 입출력을 하지 못하는 경우에 사용되며, 이를 해결하기 위해서는 데이터가 남아있는 버퍼를 비워줘야 한다. 이때 사용하는 함수가 fflush 함수이다. - 헤더파일 stdio.h - 함수 원형 int fflush(FILE *stream); 입력 스트림(stdin) 입력 버퍼 안에 존재하는 데이터를 비우는 즉시 삭제한다. 출력 스트림(stdout) |
while(1)으로 계속해서 반복문을 실행하도록 하여,
만약 count가 100 이상일 경우 "what are you trying to do?"를 출력하도록 하고,
만약 check가 0xdeadbeef일 경우 shellout함수를 호출하도록 하였다.
check가 0xdeadbeef가 아닌 경우에는
FD_ZERO(&fds); 로 fds를 초기화해준다.
※ FD_ZERO함수 fd_set 구조체를 선언만 하고 안에 배열값을 찍어보면 쓰레기 값이 있을 수 있다. 따라서 FD_ZERO함수를 사용하여 초기화해준다. - 함수 원형 void FD_ZERO(fd_set *set); 파라미터 set : 초기화 할 fd_set RETURN 없음. |
FD_SET(STDIN_FILENO, &fds); fds를 표준 입력으로 설정한다.
※ 파일 디스크립터 (File Descriptor) - 시스템으로부터 할당 받은 파일을 대표하는 0이 아닌 정수값 - 프로세스에서 열린 파일의 목록을 관리하는 테이블의 인덱스 |
|||
파일 디스크립터 | 목적 | POSIX이름 | stdio 스트림 |
0 | 표준 입력 | STDIN_FILENO | stdin |
1 | 표준 출력 | STDOUT_FILENO | stdout |
2 | 표준 에러 | STDERR_FILENO | stderr |
※ FD_SET함수 fd_set에 fd를 추가하는 함수 fd_set변수의 fd번째의 비트를 1로 바꾼다. - 함수 원형 void FD_SET(int fd, fd_set *set); 파라미터 fd : set에 추가할 file descriptor set : fd를 추가할 fd_set read용/write용/예외용에 대한 set이 있으며, 각각의 set에 fd를 추각할 수 있다. RETURN 없음. |
select(FD_SETSIZE, &fds, NULL, NULL, NULL)의 리턴 값이 1 이상이고,
(리눅스에서 FD_SETSIZE는 대체적으로 1024이다.)
FD_ISSET(fileno(stdin), &fds))의 리턴 값이 0이 아닌 경우
※ select 함수 select함수는 socket / pipe 등에서 동시에 여러개의 I/O를 대기할 경우 특정한 fd에 blocking되지 않고 I/O를 할 수 있는 상태인 지를 모니터링하는 함수이다. - 헤더파일 - 함수 원형 int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 파라미터 nfds : 감시할 파일 디스크립터의 갯수 FD_SET함수로 설정한 fd값들 중에서 가장 큰 fd값 + 1을 설정해야 한다. readfds : '읽기'가 가능한지 감시 ① FD_SET(fd, readfds)로 설정한 fd들이 데이터를 읽을 수 있는 상태일 때까지 대기하게 하고, select함수가 return된 후에는 readfds에 읽을 수 있는 상태에 있는 fd를 제외하고 모두 clear됩니다. ② FD_ISSET(fd, readfds)으로 읽을 수 있는 상태에 있는 fd인지를 확인하여 0이 아닌값이 return되면 읽을 수 있는 상태에 있는 fd입니다. writefds : '쓰기'가 가능한지 감시 ① FD_SET(fd, writefds)로 설정한 fd들이 데이터를 쓸 수 있는 상태일 때까지 대기하게 하고, select(2) 함수가 return된 후에는 writefds에 쓸 수 있는 상태에 있는 fd를 제외하고 모두 clear됩니다. ② FD_ISSET(fd, writefds)으로 쓸 수 있는 상태에 있는 fd인지를 확인하여 0이 아닌값이 return되면 쓸 수 있는 상태에 있는 fd입니다. exceptfds : 예외가 발생했거나 대역을 넘어서는 데이터(소켓)가 존재하는지 감시 ① FD_SET(fd, exceptfds)로 설정한 fd들이 오류 상태일 때까지 대기하게 하고, select(2) 함수가 return된 후에는 오류상태가 아닌 fd들은 exceptfds에서 모두 clear됩니다. ② FD_ISSET(fd, exceptfds)으로 오류 상태에 있는 fd인지를 확인하여 0이 아닌값이 return되면 오류 상태인 fd입니다. timeout : ① readfds, writefds, exceptfds 들이 timeout 시간동안 모두 상태변화가 없으면 select함수를 종료시킵니다. ② timeout이 NULL이면 timeout이 없이 계속 대기합니다. struct timeval은 sys/time.h에 선언되어 있으며, 마이크로초 단위의 timeout을 설정을 지원합니다. RETURN 0보다 큼 : readfds, writefds, exceptfds에 대한 처리가능 상태인 fd의 전체 건수를 return 합니다. 0 : timeout이 발생하였으며, 처리 가능 상태인 fd가 하나도 없습니다. -1 : 오류가 발생하였으며, 상세한 오류 내용은 errno에 저장됩니다. 오류 EBADF : 설정된 fd 중에서 유효하지 않은 fd가 있음. (한개라도 있으면) EINTR : signal이 catch되어 select()가 중단됨 EINVAL : nfds 가 -값이거나 timeout의 tv_sec, tv_usec값이 잘못 설정됨. ENOMEM : 처리를 위한 내부 메모리 할당이 실패함. |
※ FD_ISSET 함수 set에 fd가 포함되어 있는 지를 확인하는 함수 select함수가 호출된 후에는 처리가 가능한 fd만 남게되므로 읽기 또는 쓰기 준비가 되어 있는 지 여부를 알 수 있다. - 함수 원형 void FD_ISSET(int fd, fd_set *set); 파라미터 fd : set에서 삭제할 file descriptor set : 읽기/쓰기/오류에 대한 모니터링을 할 fd 목록을 관리하는 구조체 RETURN 0 : fd가 set에 설정되지 않음. select 호출 후 FD_ISSET을 호출한 경우이며, 처리할 수 있는 상태가 아니다. 0이 아닌 경우 : fd가 set에 설정되어 있음. select 호출 후 FD_ISSET을 호출한 경우이면 처리할 수 있는 상태이다. |
※ fileno함수 파일 포인터를 파일 디스크립터로 바꿔주는 함수 - 헤더파일 stdio.h - 함수 원형 int fileno(FILE* stream); |
즉, select함수에서 nfds와 readfds를 제외한 모든 파라미터가 NULL이므로,
fd들이 데이터를 읽을 수 있는 상태일 때까지 timeout 없이 계속 대기하도록 하였다.
이전에 FD_SET에서 STDIN_FILENO으로 설정해주었기 때문에 select함수의 리턴 값은 1 이상일 것이다.
이후 FD_ISSET함수에서 fileno(stdin)을 fd에 넣어주며 fd가 표준 입력인지 확인한다.
FD_SET에서 표준 입력으로 설정하였기 때문에 0이 아닌 값을 리턴하게 될 것이다.
이후에 read함수에서 입력을 받게 된다.
※ read함수 파일 데이터를 입력받는 함수 - 함수 원형 ssize_t read(int fd, void* buf, size_t nbytes); 파라미터 fd : 입력받을 File Descriptor를 지정 buf : 입력받은 값을 저장할 공간 count : 입력받을 값의 크기 RETURN 성공 : 수신한 바이트 수, 실패 : -1 |
read함수에서 fd는 표준 입력, buf는 x로, 입력받을 크기는 1byte로 설정했다.
( fileno(stdin)은 0이다. (파일 디스크립터에서 0은 표준 입력이다.) )
switch문을 통해 입력받은 x가 \r(줄의 처음으로 이동)이거나 \n(개행)인 경우 \a(경고음)을 출력하고,
x가 0x08(backspace)인 경우 count--와 함께 \b(backspace)를 두 번 출력하고,
x가 \r, \n, 0x08이 모두 아닌 경우에는 string[count]에 x를 대입하고, count++을 시켜준다.
check에 0xdeadbeef를 넣기 위해서는 우선 gdb로 attackme를 실행한다.
0xdeadbeef와 비교하는 것으로 보아 ebp-104는 check를 가리킨다는 것을 알 수 있고,
위 사진에서는 switch문과 같이 무언가를 비교하다가 0x8을 비교하는 것을 볼 수 있다.
C언어 코드에서 0x8과 비교하는 것은 x밖에 없기 때문에 ebp-252는 x를 가리키고 있다는 것을 알 수 있다.
순서를 보면 0xa, 0xa, 0x8, 0xd를 차례로 비교를 하는데,
이때 알 수 있는 것은 0xd와 비교하는 것은 default라는 것을 알 수 있다.
C언어 코드에 있는 default에는 string변수가 쓰인다.
따라서 어셈블리어에서도 default를 따라가면 string의 주소도 알 수 있을 것이다.
default에 들어가면 jmp를 통해 <main+499>로 이동한 것을 볼 수 있다.
check변수가 ebp-104인데 stack에서는 string이 먼저 push 되었기 때문에
string은 check보다 ebp에 가까워야 한다. 또한 string은 100byte만큼의 공간을 만들었기 때문에
ebp-100이 string인 것을 알 수 있다.
( check와 string 사이의 dummy공간이 없다는 것도 확인할 수 있다. )
fds | 128byte |
count | 4byte |
x | 4byte |
check | 4byte |
string | 100byte |
EBP | 4byte |
RET | 4byte |
프로그램을 실행하여 x를 입력받을 때,
0x08을 입력하면 switch문을 통해 count가 0에서 -1로 내려갈 것이다.
만약 string[0]의 주소가 0xFFFFFF9C라면 string[-1]의 주소는 0xFFFFFF9B가 될 것이다.
하지만 string과 check 사이의 dummy 공간이 없기 때문에 string[-1]은 check가 될 것이다.
그렇다면 x에 0x08을 4번 입력하여 count를 -4로 만들어 준다음, 0xdeadbeef를 넣어주면,
check에 0xdeadbeef가 들어가게 될 것이다.
(python -c 'print "\x08"*4 + "\xef\xbe\xad\xde"'; cat) | ./attackme를 입력하여 attackme를 실행시켜준 후,
my-pass를 입력해보면,
level19의 Password를 얻을 수 있다!
[ Level 18 Clear ]
'FTZ' 카테고리의 다른 글
[FTZ] Level 20 (0) | 2021.02.25 |
---|---|
[FTZ] Level 19 (0) | 2021.02.23 |
[FTZ] Level 17 (0) | 2021.02.15 |
[FTZ] Level 16 (0) | 2021.02.15 |
[FTZ] Level 15 (0) | 2021.02.15 |