본문 바로가기

3. Pwnable (포너블)/2) 개념 정리

[2021.09.25] 해커스쿨 F.T.Z Level11~15 풀이

Level11

$프로그램이름 인자값 명령을 주면 실행 동안 잠깐 uid가 3092로 바뀌고 인자값을 str 배열에 복사해 그 문자열을 출력해 준다. 이때 strcpy 함수가 복사하는 값의 길이를 검증하지 않음을 이용해 버퍼 오버플로우 공격을 일으킬 수 있다. 이를 위해 우리가 알아야 할 것은,

  1. str 버퍼와 ret 시작 주소값의 거리차
  2. ret 시작 주소에 넣어줄 쉘코드

1. $cp attackme ./tmp 명령으로 파일을 다른 디렉토리로 복사해주고 gdb로 attackme 파일을 열어 main을 덤프하면 아래와 같다.

눈여겨 봐야 할 곳은 strcpy 함수를 호출하기 전, str 버퍼를 할당시키는 <main+41> 부분이다. 소스코드에서 설정한 str의 버퍼는 256byte인데 ebp-264의 주소부터 입력받는다는 것을 보아 8byte의 더미가 포함되어 있는 것을 알 수 있다. 메모리 구조는 아래와 같으며 총 272byte가 되겠다.

str[256] + dummy[8] + sfp[4] + ret[4]

2. 아래는 쉘을 실행시키는 25byte의 코드이다.

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80

ret을 제외한 268byte에서 쉘코드의 길이인 25byte를 뺴면 243byte가 된다. 이제 제외시켰던 ret 주소를 구해보자. 일단 strcpy를 하고 난 후인 <main+53>에 break를 걸고 dummy[243]+쉘코드[25]+임의의리턴[4] 문자열을 출력해보자.

(gdb) b *main+53
(gdb) r `python -c 'print "\x90"*243+"A"*25+"B"*4'`

이후 x/268x $esp 명령으로 메모리구조를 보면 메모리 어디엔가 0x90909090으로 가득 찬 화면이 뜬다. 이때 0x90909090이 시작하는 주소를 확인한 후 gdb를 나가고 level11 디렉토리로 이동하여 attackme 프로그램을 실행해 버퍼 오버플로우를 일으키면 level12 uid를 얻을 수 있다. 이후 my-pass 명령으로 패스워드를 확인할 수 있다.

[level11@ftz level11]$ ./attackme  r `python -c 'print "\x90"*243+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"+"당신의 RET주소를 리틀엔디안 방식으로 작성"'`

level12의 비밀번호는 it is like this입니다.


level12

이번 소스코드는 level11과 다르게 argv[1]은 받지 않는다. 대신, 문자열의 길이를 제한하지 않는 gets 함수로 입력을 받기 때문에 이를 이용해 버퍼 오버플로우를 일으킬 수 있다. 우리는 gets 함수에서 입력을 받을 때 입력받는 주소와 ret 주소의 거리를 알아내고 ret 주소에 쉘을 실행시키는 코드를 작성할 것이다.

level11과 똑같이 str[256]+dummy[8]+sfp[4]+ret[4]의 구조를 갖고 있다. 그렇다면 268byte만큼 더미를 채우고 그 뒤에 실행시킬 주소를 입력하면 되겠지만 ftz 서버에는 버퍼 오버플로우 공격을 막기 위해 aslr 기법을 사용했기 때문에 우리는 환경변수에 쉘코드를 등록할 것이다. 

NOP*243과 쉘코드로 이루어진 gets라는 이름의 환경 변수를 만들어주었다.

환경변수의 주소를 반환하는 소스코드를 작성하고 컴파일해 실행하면 아래와 같이 리턴 주소값을 확인할 수 있다.

다시 tmp 디렉토리를 빠져나와 아래와 같이 명령하면 level13의 uid를 획득하는 데 성공한다.

level13의 비밀번호는 have no clue입니다.


level13

argv[1]의 내용을 buf로 복사하며, i가 0x1234567이 아닐 때 버퍼 오버플로우가 일어남을 감지해 종료시킨다. 따라서 i값을 변화시키지 않고 쉘코드를 삽입해야 한다.

  • <main+54>를 보면 buf와 ebp 사이의 거리가 1048임을 알 수 있다. buf와 ret의 거리는 4byte를 더한 1052byte일 것이다.
  • <main+69>를 보면 i와 ebp 사이의 거리가 12임을 알 수 있다. i와 ret의 거리는 4byte를 더한 16byte일 것이다.

따라서 buf와 i의 거리는 1036byte이고, i의 끝에서 ret의 거리는 16byte에서 long형 4byte를 뺀 12byte일 것이다. 이제 SHELL이라는 이름의 환경변수를 만들어주고 쉘코드의 주소를 아래와 같이 알아내었다.

공격코드의 구조는 다음과 같다.

buf와 i의 거리 0x1234567 변수 끝과 ret의 거리 쉘코드의 주소

level14의 비밀번호는 what that nigga want?입니다.


level14

fgets 함수로 buf[20]에 45byte만큼 입력을 받고 check가 0xdeadbeef와 같다면 uid를 올려주고 쉘을 실행시킨다.

우리는 fgets 함수에 입력할 때 buf[20] 뒤 어딘가에 있을 check에 0xdeadbeef를 넣어주어야 한다.

  • <main+17>에서 ebp-56이 buf의 주소일 것임을 유추할 수 있다.
  • <main+29>에서 ebp-16에 들어있는 값과 0xdeadbeef를 비교한다. ebp-16에 들어있는 값이 check일 것.

따라서 buf의 첫 주소와 check의 첫 주소 사이의 거리는 40byte이다. 그렇다면 입력을 할 때 40byte만큼은 NOP을 채워주고 그 뒤에 0xdeadbeef를 넣어 익스플로잇 코드를 작성할 수 있다.

level15의 비밀번호는 guess what입니다.


level15

level14와 코드가 유사하나 check 변수가 포인터로 변경되었다. 포인터 변수는 변수 값이 아닌 메모리 주소를 저장하는 변수이다. 따라서 *check에 들어가는 주소의 메모리 주소에 0xdeadbeef를 넣어주어야 한다.

buf의 시작 주소는 [ebp-56]이고 *check의 시작 주소는 [ebp-16]이므로 buf와 check 사이의 거리는 40byte이다. 이제 *check에 들어가는 값 0xdeadbeef의 주소를 알아보자. <main+32>의 메모리 주소인 0x80484b0에 들어있는 값을 확인해보고 메모리주소를 1씩 증가시켜보면 0xdeadbeef가 0x80484b2라는 주소에 들어있는 것을 볼 수 있다.

이제 tmp 디렉토리를 빠져나와 공격코드를 작성해주면 된다.

level16의 비밀번호는 about to cause mass입니다.