This is a followup to this blog post, where the author corrects tw_cli
to read the effective uid of a process instead of the real uid.
We have a use case where we want an external monitoring system to run as an unprivileged user, and still monitor parts of the system that are more privileged (3ware RAID status is one of them). The system works great under IA-32; we were able to successfully follow the steps in the above blog post to hack tw_cli
to allow an unprivileged user to read the status of the system’s RAID.
We hit a problem with the steps the author described after we migrated a server to x86_64 architecture. tw_cli has a check to make sure it’s running on the correct architecture as well. And of course, repeating the exact process on x86_64 was futile, since it’s compiled differently.
Before I begin this, I’m going to reiterate the following warning:
If you do any of these hacks, be sure that you do NOT install thetw_cli
program itself setuid root; usesudo
, or another wrapper that filters user access to runningtw_cli
as root. If you do not take appropriate precautions, any user will be able to runtw_cli
, bugs and all, and have all the powers of root while doing so!
First, I wanted to know exactly what was going on when I tried to run tw_cli
as an unprivileged user. For that, I turned to strace:
$ sudo -u monitoring-user strace -vvtf -s 1024 ./tw_cli info c4
This is the most important bit I got back:
07:54:39 getuid() = 102
07:54:39 brk(0x246a000) = 0x246a000
07:54:39 brk(0x245d000) = 0x245d000
07:54:39 write(2, "Error: (CLI:001) Only root/administrator can run this program.\n", 63Error: (CLI:001) Only root/administrator can run this program.
) = 63
According to strace, the system ran getuid(), found that my current uid is 102, and bailed out due to its internal checking. I proceeded to use gdb to see if I could get any more information.
$ sudo -u monitoring-user gdb ./tw_cli
So gdb
has a pretty useful function called catch
, where you can catch different types of functions and have it break you out. I set a catch on getuid
, to inspect the stack frame where it calls that function:
(gdb) catch syscall getuid
Catchpoint 1 (syscall 'getuid' [102])
I ran the program with my catch statement set:
(gdb) r
Starting program: /opt/monitor/sbin/tw_cli
Catchpoint 1 (call to syscall 'getuid'), 0x00000000005a4099 in ?? ()
So it caught my call to getuid()
. Look at that address. That’s the address in the program where the getuid()
call is located. Wouldn’t it be nice if we could skip this getuid call()
, or skip the conditional where it checks? I disassembled the source to find this location:
$ objdump -d ./tw_cli >tw.dump
So now I had a dump of the disassembled program. Searching for 5a4099
yielded:
5a4090: 48 c7 c0 66 00 00 00 mov $0x66,%rax
5a4097: 0f 05 syscall
5a4099: c3 retq</pre>
Here, the system is moving 0x66
into %RAX
, and then doing a syscall on it (syscall pulls the value from the %RAX
).
So what’s going on here? This is my getuid()
call, in its disassembled form. It is throwing 0x66 at the operating system (number for getuid()
), and then returning the value. How do I know that 0x66
is getuid
? Well, if you browse the linux source (/arch/x86/include/asm/unistd_64.h
, Linux 3.2 in my case), you will see the following code:
#define __NR_getuid 102 __SYSCALL(__NR_getuid, sys_getuid)
0x66
converts to 102 decimal.
One problem though, is that this seemed to be a function due to the use of retq
(returning to the address on the top of the stack.) So I could either hack this function, or hack something around where the caller was calling the function.
I grabbed a backtrace from gdb
and inspected it:
Catchpoint 1 (call to syscall 'getuid'), 0x00000000005a4099 in ?? ()
(gdb) bt
#0 0x00000000005a4099 in ?? ()
#1 0x0000000000484cb8 in ?? ()
#2 0x000000000044fdde in ?? ()
#3 0x0000000000578371 in ?? ()
#4 0x000000000040018a in ?? ()
#5 0x00007fff274ee758 in ?? ()
#6 0x0000000000000000 in ?? ()
0x5a4099
(#0) was where I just was in that function. I looked further back at 0x484cb8
(#1).
484cb3: e8 d8 f3 11 00 callq 0x5a4090
484cb8: 85 c0 test %eax,%eax
484cba: 0f 94 c0 sete %al
484cbd: 88 45 e7 mov %al,-0x19(%rbp)
484cc0: 80 7d e7 00 cmpb $0x0,-0x19(%rbp)
484cc4: 75 57 jne 0x484d1d
Here’s where 0x5a4090
is called. I looked down towards the cmpb
and the jne. I was pretty sure that this cmpb
was where it was testing if we were root, and the jne
would jump if we were root. (I could have tested this assumption by running it as root in gdb
using the ni
(next instruction) command). I wanted to change this so it would jump no matter what. I grabbed a copy of shed
, and ran it against my binary:
$ shed ./tw_cli
I should note that in shed
, I searched for 80 7d e7 00 75 57
(the last two lines of the above code). I got the following back:
00543936: 80 128 200 10000000
00543937: } 7D 125 175 01111101
00543938: E7 231 347 11100111
00543939: 00 000 000 00000000
00543940: u 75 117 165 01110101
00543941: W 57 087 127 01010111
So! Using my prior knowledge of assembler, I already knew that 0x75
is the jne
command, and was informed that 0xeb
is the jmp
command (easily searchable via your favorite search engine). I changed the 75
to eb
:
00543936: 80 128 200 10000000
00543937: } 7D 125 175 01111101
00543938: E7 231 347 11100111
00543939: 00 000 000 00000000
00543940: EB 235 353 11101011
00543941: W 57 087 127 01010111
Testing…
$ sudo -u monitoring-user ./tw_cli info c4
Unit UnitType Status %RCmpl %V/I/M Stripe Size(GB) Cache AVrfy
——————————————————————————
u0 RAID-10 OK - - 64K 931.303 ON OFF
Great success!