[에러 해결] Pwntools: ‘Process creation failed’ (sh.process() None 반환) 원인과 해결 방법

안녕하세요, 개발자 여러분! Pwntools는 CTF(Capture The Flag)와 익스플로잇 개발에 필수적인 파이썬 라이브러리입니다. 하지만 원격 서버에서 프로그램을 실행하려 할 때, 때때로 예상치 못한 에러와 마주치기도 합니다. 오늘은 sh.process() 메서드 사용 시 발생하는 ‘Process creation failed’ 에러와 None 반환 문제의 원인을 분석하고, 효과적인 해결 방법을 제시하겠습니다. 특히 pwnable.kr과 같은 CTF 플랫폼에서 이 문제에 직면한 분들께 도움이 될 것입니다.

1. 에러 발생 상황

이 에러는 주로 Pwntools의 ssh 모듈을 사용하여 원격 서버에 연결한 후, 특정 프로그램을 sh.process()로 실행하려 할 때 발생합니다. 사용자는 sh.run() 메서드를 통해 ls 같은 명령은 정상적으로 실행되지만, sh.process("./passcode")와 같이 대화형 프로세스를 시작하려 하면 실패하는 상황에 직면합니다. 아래는 문제가 발생한 코드와 그 출력 예시입니다.

문제 코드 예시:

from pwn import *
HOST = "pwnable.kr"
USER = "passcode"
PASSWORD = "guest"
sh = ssh(host=HOST, user=USER,port = 2222, password=PASSWORD)
print(f"Connection:{ sh.connected()} ")
result = sh.run("ls")
print(result.recvall().decode())
print(sh.run("ls -l ./passcode").recvall().decode())
# 여기서 문제 발생:
p = sh.process("./passcode") # 이 부분이 None을 반환합니다.
print(p)
print(p.recv().decode()) # p가 None이므로 에러 발생

에러 출력 예시:

Connecting to pwnable.kr on port 2222: Done
[*] passcode@pwnable.kr:
    Distro    Unknown Unknown
    OS:       Unknown
    Arch:     Unknown
    Version:  0.0.0
    ASLR:     Disabled
    SHSTK:    Disabled
    IBT:      Disabled
    Note:     Susceptible to ASLR ulimit trick (CVE-2016-3672)
Connection:True
[+] Opening new channel: 'ls': Done
[+] Receiving all data: Done (34B)
[*] Closed SSH channel with pwnable.kr
flag  passcode  passcode.c  readme

[+] Opening new channel: 'ls -l ./passcode': Done
[+] Receiving all data: Done (61B)
[*] Closed SSH channel with pwnable.kr
-r-xr-sr-x 1 root passcode_pwn 15232 May 23 16:00 ./passcode

[-] Starting remote process None on pwnable.kr: Process creation failed
None
Traceback (most recent call last):
  File "/home/kali/Downloads/passcode.py", line 13, in 
    print(p.recv().decode())

출력에서 볼 수 있듯이, ls 명령은 정상적으로 작동하며 ./passcode 파일의 권한도 확인됩니다. 하지만 sh.process("./passcode")에서 Process creation failed 메시지가 뜨고 p 변수가 None이 되어 다음 줄에서 AttributeError가 발생하는 것을 확인할 수 있습니다.

2. 명확한 발생 원인

이 문제의 핵심 원인은 Pwntools의 sh.process() 메서드가 작동하는 방식과 원격 서버 환경의 보안 정책 때문입니다.

2.1. sh.process()의 역할

sh.process()는 로컬에서 process()를 사용하는 것과 유사하게, 원격 서버에 *새로운 대화형(interactive) 프로세스*를 독립적으로 생성하고 해당 프로세스의 표준 입출력(stdin/stdout)을 제어할 수 있는 튜브(tube) 객체를 반환하려고 시도합니다. 이는 일반적으로 원격 서버에 새로운 셸 세션이나 가상 터미널(pseudo-terminal, pty)을 할당하고 그 안에서 프로그램을 실행하는 복잡한 과정을 거칩니다.

2.2. 원격 서버의 보안 제약

대부분의 CTF 서버나 보안이 강화된 리눅스 서버 환경에서는 일반 사용자 계정(여기서는 passcode 사용자)이 임의로 새로운 대화형 프로세스를 생성하거나, fork()exec() 시스템 호출을 통해 새로운 프로세스 트리를 시작하는 것이 제한될 수 있습니다. 특히 pwnable.kr과 같은 CTF 플랫폼은 악용을 방지하기 위해 이러한 기능에 엄격한 제한을 두는 경우가 많습니다.

  • 권한 문제: passcode 사용자가 원격에서 새로운 프로세스를 생성하는 데 필요한 적절한 권한이 없을 수 있습니다.
  • 자원 제한: 서버 관리자가 특정 사용자의 프로세스 생성 수나 메모리 사용량에 ulimit 등으로 제한을 걸었을 수 있습니다.
  • 가상 터미널(PTY) 제한: 새로운 대화형 프로세스는 종종 가상 터미널이 필요한데, 서버 설정에서 이를 제한할 수 있습니다.

반면, sh.run()은 단순히 원격 SSH 세션 내에서 명령을 실행하고 그 결과를 파이프를 통해 받아오는 방식이므로, 새로운 독립적인 프로세스를 생성하는 것보다 훨씬 낮은 권한과 복잡성을 요구합니다. 따라서 sh.run()은 잘 작동하지만, sh.process()는 실패하는 것입니다.

ls -l ./passcode 결과에서 보이는 -r-xr-sr-xs (setuid) 비트는 프로그램이 실행될 때 파일 소유자(root)의 권한으로 실행되게 하지만, 이는 프로세스 *생성* 자체의 권한과는 무관합니다. 즉, passcode 사용자는 ./passcode를 실행할 수는 있지만, sh.process()가 요구하는 방식으로 독립적인 프로세스를 생성할 권한은 없을 수 있습니다.

3. 해결 방법 및 코드 예시

sh.process()가 원격 환경에서 제한된다면, 그 대신 sh.run() 메서드를 활용하여 프로그램을 실행하고 상호작용하는 방식으로 접근해야 합니다. sh.run()은 명령 실행 후 그 명령의 표준 입출력에 연결된 튜브 객체를 반환하므로, 이를 통해 얼마든지 대화형 상호작용이 가능합니다.

해결 코드 예시: sh.run()을 이용한 상호작용

다음 코드는 sh.run()을 사용하여 ./passcode 프로그램을 실행하고, 예상되는 프롬프트에 응답하며 페이로드를 전송하는 일반적인 CTF 시나리오를 보여줍니다.

from pwn import *

# 연결 정보 설정
HOST = "pwnable.kr"
USER = "passcode"
PASSWORD = "guest"
PORT = 2222

# SSH 연결
print(f"[*] Connecting to {HOST} on port {PORT}...")
sh = ssh(host=HOST, user=USER, port=PORT, password=PASSWORD)
print(f"[+] Connection status: {sh.connected()}")

# 1. 'sh.run()'을 사용하여 './passcode' 프로그램 실행
# sh.run()은 원격에서 명령을 실행하고, 해당 명령의 stdin/stdout에 연결된 tube 객체를 반환합니다.
# 이를 통해 프로그램과 상호작용할 수 있습니다.
print("[*] Using 'sh.run()' to execute './passcode' and interact.")
io = sh.run("./passcode")

# 프로그램이 특정 프롬프트를 출력할 때까지 기다립니다.
# 'Input your passcode:'와 같은 문자열을 기다리는 것이 일반적입니다.
# 여기서는 ':' 문자를 포함하는 첫 번째 줄까지 기다린다고 가정합니다.
# 실제 CTF 문제에 따라 정확한 프롬프트를 파악해야 합니다.
try:
    prompt = io.recvuntil(b":", timeout=2).decode() # 프롬프트를 기다립니다. (2초 타임아웃)
    print(f"[+] Received prompt: {prompt.strip()}")
except EOFError:
    print("[-] No prompt received, or program exited early.")
    # 프로그램이 프롬프트 없이 바로 종료되거나, 다른 방식으로 동작할 수 있습니다.
    # 이 경우 아래 페이로드 전송 로직을 조절해야 합니다.
    io.close()
    sh.close()
    exit(1)

# 2. CTF 문제의 페이로드를 전송합니다.
# 'passcode' 문제는 스택에 특정 값을 덮어쓰는 것이 목표입니다.
# 이 예시에서는 4바이트 더미 값(AAAA)을 전송하는 것으로 대체합니다.
# 실제 해법에 맞는 페이로드를 여기에 넣어주세요.
# 예를 들어, 오버플로우를 유발하는 4바이트 값 (0xdeadbeef 등)을 리틀 엔디언으로 변환하여 사용:
# payload = p32(0xdeadbeef)
# 현재는 예시를 위해 단순 바이트열 사용
payload = b"AAAA" 

print(f"[*] Sending payload: {payload.hex()}")
io.sendline(payload)

# 3. 프로그램의 응답을 받습니다.
# 프로그램이 최종 결과(예: 플래그)를 출력할 때까지 기다립니다.
print("[*] Receiving program response...")
response = io.recvall(timeout=2) # 모든 응답을 받을 때까지 기다립니다. (2초 타임아웃)
print(f"[+] Program output:\n{response.decode()}")

# 사용한 tube와 SSH 연결을 닫습니다.
io.close()
sh.close()
print("[*] Script execution completed. SSH connection closed.")

이 코드는 sh.run()을 사용하여 원격 프로그램을 실행하고 io.recvuntil(), io.sendline(), io.recvall()과 같은 Pwntools 튜브 메서드를 통해 상호작용하는 방법을 보여줍니다. 이는 sh.process()가 실패할 때 사용할 수 있는 가장 일반적이고 효과적인 대안입니다.

4. 향후 예방을 위한 팁

이러한 문제를 사전에 방지하고 효율적인 익스플로잇 개발을 위해 다음 팁들을 기억해두세요.

  1. Pwntools 메서드 용도 이해하기:

    • process(): 로컬에서 새로운 프로세스를 생성하고 상호작용할 때 사용합니다.
    • remote(): 원격 네트워크 서비스(예: nc host port)에 연결하고 상호작용할 때 사용합니다.
    • ssh.run(): SSH 연결을 통해 원격에서 명령을 실행하고 그 입출력에 연결된 튜브를 얻을 때 사용합니다. 대부분의 CTF에서 원격 프로그램 실행 및 상호작용에 가장 적합합니다.
    • ssh.process(): SSH 연결을 통해 원격에서 새로운 대화형 프로세스를 독립적으로 생성하려 할 때 사용합니다. 원격 서버의 보안 정책에 따라 실패할 확률이 높으므로, 작동하지 않는다면 ssh.run()으로 전환하는 것을 고려하세요.
  2. 원격 환경에 대한 이해:

    CTF 플랫폼이나 실제 서버 환경은 로컬 개발 환경과 다릅니다. 원격 서버의 파일 시스템 구조, 권한 설정, 방화벽, ulimit 등의 자원 제한 사항을 항상 염두에 두어야 합니다.

  3. 에러 메시지 분석:

    Process creation failed와 같은 명확한 에러 메시지는 문제의 근본 원인을 파악하는 데 중요한 단서가 됩니다. 에러 메시지를 검색하거나 Pwntools 공식 문서를 참고하여 추가 정보를 얻으세요.

  4. CTF 문제의 의도 파악:

    대부분의 CTF 문제는 특정 취약점을 통해 셸을 얻거나, 특정 값을 입력하여 플래그를 읽어내는 방식으로 설계됩니다. sh.run()을 통해 입력과 출력을 제어하는 것이 이러한 시나리오에 더 적합한 경우가 많습니다.

이 문서를 통해 Pwntools 사용 시 sh.process() 에러로 인한 어려움을 해결하고, 더욱 효율적인 익스플로잇 개발을 진행하시길 바랍니다!

댓글 남기기