Repository: Gallopsled/pwntools

Issue:
interactive mode broken since version 3.11.0
## Description
Since version 4.11.0 the interactive mode does not work properly any more. In particular the following effect takes place. If I switch to interactive mode (from within a script) and i press "enter" pwntools will stop the process and exit. 

From what I could identify as root cause for the problem is the following commit: https://github.com/Gallopsled/pwntools/commit/58222ccf8b943dffc889e550990f5cb0604a8d46)
The line 407 of the module readline.py was changed so that the ``+b"\n"`` has been removed.

For the interactive() function in tube.py (around line 902 in tube.py) this has the consequence that when entering ``<enter>`` aka ``\n`` only,  the variable data is ``data=b""`` instead of ``data=b"\n"`` (as it was in version 4.10.0 and before). Now with ``data=b""`` the script exits the while loop (which would normally keep the interactive mode alive) and stops the process instead of continuing and simply sending a b"\n" to the process as it did before.
I am not sure about the reasons regarding the change but if this can be fixed it would be great and much appreciated.

## Example with debug context
```
└─$ python3 ./test.py DEBUG
[*] '/home/hacker/HeapLAB/challenge-fastbin_dup/fastbin_dup_2'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'../.glibc/glibc_2.30_no-tcache'
/home/hacker/HeapLAB/challenge-fastbin_dup/./test.py:5: BytesWarning: Bytes is not text; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  libc = ELF(elf.runpath + b"/libc.so.6") # elf.libc broke again
[*] '/home/hacker/HeapLAB/.glibc/glibc_2.30_no-tcache/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './fastbin_dup_2' argv=[b'./fastbin_dup_2'] : pid 11743
[*] Switching to interactive mode
[DEBUG] Received 0x84 bytes:
    b'\n'
    b'===============\n'
    b'|   HeapLAB   |  CHALLENGE: Fastbin Dup\n'
    b'===============\n'kali-rolling
    b'\n'
    b'puts() @ 0x7fafb866faf0\n'
    b'\n'
    b'1) malloc 0/13\n'
    b'2) free\n'
    b'3) quit\n'
    b'> '

===============
|   HeapLAB   |  CHALLENGE: Fastbin Dup
===============

puts() @ 0x7fafb866faf0

1) malloc 0/13
2) free
3) quit
> $ 
[*] Stopped process './fastbin_dup_2' (pid 11743)
```

## Affected Version
This Bug affects versions 4.11.0+ including the latest stable release 4.11.1.
This has only been tested on the following systems:
- kali-rolling - Debian 6.5.6-1kali1
- Linux Mint 21.2
- Ubuntu 22.04.3 LTS




Discussion:
I suppose we could ask @peace-maker about this.
Oh, I missed this somehow, sorry for the regression. The change of readline not returning a `b'\n'` was done to match normal Python behavior https://github.com/Gallopsled/pwntools/pull/2129#issuecomment-1540379942
I thought I tested all cases, but sending plain newlines should be possible of course. I'll see how we can get interactive() up to speed again, maybe just reverting the removal of \n from readline?


Resolving patch:
diff --git a/pwnlib/term/readline.py b/pwnlib/term/readline.py
--- a/pwnlib/term/readline.py
+++ b/pwnlib/term/readline.py
@@ -5,6 +5,7 @@
 
 import six
 import sys
+import os
 
 from pwnlib.term import keyconsts as kc
 from pwnlib.term import keymap as km
@@ -404,7 +405,7 @@ def readline(_size=-1, prompt='', float=True, priority=10):
                     buffer = (buffer_left + buffer_right)
                     if buffer:
                         history.insert(0, buffer)
-                    return force_to_bytes(buffer)
+                    return force_to_bytes(buffer) + b'\n'
             except KeyboardInterrupt:
                 control_c()
     finally:
@@ -432,7 +433,7 @@ def raw_input(prompt='', float=True):
         float(bool): If set to `True`, prompt and input will float to the
                      bottom of the screen when `term.term_mode` is enabled.
     """
-    return readline(-1, prompt, float)
+    return readline(-1, prompt, float).rstrip(os.linesep.encode())
 
 def str_input(prompt='', float=True):
     r"""str_input(prompt='', float=True)
@@ -445,7 +446,7 @@ def str_input(prompt='', float=True):
         float(bool): If set to `True`, prompt and input will float to the
                      bottom of the screen when `term.term_mode` is enabled.
     """
-    return readline(-1, prompt, float).decode()
+    return readline(-1, prompt, float).decode().rstrip(os.linesep)
 
 def eval_input(prompt='', float=True):
     """eval_input(prompt='', float=True)
@@ -471,7 +472,7 @@ def eval_input(prompt='', float=True):
         Favorite object? 20
     """
     from pwnlib.util import safeeval
-    return safeeval.const(readline(-1, prompt, float))
+    return safeeval.const(readline(-1, prompt, float).rstrip(os.linesep.encode()))
 
 def init():
     global safeeval
diff --git a/pwnlib/tubes/tube.py b/pwnlib/tubes/tube.py
--- a/pwnlib/tubes/tube.py
+++ b/pwnlib/tubes/tube.py
@@ -900,8 +900,8 @@ def recv_thread():
             while not go.isSet():
                 if term.term_mode:
                     data = term.readline.readline(prompt = prompt, float = True)
-                    if data:
-                        data += self.newline
+                    if data.endswith(b'\n') and self.newline != b'\n':
+                        data = data[:-1] + self.newline
                 else:
                     stdin = getattr(sys.stdin, 'buffer', sys.stdin)
                     data = stdin.read(1)


Test patch: