pwnable.kr: rootkit (400)
We are given a Linux Kernel Module named rootkit
and a remote QEMU guest over
ssh. Inside the remote QEMU guest, there's a file named flag
which we can't
open. There's also rootkit.ko
which has the same hash digest as rootkit
.
Inspecting the syslog, there's a line which says rootkit: module license
'unspecified' taints kernel.
that's practically tells us that rootkit.ko
is
automatically loaded into the QEMU guest as it boots. To be 100% sure, we can
check that /etc/init.d/rcS
contains insmod /rootkit.ko
.
Since we don't get the source code for the given LKM, let's try reverse engineer it in IDA. Decompilation in IDA shows that:
int initmodule()
{
int v0; // eax@1
_DWORD *v1; // eax@1
int v2; // edx@1
int result; // eax@1
sct = -1050697696;
sys_open = vC15FA034;
sys_openat = vC15FA4BC;
sys_symlink = vC15FA16C;
sys_symlinkat = vC15FA4E0;
sys_link = vC15FA044;
sys_linkat = vC15FA4DC;
sys_rename = vC15FA0B8;
sys_renameat = vC15FA4D8;
wp();
v0 = sct;
*(_DWORD *)(sct + 20) = sys_open_hooked;
*(_DWORD *)(v0 + 1180) = sys_openat_hooked;
*(_DWORD *)(v0 + 332) = sys_symlink_hooked;
*(_DWORD *)(v0 + 1216) = sys_symlinkat_hooked;
*(_DWORD *)(v0 + 36) = sys_link_hooked;
*(_DWORD *)(v0 + 1212) = sys_linkat_hooked;
*(_DWORD *)(v0 + 152) = sys_rename_hooked;
*(_DWORD *)(v0 + 1208) = sys_renameat_hooked;
wp();
v1 = (_DWORD *)_this_module[2];
v2 = _this_module[1];
*(_DWORD *)(v2 + 4) = v1;
*v1 = v2;
result = 0;
_this_module[1] = &_this_module[1];
_this_module[2] = &_this_module[1];
return result;
}
It seems that the LKM hijack a set of system calls by means of overwriting
system call
table.
The affected system calls are open
, openat
, link
, linkat
, rename
,
renameat
. These are essentials system calls to read a file. Let's disassemble
one of the hook function.
sys_open_hooked proc near ; DATA XREF: initmodule+69o
.text:08000250
.text:08000250 arg_0 = dword ptr 8
.text:08000250 arg_4 = dword ptr 0Ch
.text:08000250 arg_8 = dword ptr 10h
.text:08000250
.text:08000250 push ebp
.text:08000251 mov ebp, esp
.text:08000253 push ebx
.text:08000254 sub esp, 0Ch
.text:08000257
.text:08000257 loc_8000257: ; DATA XREF: __mcount_loc:0800040Co
.text:08000257 call mcount
.text:0800025C mov edx, offset aFlag ; "flag"
.text:08000261 mov ebx, [ebp+arg_0]
.text:08000264 mov eax, ebx
.text:08000266 call strstr
.text:0800026B test eax, eax
.text:0800026D jnz short loc_800028C
.text:0800026F mov eax, [ebp+arg_8]
.text:08000272 mov [esp], ebx ; _DWORD
.text:08000275 mov [esp+8], eax ; _DWORD
.text:08000279 mov eax, [ebp+arg_4]
.text:0800027C mov [esp+4], eax ; _DWORD
.text:08000280 call ds:sys_open
.text:08000286
.text:08000286 loc_8000286: ; CODE XREF: sys_open_hooked+4Bj
.text:08000286 add esp, 0Ch
.text:08000289 pop ebx
.text:0800028A pop ebp
.text:0800028B retn
.text:0800028C ; ---------------------------------------------------------------------------
.text:0800028C
.text:0800028C loc_800028C: ; CODE XREF: sys_open_hooked+1Dj
.text:0800028C mov dword ptr [esp], offset aYouWillNotSeeT ; "You will not see the flag...\n"
.text:08000293 call printk
.text:08000298 or eax, 0FFFFFFFFh
.text:0800029B jmp short loc_8000286
The disassembly shows that the request is denied if there is a flag
substring
in the argument for the syscall, else it will fallback to the original system
call.
Okay, that's easy enough to fix. There is two ways that I thought of:
- We can try removing the LKM.
- We can just change the trigger for the hook function to something else other
than flag
.
- We just need to somehow restore the original system call for open/link/rename
which was overwritten.
The first solution does not work because the module does not clean the system
call table when it's unloaded. It doesn't even return anything so we can't
rmmod
. Well, at least we tried.
The second solution is easy to implement. We can just do the following:
sed -i rootkit.ko -e 's/rootkit/rootkis/g' # these line is needed so the module will not conflict with the original rootkit.ko
sed -i rootkit.ko -e 's/flag/flig/g'
Unfortunately, the first solution does not work because the second hook will
call the first hook instead of the original system call even if we bypass the
trigger. This is because the ds:sys_<func>
in hook function get its value
from the system call table as we've seen in the initmodule
's disassembly:
.init.text:080002D0 initmodule proc near ; DATA XREF: .gnu.linkonce.this_module:08000678o
.init.text:080002D0 push ebp ; Alternative name is 'init_module'
.init.text:080002D1 mov eax, ds:0C15FA034h ; sct is 0xC15FA020, sys_open is 0xC15FA034, which is sct + 20
.init.text:080002D6 mov ebp, esp
.init.text:080002D8 mov ds:sct, 0C15FA020h
.init.text:080002E2 mov ds:sys_open, eax
.init.text:080002E7 mov eax, ds:0C15FA4BCh
.init.text:080002EC mov ds:sys_openat, eax
.init.text:080002F1 mov eax, ds:0C15FA16Ch
.init.text:080002F6 mov ds:sys_symlink, eax
.init.text:080002FB mov eax, ds:0C15FA4E0h
.init.text:08000300 mov ds:sys_symlinkat, eax
.init.text:08000305 mov eax, ds:0C15FA044h
.init.text:0800030A mov ds:sys_link, eax
.init.text:0800030F mov eax, ds:0C15FA4DCh
.init.text:08000314 mov ds:sys_linkat, eax
.init.text:08000319 mov eax, ds:0C15FA0B8h
.init.text:0800031E mov ds:sys_rename, eax
.init.text:08000323 mov eax, ds:0C15FA4D8h
.init.text:08000328 mov ds:sys_renameat, eax
.init.text:0800032D xor eax, eax
.init.text:0800032F call wp
.init.text:08000334 mov eax, ds:sct
.init.text:08000339 mov dword ptr [eax+14h], offset sys_open_hooked
.init.text:08000340 mov dword ptr [eax+49Ch], offset sys_openat_hooked
.init.text:0800034A mov dword ptr [eax+14Ch], offset sys_symlink_hooked
.init.text:08000354 mov dword ptr [eax+4C0h], offset sys_symlinkat_hooked
.init.text:0800035E mov dword ptr [eax+24h], offset sys_link_hooked
.init.text:08000365 mov dword ptr [eax+4BCh], offset sys_linkat_hooked
.init.text:0800036F mov dword ptr [eax+98h], offset sys_rename_hooked
.init.text:08000379 mov dword ptr [eax+4B8h], offset sys_renameat_hooked
.init.text:08000383 mov eax, 1
.init.text:08000388 call wp
.init.text:0800038D mov eax, ds:__this_module+8
.init.text:08000392 mov edx, ds:__this_module+4
.init.text:08000398 mov [edx+4], eax
.init.text:0800039B mov [eax], edx
.init.text:0800039D xor eax, eax
.init.text:0800039F mov ds:__this_module+4, (offset __this_module+4)
.init.text:080003A9 mov ds:__this_module+8, (offset __this_module+4)
.init.text:080003B3 pop ebp
.init.text:080003B4 retn
.init.text:080003B4 initmodule endp
The third solution is implemented by changing where ds:sys_<func>
load its
value for old system call address, i.e. change to:
mov eax, <original system call address>
mov ds:sys_open, eax
The exact method to find the original system call address is left as exercise for the reader :).
After that, we load the module with insmod rootkit.ko
and then get the
content of flag
file.
Flag is: hidden because of pwnable.kr policy
Fun fact: Why use disassemble for half of the writeup when we can decompile? Apparently, decompilation with IDA 6.8 misses some important parts. For example, the decompilation for hook function trigger yield this code:
int __cdecl sys_open_hooked(int a1, int a2, int a3)
{
int result; // eax@2
const char *v4; // [sp+0h] [bp-10h]@0
const char *v5; // [sp+4h] [bp-Ch]@0
mcount();
if ( strstr(v4, v5) )
{
printk("You will not see the flag...\n");
result = -1;
}
else
{
result = sys_open(a1, a2, a3);
}
return result;
}
As you can see, it doesn't show the string flag
is loaded into any of the
local variables. It's weird that strstr(v4, v5)
is done without any
initialization on v4
and v5
. The dissassembly version is more complete so I
prefer the disassembly version.