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.

Related Posts

pwnable.kr: aeg (550)