2

Consider running the following Python code as root:

import os
f=os.open("/etc/shadow", os.O_RDONLY)
os.setuid(65535)
os.open(f"/proc/self/fd/{f}", os.O_RDONLY)

Here is a one-liner convenient for pasting:

python3 -c 'import os; f=os.open("/etc/shadow", os.O_RDONLY); os.setuid(65535); os.open(f"/proc/self/fd/{f}", os.O_RDONLY)'

Given the comment of proc_fd_permission, I would expect this code to succeed. However, I actually observe -EACCES. Why is this use of /proc/self/fd/N not permitted and what is the source code comment actually trying to convey?

Update: If the permission only applies to the symlink itself and not the target file, why can I open sockets and deleted files via /proc/self/fd/N? (e.g. exec 3>foo; echo hello >&3; rm foo; cat /proc/self/fd/3 prints hello)

Helmut Grohne
  • 6,578
  • 2
  • 31
  • 67
  • 1
    `f"/proc/self/fd/{f}"` will be a symbolic link to `/etc/shadow` which the process no longer has permission to open once it has dropped its privileges. – Ian Abbott Feb 24 '23 at 17:06
  • You still have permission to read the symbolic link: `link=os.readlink(f"/proc/self/fd/{f}"); print(link)` but that does not grant you permission to open the file that the link points to. – Ian Abbott Feb 24 '23 at 17:16
  • Of course, if you first resolve the link and then apply the permission of the target file, `-EACCES` is the natural outcome. But what is the benefit of `proc_fd_permission` and its special handling? If the link is just followed, then how does opening a deleted file work? Consider `exec 3>foo; echo hello >&3; rm foo; cat /proc/self/fd/3` which will print `hello`. – Helmut Grohne Feb 25 '23 at 18:40
  • 1
    `proc_fd_permission` is applied to inodes of directories `/proc/PID/fd`, not inodes of files/symlinks `/proc/PID/fd/*`, you got `-EACCES` most likely due to `proc_fd_access_allowed` (https://elixir.bootlin.com/linux/latest/source/fs/proc/base.c#L1752). You still can list files in directory `/proc/self/fd` after `os.setuid(65535)`, that's the purpose of `proc_fd_permission`. it is setup here (https://elixir.bootlin.com/linux/latest/source/fs/proc/base.c#L3591) – k1r1t0 Feb 26 '23 at 10:21
  • This latter comment of @k1r1t0 actually sheds some light on the issue. It highlights that the actual check is in `https://elixir.bootlin.com/linux/latest/source/fs/proc/base.c#L672` and that we require `ptrace` capability. That raises the next question: Why is my process unable to ptrace itself? – Helmut Grohne Feb 28 '23 at 05:15
  • @HelmutGrohne, let me explain. You can use `ptrace(PTRACE_TRACEME, 0)` to "trace yourself", but I can't understand what for... You might be rejected by `ptrace_may_access`not only due to lack of capability. It checks whether the process which is trying to access this inode has the same group as the owner, if check fails -- go on, next check whether owner's *uids and *gids are the same as caller's if fails -- go on, next it checks for `CAP_SYS_PTRACE` if fails -- return `-EPERM`, if one of the last two didn't fail check for `SUID_DUMP_USER`. – k1r1t0 Feb 28 '23 at 16:40
  • @HelmutGrohne, my pre-previous comment is likely false, I misunderstood the current thread, so was wrong about `-EACCESS`. This is related to permission check for `/etc/shadow`, not for `/proc/self/fd/N`, sorry for that. – k1r1t0 Feb 28 '23 at 16:49

1 Answers1

-1

Inside your program, open files are represented as integer file descriptors. These are the filenames of the entries in the directory: /proc/self/fd. The kernel's /proc/ ABI helps you debug your system by displaying what those file descriptors refer to using the conventions of symlinks. But those symlinks are not equivalent to the open file descriptor.

$ ls -l /proc/self/fd
total 0
lrwx------. 1 tinkerer tinkerer 64 Mar 10 08:19 0 -> /dev/pts/0
lrwx------. 1 tinkerer tinkerer 64 Mar 10 08:19 1 -> /dev/pts/0
lrwx------. 1 tinkerer tinkerer 64 Mar 10 08:19 2 -> /dev/pts/0
lr-x------. 1 tinkerer tinkerer 64 Mar 10 08:19 3 -> /proc/230669/fd

If you close a file descriptor, there is no guarantee you can open it again. You have to satisfy the kernel's permission model to do that at the time you attempt to open the file. When you change UID, you should expect the permission model to view the open request differently.

If you want a second file descriptor to point to an already open file (descriptor), you should use the os.dup() method.

Tinkerer
  • 865
  • 7
  • 9
  • If I had to satisfy the kernel's permission model in all cases, it would be impossible to open deleted files or network sockets via `/proc/self/fd/...` both of which are in fact possible. As such that statement is relatively obviously wrong and with it, your answer becomes relatively useless. It also is obvious that `dup2` is the way to go if possible. That's not the question about that. It just is quite hard to shoehorn `dup2` into existing ELF binaries. Hence the question of "fooling" `open`. – Helmut Grohne Mar 14 '23 at 06:45