pwnable.kr - syscall
Table of Contents
Finding a primitive
Looking at the code, we find that it adds a syscall with the number NR_SYS_UNUSED
(223), and that its source code is as follows:
asmlinkage long
I assumed that the syscall-registering code is fine, and didn’t dive too deep into it. This will later be revealed to be true, but it’s an assumption for now.
At a glance, this seems like a simple function, copying bytes from in
to out
, barring a few stipulations. It doesn’t do any verification on the given pointers, and that allows using it as a worse version of memcpy
, with entirely user-controlled addresses.
It doesn’t allow copying buffers containing 0x00
(NULL
) bytes (due to strlen
checking for them), but bytes from 0x61
to 0x7a
can be copied, as long as they’re input with a 0x20
offset.
In short, we have an arbitrary write primitive on the kernel’s memory. We can also use it to read kernel memory, but it’s pretty constricted by the aforementioned NULL
-byte limitation.
Finding our target
First, we have to understand our end goal. We’re supposed to find a flag (string), where is it? Blindly running ls
on the system and looking around reveals:
Now our target is clear - we need to read the contents of the file /root/flag
, which is owned by the root user/group (UID/GID 0
), and is only allowed to be read by them.
Research
Given the primitive, it seems like all writeups I found on the internet1 chose the approach of giving the current process root permissions, then reading the file.
When attempting this on my own (before reading the writeups, of course), I thought of a different approach2 - there probably exists a function in the kernel, called somewhere during a call to read
, which checks if the current process has permissions to read a file (let’s call it check_file_permissions()
). What if we could just make that function return true
? We have arbitrary write.
So, I went searching.
Finding the real check_file_permissions
First, I found out what kernel we’re dealing with:
() ) #13 SMP Fri Jul 11 00:48:31 PDT 2014
)
Then, I opened up Elixir for easy searching, and picked v3.11.4
.
Looking at the files, the first thing that came to mind was to search the fs
(filesystem) folder for the read
function. Conveniently, the file fs/read_write.c
is there.
Among the first lines, I found this struct:
const struct file_operations generic_ro_fops = ;
.read
leads to do_sync_read
, which leads to a call to aio_read
, so let’s just inspect generic_file_aio_read
. It’s defined in mm/filemap.c
.
Reading the function and not diving too deep, I failed to find any clear check_file_permissions()
function. Attempting another strategy, I searched the file (simple Ctrl+F
) for perm
. That doesn’t find any clear function, but it does find 2 places with return -EPERM
.
Hmm, what if we just searched the fs
folder for that? There has to be some place like:
if
Elixir found a lot of references to EPERM
, but since I’m interested in filesystems specifically, I chose to look at fs/generic_acl.c
3 (assuming ACL stands for access control lists4, it sounded relevant enough).
In the file, inside generic_acl_set
, I found our coveted code section:
if
return -EPERM;
However, we don’t want to pass an ownership check. We want to pass a read permissions check.
You can use the next section to try this yourself, and see that just patching the ownership check doesn’t allow reading the file with
cat
.Interestingly enough, It doesn’t let you
chown
the file either. I wonder why.
Let’s see how it’s implemented:
/**
* inode_owner_or_capable - check current task permissions to inode
* @inode: inode being checked
*
* Return true if current either has CAP_FOWNER to the inode, or
* owns the file.
*/
bool
;
Looks like the function we actually want to override is inode_capable
.
Patching check_file_permissions
(Or inode_capable
, which is the real version of it - no spoilers in the title!)
Now, we have to make inode_capable
just return true
.
Finding the function’s address
The function is exported, so I checked /proc/kallsyms
5.
|
The kernel doesn’t allow reading
/proc/kallsyms
as a non-root user by default.
However, in this machine, that feature seems to be off:
Writing our payload
I initially assumed that this was an x86_64
machine, however, attempting to run the payload with x86_64
instructions failed.
Looking at the boot logs6, I found CPU: ARMv7 Processor [...]
. Definitely no x86 here.
Well, I don’t know much ARM at all, so I headed to Compiler Explorer to write a simple return true
program:
bool
Choosing an armv7
compiler, I could see the compiled opcodes on the right. My function looks like:
sub , , #8
str , [ #4]
,str , [
]mov , #1
add , , #8
bx
From this, I inferred that:
- Parameters are passed in
r0
,r1
(playing with the code revealed that it’s in that order). - The return value is passed in
r0
. - To end a function,
bx lr
is called, branching to the link register.
That was great news for me, since:
r0
will always have a truthy (non-zero) value wheninode_capable
is called (it should be a valid pointer).- Therefore, I just needed one opcode (
bx lr
) at the start of the function to end it.
To get the opcode bytes, I enabled Link to binary
in the output options. bx lr
is 0xe12fff1e
. No NULL
bytes, or even any bytes in the 0x61
-0x7a
range (affected by sys_upper
), which means I could send the bytes as they are.
Running the exploit
Now I just needed a small program to call the syscall and put my payload in its place:
int
Then all that was left is to compile it, run it, and get the flag:
The machine now doesn’t perform any file permission checks until it’s restarted again, but that’s not our problem 😄
And we’re done!
Thanks for reading,
beje
Addendum - How does the kernel actually implement file permission checks?
Remember inode_owner_or_capable
?
bool
Well, checking out where CAP_FOWNER
is defined, we can find CAP_DAC_OVERRIDE
. Check out its documentation:
/* Override all DAC access, including ACL execute access if
[_POSIX_ACL] is defined. Excluding DAC access covered by
CAP_LINUX_IMMUTABLE. */
That sound very relevant, and following its references, we see its use in fs/namei.c
’s generic_permissions
.
You can follow it to see what it actually does (and where overriding inode_capable
helped us). As a hint, note that when acl_permission_check
calls check_acl
, it will always return -EAGAIN
, since CONFIG_FS_POSIX_ACL
is not set:
|
# CONFIG_FS_POSIX_ACL is not set
# CONFIG_TMPFS_POSIX_ACL is not set
# CONFIG_NFS_V3_ACL is not set
Some examples:
- GitHub - sonysame/pwnable.kr_syscall
- pwnable.kr: syscall
- Pwnable Challenge: Syscall - Alkaline Security Blog
- GitHub - agamabergel/pwnable
- How to exploit the lack of __user space check in the Linux kernel | by Gergely Bod | Medium
Finding out that no one else used my approach why is I posted this writeup.
I initially just picked a random filesystem (ext4
), but in hindsight generic_acl
seems like a more reasonable choice.
man 5 acl
man 5 proc
I could’ve also used cat /proc/cpuinfo
.