ssh_process::getenv ignores environment and returns wrong result
Introduction
ssh_process::getenv returns an incorrect result and ignores overridden environment variables with process or system.
Example
With script.py containing:
#!/usr/bin/env python2
from pwn import *
context.log_level = 'error'
user = 'narnia0'
host='narnia.labs.overthewire.org'
password='narnia0'
port=2226
s = ssh(user=user, host=host, password=password, port=port)
p = s.process('/narnia/narnia0', env={'FOO': 'BAR'})
print(hex(p.getenv('FOO')))
Running it gives:
$ ./script.py
1
Possible fix
Using the following patch:
diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py
index fd3a0307..bfcc4ab4 100644
--- a/pwnlib/tubes/ssh.py
+++ b/pwnlib/tubes/ssh.py
@@ -365,6 +365,7 @@ class ssh_process(ssh_channel):
script = ';'.join(('from ctypes import *',
'import os',
'libc = CDLL("libc.so.6")',
+ 'libc.getenv.restype = c_voidp',
'print os.path.realpath(%r)' % self.executable,
'print(libc.getenv(%r))' % variable,))
@@ -998,7 +999,7 @@ os.execve(exe, argv, env)
script = 'for py in python2.7 python2 python; do test -x "$(which $py 2>&1)" && exec $py -c %s check; done; echo 2' % sh_string(script)
with context.local(log_level='error'):
- python = ssh_process(self, script, tty=True, raw=True, level=self.level, timeout=self.timeout)
+ python = ssh_process(self, script, tty=True, env=env, raw=True, level=self.level, timeout=self.timeout)
try:
result = safeeval.const(python.recvline())
It returns:
$ ./script.py
0xffffefe5
Notes
ssh::getenv needs similar patching for the result type (even though I'm not sure why it is used for, since it doesn't handle argv nor env).
I think the way forward for this will be to force core dumps on the remote process, and use the Coredump API. Unfortunately it's difficult to do this currently, there's no pre-canned solution. It also requires duplicates of the process in the event of a setuid binary.
The current method of shelling out to python on the remote system does not work for...
- Mixed architectures (e.g. 64-bit target binary and a 32-bit python)
- Foreign architectures (e.g. ARM on Intel, anything that uses qemu-xxx emulation)
- Any target machines without
pythoninstalled at all
In the case of your example, you're running into the issue you identified (env is not passed along) but also running into (1). 0xffffefe5 does not seem like a likely / valid 32-bit stack address.
/narnia/narnia0: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0840ec7ce39e76ebcecabacb3dffb455cfa401e9, not stripped
/usr/bin/python: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=6aac156c31a134f49fb9c5c62d223c163fb1b648, stripped
Following up, I believe that we can solve (1) by using Corefile. The issue is getting at the corefile.
The issue with foreign architectures is not solvable, since any generated Corefile will be for the QEMU process itself, not the "emulated" process. What we can do is try to get qemu to generate a corefile by killing the process with e.g. kill -SIGTRAP {pid}.
For native processes, we are also trading a dependency on python for a dependency on gdb.
Indeed, I used a 32-bit VM for testing so that worked properly...
To keep it simple, we could check that python on the remote host matches context.arch.
For mixed architectures, we could rely on something like hellman's fixenv that you use in your write-ups if the remote machine has gcc and is able to build 32-bit executables (but it depends on how the challenges were compiled).
Otherwise, isn't it possible to patch the ELF entrypoint with an int3 instruction ? I haven't done an in-depth test but qemu-*-static seems to dump the inner process core when it hits the instruction and pwntools.Corefile is able to retrieve the environment variables.
To keep it simple, we could check that python on the remote host matches context.arch.
No, I'd prefer we not do this. The current implementation is ~= broken and really needs a redesign.
To keep it simple, we could check that python on the remote host matches context.arch.
Hellman's fixenv script is a subset of the functionality Pwntools provides, and also has a great many limitations. It's a cool script, but it's a uber hack and a major step backward.
possible to patch the ELF entrypoint
We can patch the entry point, but generally we don't have write access to the challenge binary and would need to create a copy.
Separately, this is generally unnecessary unless the binary terminates immediately without blocking on any input. You can just kill(SIGTRAP, ...) the process and get it to drop core (generally).
Ultimately, ssh.getenv and ssh.process.getenv need to be reworked to rely on Corefile, regardless of the mechanism for acquiring the corefile.