[Pwnable 1]

‍허진·2023년 4월 5일
0

hacking

목록 보기
7/7

#LOB 1 - gremlin

gremlin.c를 열어보았다.

크기가 256 바이트인 버퍼에 scanf로 입력받고 이를 출력한다. 굉장히 간단한 코드이다.

scanf로 입력할 때 shellcode를 입력하고 buffer의 크기를 넘어서 ret를 buffer의 주소로 덮으면 될 것 같다.

checksec으로 보호기법이 걸리지 않은 것을 확인했다.

main 함수를 disassemble한 결과이다. buffer의 크기가 256이므로 0x100의 공간을 확보하여 저장한다.

python 코드를 작성해보자. process에 gremlin을 넣고, shellcode를 미리 선언한다. 여기서 shellcode는 scanf를 우회하는 26바이트의 코드를 넣는다. 참고

그리고 scanf로 buffer에 입력 받는 것을 이용한다. shellcode를 넣고 앞뒤를 nop로 채운다. 그리고 ret를 buffer의 주소로 덮는다.

먼저 buffer의 주소를 알아보자. 이를 위해 pause()함수를 선언하였다.

그리고 lob1.py를 실행한다. 다른 터미널을 키고 gdb에 ./gremlin을 실행한 pid를 attach하고, 거기서 buffer의 주소를 확인한다. buffer의 주소는 $ebp-0x100에 해당한다.


buffer의 주소는 0xffffd238이었다! 이를 통해 python 코드를 완성했다.


위의 코드를 실행시켜보자.

쉘을 따내는 데 성공했다!

#LOB 2 - cobolt

cobolt.c를 열어보았다.

이전 문제와 비슷하다. 하지만 buffer의 크기가 16byte여서 scanf로 입력할 때 buffer 내에 쉘코드를 입력하는 것은 힘들 것 같다.

그렇다고 방법이 없는 것은 아니다. 어차피 ret를 특정한 주소로 덮는 것이기에, 굳이 buffer가 아니더라도 특정한 주소를 쉘코드로 덮고 해당 주소를 ret로 덮으면 된다! 따라서 ret 바로 다음 주소를 ret가 가리키도록 하고 해당 주소에 쉘코드를 입력하면 될 것 같다.

buffer의 주소를 얻어내자. 얻는 방법은 이전 문제와 같다.



0x90으로 16byte를 채우고 sfp까지 0x90으로 채웠다. 그리고 ret는 0xffffd340으로 채워져있다.(사실 이게 답이다..!) 그 다음 주소를 ret로 덮으면 되는데, 해당 주소가 0xffffd340이므로 이를 ret에 덮으면 된다.

이를 구현한 python 코드는 다음과 같다.

ret뒤에 쉘코드를 붙이고, ret가 쉘코드가 붙은 곳을 가리키도록 했다.

쉘을 따내는 데 성공했다!

#LOB 3 - goblin


c코드는 위와 같다. 이전 문제와 비슷하게 입력 받고 있는 buffer가 16byte라서 쉘코드를 직접 입력하기는 힘들 것 같다. 또한, 마지막에 memset으로 buffer를 0으로 초기화시키기도 한다!

따라서 이전 문제와 동일하게 ret 바로 다음 주소에 shellcode를 삽입하고, ret를 해당 주소로 덮을 계획이다.

ret 바로 다음 주소는 이전 방법과 동일하게 알아냈다.
0xffffd320

그 다음에 buffer가 16byte, dummy가 16byte이므로 32byte(dummy가 먼저 선언되었기에 더 높은 주소에 저장되어 있다. 우리는 buffer에 입력하므로 dummy의 공간까지 고려해야 한다.)에 sfp 4byte까지 해서 36byte(0x24)만큼을 0x90으로 덮고, 그 다음 ret를 덮었다.

exploit코드는 위와 같다.

쉘을 따내는 데 성공했다!

#LOB 9 - vampire


C코드를 까봤다. 이번 코드는 조금 복잡해보인다.. 하나씩 차근차근 살펴보자.

space, tmp를 전역에 선언하고, main 함수 내부에 buffer를 선언하고 있다. 그리고 tmp에는 buffer+44위치의 값을 8만큼 right shift하고 마지막 1byte만 남기고 있다.

이것이 의미하는 바가 무엇일까? 먼저 buffer+44 위치에 어떤 값이 있는지를 확인해야 한다. buffer의 크기가 40byte이기에 sfp 4byte까지해서 buffer+44에는 main 함수의 ret가 들어있다. 이 값을 8만큼 right shift하는 것은 ret 주소의 맨 마지막 1byte를 날리는 것이다.(ex. 0xffffd238 -> 0x00ffffd2) 그리고 해당 값에서 마지막 1byte만 남긴다. (ex. 0x00ffffd2 -> 0xd2) 즉, 원래 main 함수의 ret 주소 4byte 중에서 세번째 byte를 tmp에 저장하고 있는 것이다!!

그 다음 if문을 보면, space의 마지막 요소가 0인지 체크하고 0이면 함수를 종료하고 있다. 따라서 read 부분에서 space의 마지막 요소를 0이 아닌 요소로 바꿔줘야 한다.

그 다음에 strcpy 함수로 buffer에 space의 값을 복사하고 있다. strcpy는 복사할 크기를 입력받지 않기에, buffer의 크기에 상관없이 space의 크기만큼 복사하게 된다! 근데 여기서 첫번째 문제가 발생하게 되는데,, space를 그냥 0x1000byte만큼 채우고 이를 buffer에 복사하면 stack 아래의 접근 불가능한 영역까지 덮어서 프로그램이 오류를 출력하고 종료한다!

그렇다고 space의 0x1000번째 요소를 0으로 둘수도 없고,,,

여기서 생각해야 할 것이 strcpy가 문자열을 복사할 때 어디까지 복사해야 하는지 어떻게 아는지이다. 우리는 복사할 크기도 말해주지 않는데 strcpy 함수는 어떻게 아는 것일까?

strcpy 참고

strcpy는 '0x00', 즉 null 값이 나올 때까지 문자열을 복사한다. 따라서 복사되는 문자열의 끝에는 자동으로 null 값이 추가된다. 우리는 이 점을 활용하여 입력 중간에 null 값을 넣으면 space의 0x1000번째 요소는 바꾸면서 복사하는 길이는 줄일 수 있다!

그 다음에 buffer+45의 값과 tmp를 비교하고 있다. 뭔가 익숙하지 않은가? 우리가 실제로 tmp에 넣은 값이 buffer+45의 값과 동일하다!! (ret 값의 세번째 byte인데 little endian이므로) 즉, 우리가 새로 덮게 되는 main 함수의 ret 주소의 세번째 byte가 원래 byte와 동일하면 안된다.

gdb로 주솟값을 확인했다. buffer의 시작 주소로부터 44byte 이후의 값은 0xd3이다!

실제로 tmp에 d3이 저장된 것을 확인했다. 따라서 buffer에 복사할 때 shellcode가 0xffffd4xx이후의 주솟값에 저장되도록 space에 값을 저장해야 한다.

exploit 코드는 위와 같다. 먼저 buffer와 sfp를 다른 값으로 덮고 ret를 shellcode가 위치할 주소로 덮는다. 그리고 다시 다른 값으로 덮은 후, ret가 가리키는 주소에 shellcode가 들어갈 수 있도록 shellcode를 덧붙인다. 그리고 0x1000번째 요소가 0이 되지 않도록 다른 요소로 다시 덮는다.

buffer의 주소가 0xffffd310이었기에, 0x28+4 이후의 주소에 ret를 덮고, 추가로 0x100byte까지 덮게 한 후에 0xffffd410에 shellcode를 추가한다! (0xffffd400으로 하지 않은 이유는 strcpy가 인식해서 그 앞까지만 복사가 되기 때문이다.) 그리고 바로 뒤에 0x00을 넣어서 복사되는 문자열의 크기를 줄인다.


쉘 따내기 성공!

#LOB 12 - darknight


c코드는 다음과 같다. 메인의 buf에 입력을 받고 이를 problem_child의 buffer에 복사하는 구조이다.

그런데 problem_child에서 buffer의 크기는 60byte인데 strncpy로 복사하는 크기는 61byte다. ret를 덮으려면 sfp 4byte에 ret 4byte까지 해서 8byte를 더 쓸 수 있어야 되는데 1byte만 쓸 수 있는 거로 뭘 할 수 있다는 것일까??

함수 에필로그 참고
Frame Pointer Overflow, FPO 참고

구글링을 하다보니 새로운 공격 방법을 찾아냈다. 굳이 ret를 덮지 않고, sfp에 1byte만 덮어서 코드의 실행 흐름을 바꿀 수 있다!

처음에는 problem_child의 sfp를 1byte를 덮어서 problem_child 함수의 buffer에 저장된 쉘코드로 흐름을 옮기려 했다.

그런데 sfp에 저장된 값은 0xffffd3XX이고, buffer의 주소는 0xffffd2XX여서, 1byte를 덮는 것으로는 buffer의 쉘코드를 실행시킬 수 없었다.

그런데 굳이 실행 흐름을 더 낮은 주소로 옮길 필요는 없지 않은가? sfp에 0xffffd3XX가 저장되어 있고, main 함수의 buf가 0xffffd3XX를 일부 덮고 있으므로, 해당 부분으로 함수 흐름을 옮기고, 그 다음에 쉘코드가 저장된 곳의 주소를 입력해주면 된다. (쉘코드가 저장된 주소는 main 함수의 buf에 저장된 주소로 했다!)


회색 부분이 main 함수의 buf이다. 따라서 problem_child의 sfp를 0xffffd300으로 하고, 0xffffd304에 쉘코드가 저장된 주소인 0xffffd2b4를 저장하면 된다!

다음은 exploit하는 python 코드이다.


쉘을 따는 데 성공했다!

#LOB 16 - zombie_assassin


c코드를 까봤다. stack 부분은 9번 문제와 비슷하다. buffer + 44에서 24만큼 right shift 하고 있으므로 주소의 첫번째 byte를 남기는 것 같다.

실제로 확인해보니 stack에 ff가 저장되어 있었다.

즉, 이 문제에서 요구하는 것은 main 함수의 ret를 덮으로 때 0xffXXXXXX 형식의 주소는 안 되게 하려는 것 같다. 우리가 작성할 수 있는 buffer의 주소도 0xff~에 해당하는 데 과연 어떻게 쉘코드를 실행시킬 수 있을까?

이 문제를 풀기 위해선 12번 문제에서처럼 sfp를 조작해야 한다. 먼저 sfp를 조작하여 buffer-4의 주소로 ebp를 옮겨놓아야 한다.

그 다음에 ret를 어떤 값으로 덮어야 할지 감이 안 왔는데 구글링을 통해 답을 알아냈다.

fake ebp 참고

구글링을 통해 'leave-ret gadget'에 대해 알아냈다! 정확한 이해한 것인지는 모르겠지만, 함수를 종료할 때 나오는 leave와 ret 코드가 저장된 주소를 ret로 덮어 leave와 ret를 한번 더 실행하게 하는 것이다!!


disass main으로 leave 명령어가 저장된 주소는 0x080484de임을 알아냈다. 이 주소로 ret를 덮으면 된다!

그렇게 되면 leave와 ret이 한번 더 실행되는데, 이때의 과정은 먼저 leave를 통해 esp가 ebp와 같은 곳을 가리키게 된다.(buffer-4)

그리고 pop ebp로 ebp는 buffer-4가 가리키는 임의의 주소를 가리키게 되고, esp는 buffer의 시작 주소를 가리키게 된다. 그 다음 ret이 실행되어 buffer의 주소에 있는 값을 실행한다!

처음에는 exploit 코드를 다음과 같이 짰다. addr은 leave의 주소를, bf_addr은 buffer의 시작 주소를 가리킨다.

그런데 문제가 생겼다. 다시 제대로 알아보니 마지막 ret에서 buffer에 저장된 주소로 옮기는 것이었다 ㅠㅠ 따라서 payload에 앞에 buffer+4의 주소를 입력해놓고, buffer+4에 shellcode를 입력하면 된다!

exploit 코드이다.

쉘 따내기 성공!

#LOB - bonus_goldgoblin


c코드는 위와 같다. 쉘을 따는 함수가 sub함수로 따로 선언되어 있다. 따라서 a배열에 입력할 때 직접 쉘코드를 입력하지 않고 main함수의 ret를 key함수의 주소로 덮으면 될 것 같다!

key 함수의 주소를 알아보기 위해 위와 같은 코드를 작성했다.

실행해보니 key함수의 주소가 0x400537이었음을 확인했다!
(사실 elf.sybols['key']로 얻은 주소를 그대로 exploit코드에 사용하고 싶었지만 오류 때문에 직접 출력했다..)

위의 주소를 바탕으로 해서 exploit코드를 마저 작성해보자. a에 24byte를 입력할 수 있는데, 기존 a 8byte와 sfp 8byte(64bit이므로)까지 16byte를 채우고, ret 8byte를 key 함수의 주소로 덮으면 된다.


exploit 코드는 위와 같다.

쉘을 따는 데 성공했다!

#Problem - aslr_on

일단 먼저 c코드를 보자.

c코드는 단순해보인다. 하지만 이제부터는 aslr을 우회해야 한다..

aslr 우회 공격을 찾아보면서 몇가지를 공부하게 되었다.
RTL (Return-to-libc)
RTL chaining
GOT overwrite
ROP (Return Oriented Programming)
ROP in x64

대충 정리하자면, 먼저 RTL은 말 그대로 libc, 즉 공유 라이브러리에서 함수를 호출해 사용하는 공격 기법이고, RTL chaining은 여기서 한발 더 가서 공유 라이브러리의 함수들의 호출을 이어서 하는 방법이다.

함수의 PLT주소와 GOT주소에 대해서도 알게 되었다. plt는 함수가 호출될 때 함수의 주소를 찾기 위해 사용되는 테이블이다. 함수가 호출되면, 호출하는 코드는 해당 함수의 주소를 찾기 위해 plt에 있는 entry point로 jump한다. 이 entry point는 해당 함수의 주소를 찾기 위해 실제 함수가 위치한 주소를 검색하는 역할을 수행한다.

got는 함수의 주소를 포함하는 테이블이다. plt entry point는 got에 있는 함수의 주소를 호출한다. got는 함수의 주소를 저장하기 때문에, 해당 함수가 호출될 때마다 got에서 함수의 주소를 읽어와야 한다.

함수의 gadget은 함수 내에서 원하는 명령을 수행하는 코드 조각으로, 보통 ret(ret을 수행하는 명령어) 직전까지의 코드 조각을 의미한다. ROP 공격에서는 여러 개의 gadget들을 연결하여 실행 가능한 코드를 생성하게 된다.

그래서 이것들로 어떻게 exploit 코드를 짜야한다는 것인가?

참고 - ROP 공격

쉽게 정리하면 위와 같다. 차근차근 따라해보자.


먼저 read함수와 printf 함수의 plt, got 주소를 찾아냈다.


그 다음 read 함수와 system 함수 사이의 offset을 구했다.

bss 영역의 주소까지 구했다.

pop rdi;ret;과 pop rsi;ret;의 주소는 구했다.


이렇게 exploit 코드를 어느 정도 짰는데,, pop; ret;의 가젯을 어떻게 넣을지 막막해서 힘들어하는 와중에 동기한테 도움을 조금 받았다.. ROP가 아니라 메모리 릭이었던 것이다..!

c 코드를 다시 보자.

addr에 a 배열의 주소를 저장하고 있는데, gdb로 확인해보면 addr이 a 배열 바로 다음에 위치하고 있다.

a가 rbp - 0x70부터 rbp - 0x10까지 차지하고 있고, 바로 다음에 addr이 들어있어서 rbp-0x10에 a의 주소를 넣는 것을 main+28에서 확인할 수 있다.

그렇다면 메모리 릭을 어떻게 해야할까?
Memory leak 참고

printf 함수는 NULL값을 만날 때까지 출력하는데, 우리는 배열을 완전히 채워서 NULL값을 덮고 다음 주소에 저장된 값까지 출력할 것이다!

그렇게 출력된 주소를 받아서 8바이트로 만들어야 한다. 파이썬의 ljust함수를 써서 6바이트 주소 앞을 0x00으로 채운다.

그 다음에 read 함수가 한번 더 있기에 쉘코드를 넣어주고, ret를 printf의 메모리 릭에서 얻은 a의 주소로 덮으면 된다!


exploit 코드는 위와 같다.

쉘을 따내는 데 성공했다!

#Problem - final



C코드를 보자. 확실히 마지막 문제라 코드가 길지만 차분히 읽어보면 별 거 없다.

메인 함수 이전에 buf를 출력하는 함수, buf에 입력받는 함수, 어떤 행동을 할 지 메뉴를 출력하기만 하는 함수가 선언되어있다.

그리고 메인 함수에서는 반복문 안에서 계속 메뉴를 출력해주면서 사용자의 입력에 맞게 함수를 호출해주고 있다.

먼저 gdb로 메모리에 저장된 것을 확인해보자.


main함수의 disassemble 결과다.. 굉장히 긴데, 간단히 정리해보며 다음과 같다.

이 문제도 메모리 릭을 활용한 문제이다. buf 윗 주소에 buf의 주소를 저장하고 있는 변수가 없는데 어떻게 메모리 릭이라는 말인가? 사실 이건 print_buf 함수를 유심히 살펴보면 알 수 있다.

print_buf 함수에서는 index를 입력받아서 한 바이트씩 출력해주고 있는데, 15번째 이상의 index에 대해서는 접근을 불가능하게 하고 있다. 이러면 더 방법이 없어보이는데,,, 위로 가지 못한다면 아래로 가면 된다! index에 음수를 입력하는 것은 제한이 없기에 음수를 입력하여 store.ptr에 저장된 buf의 주소를 출력시킬 수 있다!!


확인를 위해 write_buf 함수를 이용해 buf를 'A'로 채웠다. 보면 0x7fffffffe110부터 0x41이 채워진 것을 볼 수 있으며, 0x30아래 주소에 이 주솟값이 저장된 것을 알 수 있다. 0x7fffffffe0e0에 store.ptr이 있고, buf의 주소를 저장하고 있는 것이다!


실제로 이렇게 store.ptr에 저장된 값을 하나씩 leak할 수 있었다!

원래는 recvuntil로 exploit 코드를 구성하려 했었는데 계속 실패하면서 시간만 버렸다,,ㅠㅠ

결국 중간중간 sleep하면서 메모리를 릭하고, 얻어낸 주소로 payload를 구성하여 buf에 작성하고, 마지막에 3을 보내 exit했다.


exploit 코드는 위와 같다.


쉘 따내기 성공!

profile
매일 공부하기 목표 👨‍💻 

0개의 댓글