본문 바로가기

코딩 문제/해커스쿨

FTZ : level19

728x90

hint를 확인한다.

이번 문제는 단순히 gets함수로 문자를 입력받아 출력만 하는 함수이다.

실행해보겠다.

역시다.

그럼 이 C코드를 가지고 어떻게 level20의 Password를 얻을까?

 

일단 tmp폴더에 실행파일을 복사한 다음 GDB 디버거를 통해 살펴보겠다.

main+3을 보게되면 0x28을 할당하는 것이 보인다.

0x28은 10진수로 40이다.

그렇다면 메모리구조는

buf 20byte + dummy 20byte + SFP 4byte + RET 4byte가 된다.

 

생각할 수 있는 풀이방법은 buf 20byte와 dummy 20byte, SFP 4byte

총 44byte에 쉘코드와 setreuid의 코드를 넣고 RET에서 그 위치를 호출하여

쉘명령을 수행하는 것이다.

 

우선 쉘코드를 어떻게 얻게 되는지 한번 알아보자.

먼저 vi 에디터로 C코드를 작성한다.

그런다음 gcc 컴파일을 통해 실행파일을 만든다.

쉘이 잘 동작되는 것을 볼 수 있다.

그럼 main이 어떻게 동작하는지 확인해보겠다.

[ gcc 옵션 ]

-o : gcc 수행결과 만들어지는 실행파일의 이름을 지정하는 옵션

-g : gdb를 사용하기 위해 debugging 정보를 assembly code와 같이 생성하라는 옵션

-static : 공유라이브러리를 사용하지 않고 사용되는 공유라이브러리들을 모두 한 파일에 묶어 컴파일하는 옵션

( 정적 라이브러리를 우선하여 링킹 )

 

[ objdump 명령어 ]

오브젝트 파일의 정보를 출력해주는 도구

< 옵션 >

-d : 디스어셈블(코드섹션만)

-s : 일반적인 오브젝트 파일 덤프를 뜰 때 사용

-j : 내가 원하는 섹션만 출력하는 옵션

-h : 섹션 헤더만 출력하는 옵션

-d : 실행코드가 들어있는 부분을 디스어셈블한다.

 

[ 덤프 ]

기억장치의 내용을 출력하는 일

 

[ 섹션 ]

부분, 구역 등을 의미

 

[ 바이너리 ]

0과 1 두 숫자로만 이루어진 이진법을 의미한다.

 

[ 오브젝트 파일 ]

컴파일 과정에서 '컴파일러'가 생성한 바이너리코드 덩어리.

< 종류 >

1. 실행 가능한 오브젝트 파일

2. 공유 오브젝트 파일 -> 동적링크 가능한 오브젝트 파일

3. 재배치 가능한 오브젝트 파일 -> 나중에 링커를 통해 재배치 가능

 

[ 어셈블 / 디스어셈블 ]

어셈블리어 : C언어와 같은 하나의 언어

기계어 : CPU가 이해하는 언어

어셈블 : 어셈블리어를 기계어로 바꾸는 작업

디스어셈블 : 기계어를 어셈블리어로 바꾸는 작업

 

[ grep 명령어 ]

입력으로 전달된 파일의 내용에서 특정 문자열을 찾고자 할때 사용

< 정규표현식 메타문자 >

\< : 단어의 시작

\> : 단어의 끝

^ : 행의 시작

$ : 행의 끝

. : 하나의 문자와 대응

* : 임의 개수와 대응

< 옵션 >

-n : 문자열이 들어있는 라인과 문두에 라인번호 출력

-i : 문자열의 대소문자 구분 X

-l : 문자열을 포함하는 파일의 이름만 출력

-A NUM : 패턴매칭라인 이후의 라인을 NUM수만큼 출력

-B NUM : 패턴매칭라인 이전의 내용을 NUM수만큼 출력

-C NUM : 출력물 앞뒤 전후의 주어진 라인만큼 출력( 기본 2라인 )

 

위의 결과값을 보게되면 제일먼저

push %ebp는 main()함수의 base pointer을 PUSH 하는 것이다.

그리고 9byte만큼의 지역변수 공간을 생성한다.

movl $0x808ef88, 0xfffffff8(%ebp)는 /bin/sh를 ebp-8지점에 저장시킨다.

movl $0x0, 0xfffffffc(%ebp)는 NULL을 ebp-12지점에 저장시킨다.

 

그리고 밑에 push가 3개 보이는데 이 명령은 execve()함수들의 인자들이

역순으로 PUSH되는 것이다.

그 후 call명령으로 execve가 실행된다.

 

원래는 buffer overflow 공격 시점에서는 /bin/sh이 어느 지점에 저장되어 있다는 것을 찾기 어렵다.

그래서 아래 사진처럼 코드를 직접 작성해야 한다.

test.c를 실행파일로 만들어서 실행해보겠다.

잘 되니 objdump로 기계어 코드를 보겠다.

그러나 여기서 문제가 발견된다.

원래 C언어에서는 char c="\x90"과 같은 형태로 값을 넣어주면 컴파일러는

16진수 90으로 인식하여 1byte 데이터로 저장한다.

push 0x0과 같은 어셈블리어 코드는 기계어로 6a 00 이다.

이것을 문자열 형태로 전달하려면 char a[] = "\x6a\x00"과 같이 해야하는데

문자열에서는 0의 값을 만나면 그것을 문자열의 끝으로 인식하게 된다.

따라서 0x00 뒤에 어떤 값이 있더라도 그 이후는 무시해버린다.

Ekfktj 0x00인 기계어 코드가 생기지 않게 만들어 줘야한다.

이렇게 코드를 바꾼뒤 다시 컴파일하여 실행해보겠다.

잘 되니 기계어코드를 확인하겠다.

우리가 짠 코드는 xor %eax, %eax 부터 int $0x80까지인데

그 사이에는 00이 없다는 것을 볼 수 있다.

 

그럼 우리가 짠 코드 구간의 기계어들만 뽑아보자.

이 기계어들이 그동안 사용한 25byte의 쉘코드이다.

 

자 이제 쉘코드가 만들어 지는 과정은 알았으니 setreuid()가 추가된 쉘코드를 만들어 보자.

setreuid()가 추가된 코드를 만들었으면 컴파일을 한 후 objdump명령어로 기계어들을 확인한다.

먼저 setreuid 함수를 살펴본다.

setreuid 함수에서 알아야 할 구간은 아래 그림이다.

다음으로 geteuid 함수를 살펴본다.

geteuid 함수에서 알아야 할 구간은 아래 그림이다.

마지막으로 execve 함수를 알아본다.

execve 함수에서 알아야 할 구간은 아래 그림이다.

3개의 함수에서 필수적인 구간들을 알았으면 코드를 작성한다.

순서는 geteuid(), setreuid(), execve() 순이다.

컴파일 한후 실행하여 잘 동작하는지 확인한다.

잘되므로 objdump명령어로 기계어를 확인한다.

00이 있으면 안되므로 코드를 수정한다.

다시한번 컴파일 후 제대로 작동되는지 확인한다.

다시 한번 기계어를 확인한다.

작성한 코드는 xor %eax, %eax 부터 int $0x80까지이므로 이 구간의 기계어들이

setreuid를 포함한 쉘코드이다.

 

그럼 이제 이 코드들을 환경변수에 등록하자.

< 환경변수에 등록시 NOP코드를 넣는 이유 : 주소공간에 의미없는 값을 넣어

공격의 성공률을 높이기 위해서 >

등록한 후 이 쉘코드가 위치한 주소를 확인하는 코드를 짜서 확인한다.

[ getenv ]

함수원형 : getenv( const char *name )

 

환경변수는 Key=Value 형태로 저장되며 getenv()의 인자로 들어가는 name은 Key가 된다.

만약 일치하는 name을 가지는 환경변수가 있다면 Value를 return하고, 없다면 NULL을 return한다.

이 쉘코드가 위치한 주소는 0xbffffbd6이다.

그럼 이제 level19폴더로 넘어가서 실행해보자.

처음에 메모리 구조가 buf 20byte + dummy 20byte + SFP 4byte + RET 4byte라고 했으니

앞의 44byte는 NOP코드를 넣고 RET 부분에 쉘코드가 위치한 주소를 넣으면 되겠다.

 

'코딩 문제 > 해커스쿨' 카테고리의 다른 글

FTZ : level20  (0) 2019.07.25
FTZ : level18  (0) 2019.07.20
FTZ : level17  (0) 2019.07.19
FTZ : level16  (0) 2019.07.14
FTZ : level15  (0) 2019.07.13