[Pwnable] pwntools 사용법
*해당 포스팅은 처음 pwntools를 공부하면서 이것저것 찾아보면서 작성하여 정확하지 않은 정보가 있을수도 있습니다..!
pwntools 설치
PEP 668 정책때문에 pwntools 직접 설치가 안되서 가상환경 venv 위에서 패키지를 설치하였다
venv 설치 방법
python3 -m venv pwntools-env
source pwntools-env/bin/activate
venv 실행 방법
1. 가상환경 생성하기
python3 -m venv pwntools-env
2. pwntools 설치
pip install --upgrade pip
pip install pwntools
3.가상환경 활성화
source pwntools-env/bin/activate
가상화면을 끄고싶을 때는 deactivate를 입력하면 된다.
deactivate


venv를 활성화한 뒤 python을 입력하면 파이썬이 실행된다
거기서 from pwn import*를 입력하면 pwntools 사용 준비가 끝났다!
pwntools 기초 명령어
접속
process() : 로컬에 위치한 프로그램을 실행하여 통신할 때 사용한다. process의 인자로 실행하여 통신할 프로그램의 경로를 전달하면 프로그램을 실행한뒤 연결을 맺어주어, 로컬 실행파일을 켜고 제어할 때 사용한다.
연결이 맺어지면 pwnlib.tubes클래스를 반환하여 p에 저장한다.
p = process("./example")
p = process(["./example", "AAAA"], env={"LD_PRELOAD":"./libc.so.6"})
해당 방식으로 사용하면 프로그램의 첫번째 argv에 "AAAA"라는 문자열을 전달하고, LD_PRELOAD환경 변수를 ./libc.so.6으로 설정하여 실행한다.
process()에 인자 전달하는 것은 터미널에서
/example AAAA
와 똑같은 명령이다.
remote() : 호스트의 도메인 혹은 IP 주소와 포트 번호를 인자로 받아 원격 서버에 통신할 때 사용되는 함수이다.
기본적으로 TCP로 연결된다
r = remote("example.com", 1337) #remote(host, port)
아래와 같이 typ = 'udp'를 추가하면 UDP로 연결된다
r = remote("example.com", 1337, typ='udp')
ssh() : Secure Shell의 약자이다. 아래코드는 123.123.123.1 호스트의 22번 포트에 열린 SSH 서버에 poiri3r이라는 사용자이름과 diamond라는 비밀번호로 로그인하여 접속하는 코드이다.
s = ssh("poiri3r", "123.123.123.1", port=22, password="diamond")
#ssh(user, host, port, password)
데이터 송수신 관련 함수
recv() : 데이터를 수신하기 위해 사용
p = process('./example')
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에 저장
데이터를 읽을 때 프로세스가 종료되지 않았다면 p.recv()와 같은 함수들을 여러번 써도 상관없지만, 프로세스가 종료되었을경우 p=process()로 다시 어태치해줘야 한다.

recv 실습!

#include <stdio.h>
#include <unistd.h>
int main() {
printf("Welcome to pwntools test!\n");
write(1, "ABCDE", 5); // 표준출력에 5바이트 직접 출력
printf("hello world\n");
printf("Goodbye!\n");
return 0;
}
다음과 같은 C언어 예제 코드를 만들어서 gcc로 컴파일하였다.

data = p.recv(1024) # 데이터를 최대 1024바이트까지 받음

p.recv(1024)로 데이터를 수신한 뒤 print(data)로 출력을 했다. 원본 코드에 printf와 write부분이 제대로 출력이 된 모습이다.
data = p.recvline() #p가 출력하는 데이터를 \n까지 받음

p.recv를 사용한 뒤 프로세스가 종료되었기 때문에 다시 어태치를 한 뒤 recvline()를 입력하였다. 첫번째 /n전인 Welcome to pwntools test!까지 출력된 모습이다.
data = p.recvn(5) # p가 출력하는 데이터를 5바이트만 받아서 data에 저장

recvn(5)로 저장한 결과 5글자인 ABCDE만 저장되어 출력되었다.
data = p.recvuntil(b'word') # p가 b'word'입력할 때까지 데이터를 수신하여 data에 저장

world를 입력할때까지 데이터를 수신하였고, hello world가 정상적으로 출력되었다.
data = p.recvall() #p가 출력하는 데이터를 프로세스가 종료될 때 까지 받아서 출력

남은 문자열인 \nGoodBye\n이 모두 출력되었다.

send() : 데이터를 전송하기 위해 사용한다. 해당 함수들은 데이터를 bytes 클래스 인자로 받아 전송한다.
p = process('./example')
p.send(b'A') # ./example에 b'A'를 입력
p.sendline(b'A') # ./example에 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'을 입력
sendafter()와 sendlineafter()은 실제 인자로 전달된 내용들이 나올때 까지 수신한 뒤 데이터를 전송한다. 즉 sendafter()와 sendlineafter()를 실행한 뒤 데이터를 수신한 경우 sendafter()와 sendlineafter()에서 수신한 데이터 이후의 데이터들을 수신하게 된다.
send()는 표준 입력(stdin)으로 데이터를 쓰는 동작으로 대상 프로세스가 종료되었거나 입력을 받지 않는다면 BrokenPipeError나 EOFError가 뜨게된다..!


send()실습을 위해 사용자 입력을 받도록 아까 예제 코드를 수정했다.
#include <stdio.h>
#include <unistd.h>
int main() {
char input[100]; // 사용자 입력 버퍼
printf("Welcome to pwntools test!\n");
write(1, "ABCDE", 5); // 표준출력에 5바이트 직접 출력
printf("hello world\n");
printf("Enter something: ");
fflush(stdout); // 출력 버퍼 강제 비우기 — 이게 중요!
fgets(input, sizeof(input), stdin); // 사용자 입력 받기
printf("You entered: %s", input);
printf("Goodbye!\n");
return 0;
}
마찬가지로 gcc로 컴파일 해준 뒤 venv에서 다시 연결해줬다.

그 뒤에 p.sendline으로 원하는 입력을 한다.

코드에 있는 You entered와 함께 입력한 문자열이 제대로 출력됐음을 볼 수 있다.
interactive() : 터미널에서 사용자가 실시간으로 데이터를 수신하고 전송할 수 있게 하는 함수이다. 이 함수를 호출하면 터미널을 통해 프로세스에 입력값을 전달할 수 있고 출력도 실시간으로 출력된다. 다른 함수처럼 pwnlib.tubes 클래스에 구현되어있기 때문에 p.interactive() 로 사용 가능하다
p = process('./example')
p.interactive()
interactive를 사용하면 프로그램 출력이 터미널에 바로 출력되고 입력이 바로 전달되어 프로그램에 여러번 입력을 주고 받아야 하거나 원격 서버 접속 후 직접 조작할 때 유용하다


예제로 작성된 example코드는 함수 실행이 끝나면 자동으로 return 0이 되고 프로그램이 종료되어 interactive모드가 필요하진 않지만 자동으로 종료되지 않는 복잡한 코드일경우 더 유용하다
close() : 위에 함수들과 동일하게 pwnlib.tubes클래스에 구현된 함수이며, 연결을 종료하고 싶을 때 사용한다.
p = process('./example')
p.close() # 실행되는 순간에 연결이 유지되고 있는 상태여야 함
로그 출력
pwntools에서는 context.log_level을 이용해 로그 출력의 상세도를 설정할 수 있다.
from pwn import*
context.log_level = 'debug'
p=process("./example")
p.recvall()
| critical | 치명적인 오류만 출력 |
| error | 에러 메세지만 출력 |
| warning | 경고 메세지 출력 |
| info | 일반적인 상태 메세지 출력 |
| debug | 송수신 데이터, 내부 변수 등 상세한 정보 출력 |
로그 출력 상세도 표
context.log_level을 따로 설정하지 않으면 기본값으로 info 상태이다.
패킹/언패킹
파이썬의 struct 모듈에서는 데이터를 원하는 포멧대로 변환하고 변환된 값을 원래대로 되돌리는 함수의 이름으로써 pack()과 unpack()이 사용되고, 실행 파일을 압축하거나 압축된 실행파일을 되돌리는 것을 패킹과 언패킹이라고 표현한다.
그 중 정수 값을 bytes 클래스로 변환하거나 그 반대의 행위를 pwnable에서의 패킹과 언패킹이라고 표현한다.
p8(),p16(),p32(),p64() : 정수 숫자를 bytes 클래스로 패킹하는 함수
u8(),u16(),u32(),u64() : bytes 클래스를 숫자로 언패킹하는 함수
함수명 뒤에 붙은 숫자는 패킹 or 언패킹을 수행할 때 bytes클래스의 비트 수를 의미한다.
패킹/언패킹 실습


좌측 사진은 s8,s16,s32,s64 변수에 각각 41부터의 16진수숫자를 넣었고 (각 알파벳이 시작하는 ASCII값)이고
우측 사진은 해당 숫자를 packing한 뒤 출력한 값이다.


반대로 변수에 A,AB,ABC,ABCDEFGH를 넣고 언패킹한 값을 헥스코드로 풀면 문자에 대응하는 ASCII숫자 값이 나오는 것을 확인할 수 있다.
GDB
GDB(GNU Debuggert)은 얼마전 포스팅했던 리눅스의 대표적인 디버거로써 실행파일을 분석할 때 사용하게 된다.
pwntools에는 GDB와 기능들이 많은데 프로세스에 디버거를 붙여서 디버깅 할 수 있는 상태로 만들 수 있다.
attach : gdb.attach()의 인자로 process()로 생성한 객체를 넣으면 해당 프로세스에 GDB가 붙어 실행이 중단되고 GDB터미널이 열려 디버깅을 수행할 수 있다. remote()로 생성한 객체나 PROCESS ID를 전달해도 가능하지만, remote()로 생성한 객체를 전달할 경우에는 remote()로 접속한 프로세스가 같은 장치에서 실행되고 있어 디버깅이 가능한 프로세스인 경우에만 가능하다.
Pwntools + GDB 프로세스 실습 코드
#include <stdio.h>
int main() {
int a = 1234;
int b;
printf("Enter number: ");
scanf("%d", &b);
printf("Sum is: %d\n", a + b);
return 0;
}
gcc -g poirier.c -o poirier #-g옵션은 GDB 디버깅 정보를 포함시킨다


왼쪽과 같이 gdb.attach(p)를 사용하면 자동으로 pwndbg창이 뜨면서 process가 attach된다.
Assemble & Disassemble
어셈블과 디스어셈블은 시스템해킹에서 자주 요구되기 때문에 pwntools에서 구현해서 제공하고 있다. 먼저 아키텍처(ISA)마다 어셈블리어와 기계어가 다르기 때문에 아키텍처를 먼저 설정해야 한다.
context.arch : 아키텍처를 설정한다
| context.arch = "amd64" | x86-64 아키텍처 |
| context.arch = "i386" | x86 아키텍처 |
| context.arch = "arm" | arm 아키텍처 |
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)

ELF
ELF는 리눅스 운영체제의 실행 파일 형식으로, pwntools에는 ELF 파일과 관련된 기능들을 지원한다.
ELF()의 인자에 ELF 파일의 경로를 넣으면 pwnlib.elf.elf.ELF 클래스를 반환한다.
pwnlib.elf.elf.ELF 클래스에는 다양한 멤버 변수들과 기능을 지원하는 함수들이 구현되어 있다.
elf = ELF("./바이너리 경로")
를 통해 ELF 파일을 열 수 있다.

ELF 심볼이니 머니 다른 것들은 봐도 모르겠어서 다음에 따로 포스팅 하겠습니다..ㅎ
다음에 문제 풀면서 Write up 작성하면서 새로 알게된 점들을 추가 포스팅 하도록 하겠습니다.
포너블은 참 어렵네요 ..
이상으로 pwntools에 대한 기초 사용법 포스팅을 마치겠습니다!
