Post

pwntools

pwntools 연결



  • Local
1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

p = process(path)  

# ex) p = process('./example')


p = process(executable = '파일이름', argv = '인자', env = '환경변수')

# ex) p = process(excutable = './test', argv= _argv, env= _env)
# ex) p = process(_argv, executable = './test', env=_env)

# argv 는 리스트 형식이여야하고 env 는 딕셔너리 형식이여야 함.


  • Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

p = remote(ip, port) 

# nc, ex) p = remote('ctf.test.kr',1234)


p = ssh(username, ip, port, password) 

# ssh, ex) p = ssh("name", 'localhost', port=8000, password="test")


shell=p.run("/bin/sh")

# ssh일 때, run함수를 통해 쉘을 띄울 수 있음.






pwntools 데이터 받기



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
p = remote('ctf.test.kr', 1234)

data = p.recv(length).decode('latin-1')           
# recv로 데이터를 최대 length만큼 받아서 data에 저장

data = p.recvline().decode('latin-1')       
# 개행문자를 만날 때 까지 받아서 data에 저장 --> recvline은 한 줄만 받아옴

data = p.recvn(n).decode('latin-1')
# 출력하는 데이터를 n바이트만 받아서 data에 저장

data = p.recvuntil(value).decode('latin-1') 
# recvuntil은 value 값이 있는 부분까지만 받아옴
# ex) data = p.recvuntil(b'\n').decode('latin-1') -> 개행문자까지만 받아옴

data = p.recvall()
# 프로세스가 종료될 때 까지 값을 수신하여 data에 저장






pwntools 데이터 보내기



1
2
3
4
5
6
7
p.send(value)  # value 송신

p.sendline(value) # value + '\n' 송신

p.sendafter(b'test', value) # test가 출력되면 value 송신

p.sendlineafter(b'test', value) # test가 출력되면 value + '\n' 송신






packing 관련 함수



  • packing (int -> byte)
1
2
3
4
5
6
7
tmp = p32(int) 

# 32bit little endian 방식으로 패킹 (default)

# ex) tmp = p32(0x12345678) -> \x78\x56\x34\x12

# ex) tmp = p32(ABCD) -> \x68\x67\x66\x65


1
2
3
tmp = p32(value, endian='big')

# 32bit big endian으로 패킹


1
2
3
4
5
6
7
tmp = p64(int) 

# 64bit little endian 방식으로 패킹

# ex) tmp = p64(0x12345678) -> \x00\x00\x00\x00\x78\x56\x34\x12

# ex) tmp = p64(ABCD) -> \x00\x00\x00\x00\x68\x67\x66\x65



  • unpacking (byte -> int)
1
2
3
4
5
6
7
tmp = u32(byte) 

# 32bit little endian 방식으로 언패킹

# ex) tmp = u32('\x78\x56\x34\x12') -> 305419896
# ex) tmp = hex(u32('\x78\x56\x34\x12') -> 0x12345678


1
2
3
4
5
6
tmp = u64(str) 

# 64비트 리틀 엔디안 방식으로 언패킹

# ex) tmp = u64('\x00\x00\x00\x00\x78\x56\x34\x12') -> 305419896
# ex) tmp = hex(u64('\x00\x00\x00\x00\x78\x56\x34\x12')) -> 0x12345678






log 함수



익스플로잇에 버그가 발생하면 익스플로잇도 디버깅해야 한다.

로그 레벨은 context.log_level 변수로 조절할 수 있다.

레벨은 error, debug, info가 있다.

에러는 에러만 표시하고 debug는 모든 데이터를 출력하며 info는 비교적 중요한 정보들만 출력해준다.


  • context.log_level = 'debug'

    • 서버 혹은 바이너리와 이 익스플로잇간에 오고가는 모든 데이터를 화면에 hexdump형식으로 출력해줌






아키텍쳐 지정



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

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


1
2
3
4
5
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386"  # x86 아키텍처
context.arch = "arm"   # arm 아키텍처

context(arch='amd64', os='linux') # 한번에 설정하는 것이 






debug



  • gdb.attach(p)

    • 중간에 debug 하고 싶을 때 사용하면 새 창에서 gdb 프로그램 보여줌.






interactive 함수



  • p.interactive()

    • 쉘과 직접적으로 명령을 전송, 수신할 수 있는 함수로 보통 쉘을 딴 후 사용






ELF 함수



  • elf = ELF('filename')

    • ELF 바이너리를 인식시켜 적용되어 있는 보호기법을 보여주고 plt, got 같은 ELF 정보 등을 가져올 수 있는 함수


  • hex(elf.address)

    • 만약 매핑된 시작 주소를 확인하고 싶으면 address 변수 사용


  • elf.functions

    • 함수의 주소 혹은 함수들의 리스트 등이 알고 싶을 때에는 functions 변수 사용

    • ex) elf.functions['main']


  • libc = elf.libc

    • plt, got, symbols 정보를 알고 싶으면 아래처럼 딕셔너리 변수들 입력

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      
        plt = elf.plt['scanf']
        # filename에서 scanf()의 plt 주소를 찾아서 저장. 
            
        got = elf.got['scanf']
        # filename에서 scanf()의 got 주소를 찾아서 저장.
            
        libc = ELF('library')
        symbols = libc.symbols['function_name']
        # libc의 base로부터 함수 사이의 오프셋을 가져옴
      


  • 특정 section 주소를 알고 싶을 때

    • 1
      2
      3
      4
      5
      
        hex(elf.get_section_by_name('.bss').header.sh_addr) # 0x804a020
            
        hex(elf.bss()) #0x804a020
            
        hex(elf.get_section_by_name('.text').header.sh_addr)
      


  • hex(next(elf.search('libc.so.6'))) -> '0x804822d'

    • 특정 문자열을 찾아 그 주소를 가져 오고 싶을 땐 search 변수 이용
      • offset을 가져오는 것이라고 함.


  • print(elf.checksec())

    • 다시 보호기법을 확인하고 싶을 땐 checksec()함수 사용






shellcraft



꼭 보기, 출처
oz1ng019.tistory.com/24?category=762782


pwntools에는 자주 사용되는 셸 코드가 저장되어 있는데, 공격에 필요한 셸 코드를 쉽게 꺼내 쓸 수 있게 명령어를 제공한다.

단, 프로그램에서 셸을 따는 데 제약 조건 등이 존재하는 경우 직접 제작해야 한다.


사용법
shellcraft.[환경].[OS].[syscall 함수](함수 인자)

함수 인자에는 ‘주소’를 입력할 때, 꼭 레지스터를 사용해야 한다고 한다.

코드 맨 위에 context(arch='amd64', os='linux')를 지정해주면, 환경과 os를 써주지 않아도 된다.
shellcraft.[syscall 함수](함수 인자)


shellcode doc
docs.pwntools.com/en/stable/shellcraft/amd64.html#pwnlib-shellcraft-amd64-shellcode-for-amd64


1
2
3
4
# context.arch = 'amd64' 지정해야 한다.
context(arch='amd64', os='linux')

code = shellcraft.sh() # 셸을 따는 코드


  • orw 셸코드를 만들 때
1
2
3
4
5
6
7
8
9
10
11
12
context(arch='amd64', os='linux')

sc = shellcraft.amd64.linux.open('./flag') 
# open으로 ./flag라는 이름의 파일을 연다.

sc += shellcraft.amd64.linux.read('rax','rsp',100) 
# ./flag에서 값을 'rsp'에 100만큼 읽어온다. ('rax'는 ./flag의 핸들러)

sc += shellcraft.amd64.linux.write(1,'rsp',100) 
# write함수를 써서 'rsp'에 저장된 값을 출력한다. 

p.send(asm(sc))






asm



pwntools는 어셈블 기능을 제공한다.

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


1
2
3
4
5
6
7
8
9
10
from pwn import *

context.arch = 'amd64' # 익스플로잇 대상 아키텍처 'x86-64'

code = shellcraft.sh() # 셸을 실행하는 셸 코드

code = asm(code)       # 셸 코드를 기계어로 어셈블

print(code)
# 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'






ROP



1
2
3
4
5
6
e = ELF(binary)
r = ROP(e) # ROP를 위한 객체 호출

prdi = (r.find_gadget(['pop rdi', 'ret']))[0] # binary에 존재하는 pop rdi, ret 가젯의 주소

출처 : [https://whitel0tus.tistory.com/18]






Example



아래는 동아리 CTF에서 pwntools를 이용한 소켓 프로그래밍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from pwn import *
import re

def recvmessage() :
    recv=p.recv()
    msg=recv.decode('utf-8')
    print(msg)

def find_answer_and_send() :
    for i in check_num :
    if random < int(numbers[i]) :
        answer=numbers[i-2].encode('utf-8')
        p.sendline(answer)
        break

p=remote('[redacted]',[redacted])
start_msg=p.recvuntil(';D')
print(start_msg.decode('utf-8'))

recvmessage()

numbers=re.findall("\d+",msg)
random=int(numbers[0])
check_num=[3,6,9,12,15]

find_answer_and_send()

while 1 :
    recvmessage()    
    
    if "[redacted]" in msg :
       break

    number=re.findall("\d+",msg)
    random=int(number[0])
    
    recvmessage()
    
    numbers=re.findall("\d+",msg)
    check_num=[2,5,8,11,14]

    find_answer_and_send()






출처



출처
security-nanglam.tistory.com/155
lclang.tistory.com/94?category=254640
Dreamhack - Tool: pwntools






This post is licensed under CC BY 4.0 by the author.