DEV Community

Rishit Bansal
Rishit Bansal

Posted on

Reverse Engineering Keyboard Driver: Part 2 (Decompiling .NET applications)

Introduction

This post is a part of my series to reverse engineer the keyboard driver/Omen Light Studio application on my HP Omen Laptop and re-implement its functionality on Linux. In this post, I will be covering how to decompile .NET services/DLLs

Locating the Service

I have been using the Light Studio Application for a while, and one of its best features is the ability to set up dynamic "wave" lighting on your keyboard. In this setting, the lighting on the keyboard keeps changing dynamically to show a sort of wave animation. An intriguing behavior I have observed with this feature is that the animation works just once you have booted into Windows. During the boot process in Ubuntu, the lighting is set to the last set color from windows and remains static. This means that the animation is most likely set by a background service on windows which starts on boot and is not a feature of the keyboard on the hardware level.

So, I began by opening a task manager and looking for any relevant background services that are running. I found two of particular interest:

  • Omen Gaming Hub

Omen Gaming Hub Process

  • Light Studio Helper

Light Studio Helper Process

The second service looks more interesting, so I right-clicked on the service and selected "Open File Location", and found that it was located at C:\Program Files\HP\LightStudioHelper

We can see several files, but one which caught my eye was Newtonsoft.Json.dll. I instantly recognized it as a C# library (https://www.newtonsoft.com/json), as I have worked with it in the past. This is important, as this means that the application was likely to be written in .Net in C#.

Decompiling the Executable and DLL Files

Next, I looked for tools to decompile .NET applications. The top result was a free tool by Jetbrains called DotPeek. I began by opening the folder on DotPeek and it was able to decompile all the DLLs. The two results of most importance to us are the ones for LightStudioHelper and OmenFourZoneLighting:

The LightStudioHelper binary is what runs the background service. We start by looking at the Main() method in this class:

 private static int Main(string[] args)
    {
      string productVersion = Process.GetCurrentProcess().MainModule.FileVersionInfo.ProductVersion;
      Logger.logger.Info("");
      Logger.logger.Info("----- Log Start, version: " + productVersion);
      if (Program.ProcessCommands(args))
      {
        Logger.logger.Info(string.Format("----- program exits, returnCode = {0}", (object) Program._returnCode));
        return Program._returnCode;
      }
      if (Program.IsAnotherInstanceRunning("OLS_HELPER"))
      {
        Logger.logger.Info("----- program exits");
        return Program._returnCode;
      }
      Program.Cleanup();
      Program.CreateTimer();
      Program.CreateAndRunThread();
      Logger.logger.Info("----- program exits");
      return Program._returnCode;
    }
Enter fullscreen mode Exit fullscreen mode

This method first gets the version of the service, processes command line arguments, and then checks if another process of the OLS_HELPER service is running (not sure why yet) and then runs the Cleanup() method:

private static void Cleanup()
    {
      Logger.logger.Info("Cleanup()");
      TaskScheduler taskScheduler = new TaskScheduler("LightStudioHelperTemp");
      taskScheduler.Stop();
      taskScheduler.Delete();
      DirectoryInfo directoryInfo = new DirectoryInfo(Program.InstallDirTemp);
      if (!directoryInfo.Exists)
        return;
      directoryInfo.Delete(true);
    }
Enter fullscreen mode Exit fullscreen mode

The cleanup() method creates a TaskScheduler class, which is another user-defined class in the source code of the service.
The implementation for Stop() and Delete() can be seen in the source, but it's irrelevant as it just deals with killing any already running process of the LightStudioHelper. We concentrate on the CreateAndRunThread() method which is run next:

 private static void CreateAndRunThread()
    {
      Logger.logger.Info("CreateAndRunThread()");
      Thread thread = new Thread(new ParameterizedThreadStart(Program.ThreadLightingUpdate));
      thread.IsBackground = true;
      thread.Start();
      if (thread == null || !thread.IsAlive)
        return;
      thread.Join();
    }
Enter fullscreen mode Exit fullscreen mode

This method creates a new Thead which executes the ThreadLightingUpdate method in the background:

private static void ThreadLightingUpdate(object state)
    {
      Color[] colorArray = new Color[4];
      Logger.logger.Info("enter ThreadLightingUpdate()");
      while (Program._isRunning)
      {
        FourZoneLightingData zoneLightingData = LightStudioStorage<FourZoneLightingData>.ReadData();
        if (zoneLightingData != null && zoneLightingData.FourZoneColors != null)
        {
          bool flag = false;
          for (int index = 0; index < 4; ++index)
          {
            if (!flag && colorArray[index] != zoneLightingData.FourZoneColors[index])
              flag = true;
            colorArray[index] = zoneLightingData.FourZoneColors[index];
          }
          if (flag && FourZoneLighting.IsTurnOn())
          {
            Thread.Sleep(5);
            FourZoneLighting.SetZoneColors(zoneLightingData.FourZoneColors);
          }
        }
        Thread.Sleep(33);
      }
      Logger.logger.Info("leave ThreadLightingUpdate()");
    }

Enter fullscreen mode Exit fullscreen mode

This is the main loop of the thread(), and on looking at it, we already got a lot of clues on how the background service works:

  1. It maintains a 4 element array of Color, which is encouraging, as I know my keyboard has 4 configurable lighting zones, this might refer to the color of each zone!
  2. The program has a while loop that seems to read colors from some sort of storage (LightStudioStorage<FourZoneLightingData>.ReadData()), and then stores the color data in the 4 element color array. It maintains a flag variable to check if any of the regions has a different color. Finally, if the flag variable is set, and FourZoneLighting.IsTurnOn() is true (presumably checking if the keyboard lights are turned on), it calls FourZoneLighting.SetZoneColors to set the colors.

I went in a little side adventure in checking out the LightStudioStorage and where it stores data, and found that it is a MemoryMappedFile:

namespace CommonLib.SharedMemory
{
  public sealed class LightStudioStorage<T>
  {
    private static MemoryMappedFileHelper _mmfHelper = new MemoryMappedFileHelper(typeof (T).Name);

    private LightStudioStorage()
    {
    }

    public static void WriteData(T data)
    {
      if (LightStudioStorage<T>._mmfHelper == null)
        return;
      LightStudioStorage<T>._mmfHelper.WriteData<T>(data);
    }

    public static T ReadData()
    {
      T obj = default (T);
      if (LightStudioStorage<T>._mmfHelper != null)
        obj = LightStudioStorage<T>._mmfHelper.ReadData<T>();
      return obj;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This likely refers to some shared memory logic, and another process might be writing to this memory-mapped file and calculating colors based on an algorithm. For now, I stopped here, but this area might be interesting to look at in the future as well.

Moving on, I searched for the implementation of FourZoneLighting.SetZoneColors, and found it was implemented in OmenFourZoneLighting.dll:

public static bool SetZoneColors(Color[] zoneColors)
    {
      if (zoneColors.Length != 4)
        return false;
      byte[] returnData = (byte[]) null;
      int num = FourZoneLighting.Execute(131081, 2, 0, (byte[]) null, out returnData);
      Thread.Sleep(5);
      if (num == 0 && returnData != null)
      {
        byte[] inputData = returnData;
        returnData = (byte[]) null;
        for (int index = 0; index < 4; ++index)
        {
          inputData[25 + index * 3] = zoneColors[index].R;
          inputData[25 + index * 3 + 1] = zoneColors[index].G;
          inputData[25 + index * 3 + 2] = zoneColors[index].B;
        }
        num = FourZoneLighting.Execute(131081, 3, inputData.Length, inputData, out returnData);
      }
      return num == 0;
    }
Enter fullscreen mode Exit fullscreen mode

This file basically seems too transfer the colors to another data structure, inputData, and then passes them to Execute():

 private static int Execute(
      int command,
      int commandType,
      int inputDataSize,
      byte[] inputData,
      out byte[] returnData)
    {
      returnData = new byte[0];
      try
      {
        ManagementObject managementObject1 = new ManagementObject("root\\wmi", "hpqBIntM.InstanceName='ACPI\\PNP0C14\\0_0'", (ObjectGetOptions) null);
        ManagementObject managementObject2 = (ManagementObject) new ManagementClass("root\\wmi:hpqBDataIn");
        ManagementBaseObject methodParameters = managementObject1.GetMethodParameters("hpqBIOSInt128");
        ManagementBaseObject managementBaseObject1 = (ManagementBaseObject) new ManagementClass("root\\wmi:hpqBDataOut128");
        managementObject2["Sign"] = (object) FourZoneLighting.Sign;
        managementObject2["Command"] = (object) command;
        managementObject2["CommandType"] = (object) commandType;
        managementObject2["Size"] = (object) inputDataSize;
        managementObject2["hpqBData"] = (object) inputData;
        methodParameters["InData"] = (object) managementObject2;
        InvokeMethodOptions invokeMethodOptions = new InvokeMethodOptions();
        invokeMethodOptions.Timeout = TimeSpan.MaxValue;
        InvokeMethodOptions options = invokeMethodOptions;
        ManagementBaseObject managementBaseObject2 = managementObject1.InvokeMethod("hpqBIOSInt128", methodParameters, options)["OutData"] as ManagementBaseObject;
        returnData = managementBaseObject2["Data"] as byte[];
        return Convert.ToInt32(managementBaseObject2["rwReturnCode"]);
      }
      catch (Exception ex)
      {
        Console.WriteLine("OMEN Four zone lighting - WmiCommand.Execute occurs exception: " + ex?.ToString());
        return -1;
      }
    }
Enter fullscreen mode Exit fullscreen mode

This method seems to be doing the actual interaction with the hardware of the keyboard. I did a bit of research about ManagementObject and found that it's a class used to interact with WMI (Windows Management Instrumentation). WMI, specifically WMIACPI allows you to interact with the Bios and hardware devices, but more on this on the next blog post, for now, let us just treat this function as a black box which does some magic to set the colors of the keyboard.

Since now we have enough information on how the service works, I tried to implement everything in the OmenFourZoneLighting.dll file in my command line C# program for windows.

Rewriting the WMI Code in a C# program

I started by setting up a .NET console application on Rider and added the System.Drawing, and System.Management DLLs as assembly references from my system.

I copied most of the Code from the dotPeek decompiled result, and fixed some variable references, and wrote a CMD application which is available on this Github repository:


Logo

omen-cli

A CLI to customize your keyboard backlight 🚀
Explore the docs »

View Demo · Report Bug · Request Feature

🎯 Table of Contents

📖 About The Project

omen-cli is a lightweight CLI tools built in C# to customize keyboard backlights on HP Omen laptops similar to how Omen Light studio does.

Built With

✈️ Getting Started

To get a local copy up and running follow these simple steps.

Prerequisites

  • .NET Framework 4.8
  • Nuget.exe CLI
  • MSBuild.exe CLI

Installation

  1. Clone the repo
git clone https://github.com/thebongy/omen-cli.git
Enter fullscreen mode Exit fullscreen mode
  1. Install dependencies
nuget install .\CLI\packages.config -OutputDirectory packages
Enter fullscreen mode Exit fullscreen mode
  1. Run the following command from the root dir to build the project
MSBuild.exe
Enter fullscreen mode Exit fullscreen mode

🔧 Usage

To view all the options available, use the --help command:

ss-1


The set4 command is used to set 4 colors to…

Conclusion

In this post, we saw how to decompile a C# application, and then implemented is using .NET Framework. In the next post, I will research more into ACPI and WMI drivers for Linux, to get a better idea of how to implement this functionality on Linux.

Top comments (3)

Collapse
 
servalex profile image
Alex Servirog

This is a cool theme, I hope we'll see the third part!

Collapse
 
ashikka profile image
ashikka

@rishit This is super cool and informative! Thanks for sharing this. 🎉

Collapse
 
rishit profile image
Rishit Bansal

tysm @ashikka