Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not working with PyInstaller #3

Open
vdun opened this issue Dec 13, 2022 · 4 comments
Open

Not working with PyInstaller #3

vdun opened this issue Dec 13, 2022 · 4 comments

Comments

@vdun
Copy link

vdun commented Dec 13, 2022

# cat h.py
print('hello')
# pyinstaller -F -c h.py
...
# ulexecve ./dist/h
[1512371] Cannot open PyInstaller archive from executable (/usr/bin/python3.10) or external archive (/usr/bin/python3.10.pkg)
@vdun vdun changed the title Now working with PyInstaller Not working with PyInstaller Dec 13, 2022
@gvb84
Copy link
Contributor

gvb84 commented Dec 18, 2022

Quickly debugging this makes it look like pyinstaller has a bootloader that resolves the calling binary (that it tries to extract the pydata segment from) via /proc/self/exe as can be seen in pyi_path.c.

The --fallback method that uses memfd_create() also will not work for the same reasons. Output looks like:

$ ulexecve --fallback ./dist/h
[3051] Cannot open PyInstaller archive from executable (/memfd: (deleted)) or external archive (/memfd: (deleted).pkg)

We may be able to solve this by using a prctl() call with PR_SET_MM_EXE_FILE. Based on the manpage the kernel would require us to unmap everything else first so this might be not as easy as we would hope it to be. This will also severely restrict us as it is rather likely we get an EPERM back from that call as one can only set this option if the caller has the CAP_SYS_RESOURCE capability.

I'll see if I have some time in the next few weeks to attempt to patch this but it's already clear to me that any binary produced by PyInstaller will not be as easily executable in userland due to these constraints that are hard to work around and, in some cases, simply can't be worked around.

@gvb84
Copy link
Contributor

gvb84 commented Feb 26, 2023

There are two other ways this can work. Both however require access to a writable filesystem though.

The first option is to extract the embedded archive from the PyInstaller binary, write this all to temporary storage, and then name the binary to be executed such that we can also create the binary.pkg version. If we then set _MEIPASS2 we trick the PyInstaller binary into thinking it has already unpacked its embedded archive. This is all a lot of work, we would have to write a ton of things to temporary storage although we might be able to do it by using memfds for all files and simply only writing symlinks to disk.

The second option is less reliable in very restricted scenario's but a whole lot easier. PyInstaller uses /proc/self/exe so we can attempt to replace, braindead, in the binary /proc/self/exe with a path we control that points to the binary again and not to the python executable. Given that we don't want to reassemble the ELF and all the instructions completely the easiest way is to come up with some random path that is just as long as the string /proc/self/exe. Assuming we can write to /tmp then something like /tmp/XXXXXXXXX can work. We symlink then from that filename to the memfd created binary that is only in memory and then we can execute from there.

Both options will leave at minimum a bunch of symlinks behind and the first option probably also a binary that we copied to a different location to make it all work. So we would need to fork() as well and have a watchdog process so that after the child is done doing a userland execution to unlink the files we created.

I got a proof of concept working for the /proc/self/exe trick but I want to clean it up a bit and test it on both py2/py3 and then push it out. But so far it looks like this:

gvb@caladan:~/src/ulexecve$ cat ./tmp/dist/h  | ./ulexecve.py -
[5064] Cannot open PyInstaller archive from executable (/usr/bin/python2.7) or external archive (/usr/bin/python2.7.pkg)
gvb@caladan:~/src/ulexecve$ cat ./tmp/dist/h  | ./ulexecve.py --pyi-fallback -
hello
gvb@caladan:~/src/ulexecve$

@gvb84
Copy link
Contributor

gvb84 commented Feb 27, 2023

The workaround as mentioned above with the /proc/self/exe trick got merged and can be used via --pyi-fallback. A new version was also published (1.3) on pypi.org.

@gvb84
Copy link
Contributor

gvb84 commented Feb 27, 2023

@vdun Can you test if in your use cases of ulexecve the workaround trick as just released alleviates some of the pain?

I'm not a fan of implementing either one of the other options as the first one can only be used under high privilege (CAP_SYS_RESOURCE) anyway and the second is a chunk of work, very specific to PyInstaller that can break with any new upgrade at the moment.

And anyone using some form of obfuscation on a PyInstaller binary can still prevent it from being run via ulexecve. But most vanilla binaries should work fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants