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
- Light Studio Helper
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;
}
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);
}
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();
}
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()");
}
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:
- 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!
- 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 aflag
variable to check if any of the regions has a different color. Finally, if theflag
variable is set, andFourZoneLighting.IsTurnOn()
is true (presumably checking if the keyboard lights are turned on), it callsFourZoneLighting.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;
}
}
}
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;
}
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;
}
}
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:
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
- Clone the repo
git clone https://github.com/thebongy/omen-cli.git
- Install dependencies
nuget install .\CLI\packages.config -OutputDirectory packages
- Run the following command from the root dir to build the project
MSBuild.exe
๐ง Usage
To view all the options available, use the --help
command:
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)
This is a cool theme, I hope we'll see the third part!
@rishit This is super cool and informative! Thanks for sharing this. ๐
tysm @ashikka