pwntools recv recvline pitfall

최근에 문제를 풀다가 내가 pwntools에서 recv recvline에 대해 오해한 정보가 있어서 여기에 서술하겠다.

C
    p = remote('host8.dreamhack.games', 20769)
    
    p.sendline(str(3))
    p.sendline(str(20))
    p.recvline()
    p.sendline(str(1))

    payload = b"A" * 80
    p.sendline(payload)
    p.recvline()
    p.sendline(str(3))
    p.sendline(str(2))

    data = p.recvline()
    print("data -> " , data[:-1])
    data = int(data[:-1] , 10)
    print(hex(data))
    p.interactive()

예를 들어 다음과 같은 익스 코드가 있다고 하자 중간 중간에 p.recvline() 이 보일 것이다.

이런식으로 recvline() 하고 sendline 하고를 하는 대화형 프로그램인데… 과연 상단의 코드가

C
    p = process(fname , env = {'LD_PRELOAD' : './libc.so.6'} ) # libc 지정
    
    p.sendline(str(3))
    p.sendline(str(20))

    p.sendline(str(1))

    payload = b"A" * 80
    p.sendline(payload)

    p.sendline(str(3))
    p.sendline(str(2))

    p.recvline()
    p.recvline()
    data = p.recvline()
    print("data -> " , data[:-1])
    data = int(data[:-1] , 10)
    print(hex(data))
    p.interactive()

p.recvline() 을 한번에 연달아 2번을 하는 이 코드가 똑같이 작동할까?

나는 처음에는 당연히 안될줄 알았다. p.recvline() 이 실시간으로 버퍼에서 받을 때까지 대기를 하는 함수인지 알았기 때문이다. 하지만, 약간의 오해가 있다.

p.recvline() 은

  1. 내부 버퍼에 이미 개행까지 한 줄이 있으면 → 그걸 즉시 반환
  2. 아니면 소켓에서 새 데이터를 받으며 개행이 나올 때까지 대기(또는 타임아웃 시 b'')

다음과 같이 작동한다. 즉, 1번이 있다는 것이다.

이 내부 버퍼에 대해 서술하자면,

  • pwntools의 모든 I/O 객체(process, remote, ssh 등)는 공통으로 튜브(tube) API를 씁니다. 내부에 Buffer가 있고, 먼저 도착한 데이터가 먼저 소비됩니다. recvline()은 이 버퍼에서 개행까지 한 줄을 꺼내옵니다(없으면 새 데이터가 올 때까지 대기/타임아웃).

다음과 같다. 즉, 내부 버퍼에 프로그램의 출력들이 차곡차곡 저장이 되고 p.recv~~() 계열의 함수들은 그걸 버퍼의 시작부터 가져오기만 할 뿐이다. 만약 버퍼가 없으면 그때 대기를 한다.

따라서,

C
    p = remote('host8.dreamhack.games', 20769)
    
    p.sendline(str(3))
    p.sendline(str(20))
    p.recvline()
    p.sendline(str(1))

    payload = b"A" * 80
    p.sendline(payload)
    p.recvline()
    p.sendline(str(3))
    p.sendline(str(2))

    data = p.recvline()
    print("data -> " , data[:-1])
    data = int(data[:-1] , 10)
    print(hex(data))
    p.interactive()

다음과 같이 받을때마다 p.recvline() 을 통해 내부버퍼를 정렬해놓아야한다. 근데 이는 귀찮다.

따라서,

C
    p = remote('host8.dreamhack.games', 20769)
    
    p.sendline(str(3))
    p.sendline(str(20))
    //p.recvline()
    p.sendline(str(1))

    payload = b"A" * 80
    p.sendline(payload)
    //p.recvline()
    p.sendline(str(3))
    p.recv()    //버퍼 미리 다 사용하기
    p.sendline(str(2))

    data = p.recvline()
    print("data -> " , data[:-1])
    data = int(data[:-1] , 10)
    print(hex(data))
    p.interactive()

다음과 같이 새로운 입력을 받기 전에 p.recv() 등을 사용해서 버퍼에 있는 내용을 다 사용하거나,

p.clean() 으로 명시적으로 버퍼를 정리할 수 있다.

근데 이때 네트워크 환경 ( remote )에서는 타이밍과 관련된 문제가 생길 수 있다.

상당의 코드를 동작시켰을때,

C
   payload = b"A" * 80
   p.sendline(payload)
   //p.recvline()

이 이후에 데이터를 받아야 하는데 네트워크 환경이라서 데이터가 늦게와

C
    p.sendline(str(3))
    p.recv()    //버퍼 미리 다 사용하기
    p.sendline(str(2))

이 부분이 먼저 실행이 되어버리는 것이다. 그럼 이 다음 코드인

C
data = p.recvline()
print("data -> " , data[:-1])

이 부분에서 해당 데이터가 처리가 되어버리는 의도하지 않은 전개가 될 수 있다. 따라서 네트워크 환경에서는 입력을 받을때 p.recv~ 계열 함수로 정렬을 시켜놓던가 아니면,

C
    p.sendline(str(3))
    sleep(0.5)
    p.recv()    //버퍼 미리 다 사용하기
    p.sendline(str(2))

이런식으로 기다리는 것도 하나의 방법이긴 하다.