본문 바로가기

Cryptography/Cryptography

[Dreamhack] Cryptography - pwntools 기초

pwntools 프로세스 통신 기능

일반적으로 프로세스 간 통신을 수행하려면 통신하려는 프로세스 사이의

1. 연결을 맺고,

2. 데이터를 주고 받으며 통신하고,

3. 연결을 종료

하는 과정을 거친다.

 

연결을 맺는 함수

통신을 위한 첫 번째 단계로 연결을 맺어야한다. 이 연결에도 몇 가지 종류가 존재한다.

연결을 맺는 함수가 성공적으로 실행되면 pwnlib.tubes 클래스를 반환함

 

process()

  • 로컬에 위치한 프로그램을 실행하여 통신할 때 사용
  • 인자로 실행하여 통신할 프로그램의 경로를 전달하면, 전달된 프로그램을 실행한 뒤 프로그램과 연결을 맺어준다. → process(통신할 프로그램의 경로)
from pwn import *
p = process("./example")

 

process에서 성공적으로 연결이 맺어지면 해당 함수는 데이터를 송수신하는데 사용될 pwnlib.tubes 클래스를 반환하고 해당 클래스는 p에 저장된다.


from pwn import *
p = process(["./example", "AAAA"], env={"LD_PRELOAD":"./libc.so.6"})
./example
실행할 파일
"AAAA"
명령줄 인자 (즉, argv[1] 값으로 전달됨)
env={"LD_PRELOAD": "./libc.so.6"}
환경 변수 설정. LD_PRELOAD는 특정 .so (공유 라이브러리)를 강제로 미리 로드하게 함

이 코드는 ./example 프로그램을 다음과 같은 조건으로 실행:

  • 인자 AAAA를 전달해서 argv[1] = "AAAA"가 됨
  • 환경 변수 LD_PRELOAD=./libc.so.6를 설정해서, 원래 시스템 라이브러리가 아닌 내가 만든 가짜 ./libc.so.6를 먼저 로드하게 만듦

 

remote()

  • 호스트의 domain / IP 주소 / 포트번호를 인자로 받아 원격 서버에 통신할 때 사용
  • process에서 성공적으로 연결이 맺어지면 pwnlib.tubes 클래스를 반환한다.
from pwn import *
r = remote("example.com", 1337)

 

example.com 호스트의 1337번 포트에 연결

  • 기본적으로 TCP 연결을 맺지만, TCP 대신 UDP 연결을 맺고 싶다면 typ 인자에 ‘udp’를 전달
r = remote("example.com", 1337, typ='udp')

 

 

ssh()

  • SSH 서버에 접속하여 통신하기 위해서 사용
  • SSH = Secure Shell, 원격 호스트에 접속하기 위해 사용되는 보안 프로토콜
s = ssh("dreamhack", "127.0.0.1", port=22, password="dreamhack")

 

127.0.0.1 호스트의 22번 포트에 열린 ssh 서버에 dreamhack이라는 사용자 이름와 dreamhack이라는 비밀번호로 로그인을 하여 접속

 

 

데이터 송수신 함수

recv()

  • 데이터를 수신하기 위해 사용
  • 수신한 데이터를 bytes 클래스로 반환
from pwn import *
p = process('./example')

data = p.recv(1024)
data = p.recvline() 
data = p.recvn(5)
data = p.recvuntil(b'hello') 
data = p.recvall()  
p.recv(1024)
p가 출력하는 데이터를 최대 1024바이트까지 받아서 저장
p.recvline()
p가 출력하는 데이터를 개행문자 만날 때까지 받아서 저장
p.recvn(5)
p가 출력하는 데이터를 5바이트만 받아서 저장
p.recvuntil(b’hello’)
p가 b’hello’를 출력할 때까지 데이터를 수신해 저장
p.recvall()
p가 출력하는 데이터를 프로세스가 종료될 때까지 받아서 저장

 

recv(n) vs. recvn(n)

recv(n) : 최대 n바이트를 받는 함수. 그만큼의 데이터를 받지 못해도 당장 받을 수 있는 만큼 수신한 뒤 함수를 종료함.

recvn(n) : 무조건 n바이트를 받는 함수. n바이트가 채워지지 않으면 채워질 때까지 수신을 계속 기다림.

 

 

send()

  • 데이터를 전송하기 위해 사용
  • 데이터를 bytes 클래스 인자로 받아 전송
from pwn import*
p = process('./example')

p.send(b'A') 
p.sendline(b'A')  
p.sendafter(b'hello', b'A')
p.sendlineafter(b'hello', b'A') 
send(b'A')
# ./example에 b'A'를 입력
sendline(b'A')
# ./example에 b'A' + b'\n'을 입력
sendafter(b'hello', b'A')
# ./test가 b'hello'를 출력하면, b'A'를 입력
sendlineafter(b'hello', b'A')
# ./test가 b'hello'를 출력하면, b'A' + b'\n'을 입력

 

sendafter() vs. sendlineafter()

→ 실제 인자로 전달된 내용들이 나올 때까지 수신한 뒤 데이터를 전송

sendafter() , sendlineafter() 를 실행한 뒤에 데이터를 수신할 경우 sendafter() , sendlineafter()에서 수신한 데이터 이후의 데이터부터 수신

 

 

interactive()

  • 터미널에서 사용자가 실시간으로 데이터를 수신하고 전송할 수 있게 하는 함수
  • 함수 호출시 터미널을 통해 프로세스에 입력 값을 전달할 수 있게 되고 프로세스의 출력도 실시간으로 터미널에 표시됨.
  • 터미널에서 직접 프로그램을 실행시켜 조작하는 것과 비슷한 효과
from pwn import*
p = process('./example')
p.interactive()

 

연결을 종료하는 함수

연결된 두 프로세스 중 어느 한 프로세스가 연결을 종료한다면 해당 연결 종료

 

close()

  • pwnlib.tubes 클래스에 구현된 함수
from pwn import *
p = process('./example')
p.close() # 실행되는 순간에 연결이 유지되고 있는 상태여야 함

 

로그 출력

context.log_level을 이용새 로그 출력의 상세도를 설정할 수 있다.

from pwn import *
context.log_level = 'debug'

p = process("./example")
p.recvall()
crititcal
치명적인 오류 외에 출력하지 않음
error
에러 메시지만 출력
warning / warn
경고 메세지 출력
info
일반적인 상태 메시지 출력 → 기본값
debug
송수신 데이터, 내부 변수 등 상세히 출력

 

pwntools 편리한 기능

Packing & Unpacking

데이터를 하나의 형태로 모아 포장하거나, 다시 분해하는 것

시스템 해킹에서는 정수 값을 bytes 클래스로 변환하거나 그 반대의 행위

리틀 엔디언으로 동작한다.

p8(), p16(), p32(), p64()
숫자를 bytes 클래스로 패킹하는 함수
u8(), u16(), u32(), u64()
bytes 클래스를 숫자로 언패킹하는 함수

→ 함수명 뒤에 붙는 숫자는 bytes 클래스의 비트 수

from pwn import *

s8 = 0x41
s16 = 0x4142
s32 = 0x41424344
s64 = 0x4142434445464748

print(p8(s8))
print(p16(s16))
print(p32(s32))
print(p64(s64)) -> Packing

s8 = b"A"
s16 = b"AB"
s32 = b"ABCD"
s64 = b"ABCDEFGH"

print(hex(u8(p8)))
print(hex(u16(s16)))
print(hex(u32(s32)))
print(hex(u64(s64))) -> Unpacking

-----------------------------------------------------------
b'A'
b'BA'
b'DCBA'
b'HGFEDCBA'
0x41
0x4241
0x44434241
0x4847464544434241

 

GDB

 

attach

from pwn import *
p = process("./example")
gdb.attach(p)
sleep(1) -> gdb가 성공적으로 붙을 수 있는 시간을 벌어주기 위함

 

gdb.attach()에 인자로 process()로 생성한 객체를 넣으면 해당 process에 gdb가 붙어 실행이 중단되고 gdb 터미널이 열려서 디버깅을 할 수 있게된다.

remote()로 생성한 객체나 프로세스의 PID를 전달해도 되지만 프로세스가 같은 장치에서 실행되고 있어서 디버깅이 가능한 프로세스인 경우에만 허용한다.

 

Assemble & Disassemble

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

 

context.arch로 아키텍처 설정할 수 있음.

asm()과 disasm()로 어셈블과 디스어셈블을 수행할 수 있음

from pwn import *

context.arch = "amd64" # x86-64 아키텍처로 설정

machine_code = asm('mov eax, 0')  # 어셈블리 'mov eax, 0'를 기계어로 변환
print(machine_code)
assembly_code = disasm(machine_code)  # 기계어를 어셈블리어로 변환
print(assembly_code)
b'\xb8\x00\x00\x00\x00'
   0:   b8 00 00 00 00          mov    eax, 0x0

 

ELF (Executable and Linkable Formate)

  • 리눅스의 실행파일
  • ELF()의 인자에 ELF 파일의 경로를 넣으면 pwnlib.elf.elf.ELF 클래스를 반환

 

symbol

pwnlib.elf.elf.ELF 클래스의 symbols 멤버 변수를 심볼들의 주소들을 가지고 있는 doctdict 클래스

doctdict 클래스 : 기존의 딕셔너리처럼 사용할 수 있으면서도 속성으로 접근이 가능

from pwn import *

e = ELF("./example")  # ELF 파일 열기
print(hex(e.symbols['write']))  # 'write' 심볼의 주소
print(hex(e.symbols.write)) # 'write' 심볼의 주소, 속성 접근
0x400000
0x401074
0x401074

 

시스템 해킹 수행시에는 대부분의 상황에서 물리주소보다는 가상 주소를 필요로 함

따라서 symbols에서 제공하는 주소는 가상 주소

실행파일에 PIE 보호 기법이 적용되어 있지 않다면 가상 주소 제공

PIE 보호 기법이 적용되어있다면 이미지 베이스로부터의 상대 가상주소를 제공