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 |