Pwnablekr_Toddlers Bottle_passcode

ssh로 접속하면 소스코드와 실행파일이 있다.

C언어 파일에서 핵심 알고리즘을 정리하면 다음과 같다.

  • welcome 함수에서 scanf로 이름을 입력 받고, 환영 메시지를 띄운다.
  • login 함수에서 비밀번호를 두 개 입력 받고, 둘 다 맞을 시 플래그를 출력한다.

코드에 비밀번호가 노출되어 있다. 그대로 쓰면 될 것 같지만 비밀번호를 입력하면 Segmentation fault가 일어난다. 코드를 다시 보면 원인을 알 수 있다.

scanf 함수에 주소를 인자로 넣어야 하는데, 값을 넣고 있다. 정상적인 상황이라면 &passcode1, &passcode2가 되어야 한다. 코드 자체가 잘못되었으므로 암호를 입력하더라고 통과할 수 없다.

코드의 오류를 우회하여 작동시키기 위한 아이디어는 다음과 같다.

  • 이름을 입력 받는 scanf를 이용하여, passcode1과 2의 주소를 각각 passcode1과 2에 저장해 둔다. 이렇게 하면 코드를 정상적으로 작동시킬 수 있다. 단, 이 방법은 방어기법이 적용되지 않아 항상 같은 주소를 이용할 때 가능하다.
  • 이름을 입력 받는 scanf를 이용하여 셸을 딴다.

시도

첫 번째 방법을 이용해서 우회해 보자.  간단하게 디버거를 이용하여 메모리 값을 조작해 실험해보고, 성공한다면 이름을 입력하는 scanf를 이용하여 같은 작업을 적용해 본다.

첫 번째 비밀번호 입력 루틴이다. 원래는 scanf의 인자로 edx에 passcode1 변수의 주소를 넣어야 한다. 하지만 코드가 잘못되어 ebp-0x10에 저장된 값을 edx에 넣는다. 정상 상황이라면 ebp-0x10의 주소가 edx에 들어가야 한다. 

그렇다면 ebp-0x10ebp-0x10의 주소를 저장해두면 되겠다.

ebp의 값은 0xffffcc88이다. 따라서 ebp-0x10의 값인 0xffffcc78을 ebp-0x10 위치에 저장한다.

이와같이 설정하고 진행한 뒤, scanf에는 코드에서 본 첫 번째 암호인 338150을 입력한다.

두 번째 비밀번호 입력 루틴이다. 이전과 같은 맥락으로, ebp-0xcebp-0xc주소를 저장해 준다.  ebp-0xc의 값은 0xffffcc7c이다.

이와같이 설정하고 진행한 뒤, scanf에는 코드에서 본 두 번째 암호인 13371377을 입력한다.

성공 메시지가 뜬다. 

(하지만 로컬에서 실험해본 것이라 flag파일이 없어 플래그를 읽을 수는 없다. 참고로 서버에서 같은 방법을 이용하여 gdb를 이용하여 flag를 알아내려 했지만, permission denied로 막혔다. )

이제 name 입력의 scanf를 이용하여, login함수에서 ebp-0x10ebp-0xc가 될 위치에 각자의 주소를 저장하도록 해 보자.

이름을 받는 함수인 welcome함수에서의 스택 구조를 파악해 보자. welcome함수는 처음에 ebp를 백업해둔 후, sub esp, 0x88하여 스택을 0x88만큼 확보한다. 이 상태에서 ebp는 0xffffcc88이고, esp는 0xffffcc00이다. 

그리고 name을 입력받기 위한 주소로 ebp-0x70을 준다. 이는 0xffffcc18에 해당된다. 0xffffcc78 위치에 0xffffcc78를 저장하고, 0xffffcc7c 위치에 0xffffcc7c를 저장해야 한다.  그 점에 주의하여 다음과 같은 스택을 만든다. 0xAA 위치에는 0이 아닌 더미값을 넣으면 된다.

이 방식은 실패했다. scanf("%100s", name)이기 때문이다. 0xffffcc18부터 100개만큼 채워봤자 0xffffcc7c까지밖에 채울 수 없다. 0xffffcc78~0xffffcc7b는 바꿀 수 있지만, 0xffffcc7c~0xffffcc7f는 바꿀 수 없다. 

같은 이유로 scanf를 이용하여 셸을 바로 따는 방법도 어렵겠다.

풀이

위의 실패에서 한 가지 소득이 있다. passcode1과 2의 위치 둘 다를 바꿀 수는 없지만, passcode1의 위치인 0xffffcc78~0xffffcc7b는 변경 가능하다는 사실이다.

즉, passcode1의 scanf는 이용할 수 있다는 의미이다. scanf("%d", passcode1) 이므로, 원하는 주소에 4바이트 크기의 데이터를 넣을 수 있다는 의미이다.

passcode의 프로그램 헤더를 분석해본 결과, 코드가 있는 세그먼트는 읽기와 실행 권한밖에 없다는 것을 알 수 있다. 따라서 코드를 직접 수정할 수는 없겠다. 하지만 코드를 둘러보면, 동적 라이브러리를 이용한다는 것을 알 수 있다. 즉, GOT를 이용해볼 수 있다.

gdb의 주석을 통해 함수들이 동적 링킹되어 이용된다는 사실을 알 수 있다. 첫 번째 scanf 이후에 이용되는 함수의 GOT에 원하는 코드 주소인, system 함수의 인자를 넣고 호출하는 코드의 주소를 넣으면 호출할 수 있다.

첫 번째 scanf 바로 다음에 이용되는 함수인 fflush의 GOT를 이용해보자.

fflush@plt를 따라가면 GOT 주소에 저장된 주소를 참조하여 점프하는 루틴이 나온다. 즉, fflush의 GOT는 0x0804a004이다.

실행하기 원하는 코드, system함수의 인자를 넣고 호출하는 코드는 이 부분이다. GOT에 써 줘야 하는 주소는 0x080485e3이다.

결론은, scanf("%d", passcode1); 이 성공할 수 있도록 passcode1의 위치에 주소를 저장하는데, 그 주소는 fflush의 GOT가 되도록 한다.

그리고 scanf("%d", passcode1);의 입력값으로는 system함수를 호출하는 루틴의 주소를 입력하여 fflush를 호출했을 시 system함수 루틴이 호출되도록 한다. 주의할 점은 %d를 이용하면 입력값을 10진수로 해석한다는 점이다. 즉, 원하는 주소를 10진수로 변환하여 넣어야 한다.

0x080485e3는 10진수로 134514147이다. 파이썬을 이용하여 입력값을 넣었다.

flag는 Sorry mom.. I got confused about scanf usage :( 이다.


tags: writeup, pwnable, elf file, linux, c lang, memory corruption, dynamic library, procedure linkage table, global offsets table, remote code execution, x86asm