Hacking the Weather Station – Part 3

Hacking the Weather Station – Part 3

In HACKING THE WEATHER STATION – PART 2 I mentioned I needed to capture the reading and writing to the weather station over USB and that I’d started to play with EasyHook. I described roughly how EasyHook works but in this part of the series I’ll explain how I hooked the I/O operations in WeatherSmart in a little more detail.

The first step was to define an object that can be used in remote procedure calls (RPC) between my application and the WeatherSmart application. This should be descended from MarshalByRefObject which is pretty standard practice for remoting.

I defined methods for each of the hooks to basically dump to the console, I also added a way of filtering the handles because I only cared about I/O operations for the USB device and not anything else. What I ended up with is something like this:

public class MonitorInterface : MarshalByRefObject
{
	private Dictionary<IntPtr, string> handles;

	public MonitorInterface()
	{
		handles = new Dictionary<IntPtr, string>();
	}

	public void IsInstalled(int pid)
	{
		Console.WriteLine("Hooks have been installed in target: {0}.\r\n", pid);
	}

	public void Ping()
	{
	}

	public void OnCreateFile(int pid, IntPtr fileHandle, string fileName)
	{
		if (!fileName.StartsWith(@"\\?\hid", StringComparison.OrdinalIgnoreCase))
			return;

		handles[fileHandle] = fileName;

		Console.WriteLine("[CreateFile] PID: {0}, Handle: {1}, Filename: {2}", pid, fileHandle, fileName);
	}

	public void OnReadFile(int pid, IntPtr fileHandle, uint bytesRead, byte[] buffer, bool overlapped)
	{
		var builder = new StringBuilder();
		var queue = new Queue<byte>(buffer);

		while (queue.Count > 0)
		{
			var popCount = 0;

			if (queue.Count > 10)
			{
				popCount = 10;
			}
			else
			{
				popCount = queue.Count;
			}

			var line = "\r\n    ";

			for (var i = 0; i < popCount; i++)
			{
				var b = queue.Dequeue();

				line += b.ToString("X2") + " ";
			}

			builder.Append(line);
		}

		Console.WriteLine("[ReadFile]\r\n  Timestamp: {4}\r\n  PID: {0}\r\n  Handle: {1}\r\n  Size: {2}\r\n  Buffer: {3}\r\n  Overlapped: {5}", pid, fileHandle, bytesRead, builder.ToString(), DateTime.Now.ToString("hh:mm:ss.fff"), overlapped);
	}

	public void OnWriteFile(int pid, IntPtr fileHandle, string fileName, byte[] buffer)
	{
		var builder = new StringBuilder();
		var queue = new Queue<byte>(buffer);

		while (queue.Count > 0)
		{
			var popCount = 0;

			if (queue.Count > 10)
			{
				popCount = 10;
			}
			else
			{
				popCount = queue.Count;
			}

			var line = "\r\n    ";

			for(var i = 0; i < popCount; i++)
			{
				var b = queue.Dequeue();

				line += b.ToString("X2") + " ";
			}

			builder.Append(line);
		}

		Console.WriteLine("[WriteFile]\r\n  Timestamp: {4}\r\n  PID: {0}\r\n  Handle: {1}\r\n  Size: {2}\r\n  Buffer: {3}", pid, fileHandle, buffer.Length, builder.ToString(), DateTime.Now.ToString("hh:mm:ss.fff"));
	}

	public void OnOverlapped(int pid, IntPtr fileHandle, IntPtr internalLow, IntPtr internalHigh, long offset, IntPtr eventHandle)
	{
		Console.WriteLine("[Overlapped]");
		Console.WriteLine("  Low: {0}", internalLow);
		Console.WriteLine("  High: {0}", internalHigh);
		Console.WriteLine("  Offset: {0}", offset);
		Console.WriteLine("  Event Handle: {0}", eventHandle);
	}
}

I could then start work on the injection library which would be the part that’s pushed into WeatherSmart and acts as the proxy between the Windows API actual, and the application, whilst also reporting back to me.

The injection library is based around a class called Main which implements the EasyHook IEntryPoint and provides the actually hooking functionality in the immediate process which looks something like this:

public partial class Main : EasyHook.IEntryPoint
{
	public readonly FileMon.FileMonInterface Interface;
	public readonly Dictionary<IntPtr, string> HandleFilenames;

	private LocalHook _createFileHook;
	private LocalHook _writeFile;
	private LocalHook _readFile;

	public Main(RemoteHooking.IContext context, string channelName)
	{
		Interface = RemoteHooking.IpcConnectClient<FileMon.FileMonInterface>(channelName);
		HandleFilenames = new Dictionary<IntPtr, string>();

		Interface.Ping();
	}

	public void Run(RemoteHooking.IContext InContext, String InChannelName)
	{
		HookCreateFile();
		HookWriteFile();
		HookReadFile();

		Interface.IsInstalled(RemoteHooking.GetCurrentProcessId());
		RemoteHooking.WakeUpProcess();

		// Wait for host process termination...
		try
		{
			while (true)
			{
				Thread.Sleep(250);
			}
		}
		catch
		{
		}
	}

	private void HookCreateFile()
	{
		try
		{
			_createFileHook = LocalHook.Create(
				LocalHook.GetProcAddress("kernel32.dll", "CreateFileW"),
				new CreateFileDelegate(CreateFile_Hooked),
				this
			);
			_createFileHook.ThreadACL.SetExclusiveACL(new int[] { 0 });
		}
		catch (Exception ex)
		{
			Interface.OnException(ex.ToString());
		}
	}

	private void HookWriteFile()
	{
		try
		{
			_writeFile = LocalHook.Create(
				LocalHook.GetProcAddress("kernel32.dll", "WriteFile"),
				new WriteFileDelegate(WriteFile_Hooked),
				this
			);
			_writeFile.ThreadACL.SetExclusiveACL(new int[] { 0 });
		}
		catch (Exception ex)
		{
			Interface.OnException(ex.ToString());
		}
	}

	private void HookReadFile()
	{
		try
		{
			_readFile = LocalHook.Create(
				LocalHook.GetProcAddress("kernel32.dll", "ReadFile"),
				new ReadFileDelegate(ReadFile_Hooked),
				this
			);
			_readFile.ThreadACL.SetExclusiveACL(new int[] { 0 });
		}
		catch (Exception ex)
		{
			Interface.OnException(ex.ToString());
		}
	}
}

I discussed the general concept of the way EasyHook works in the last part so I won’t explain them all here, instead I’ll just show you what I used for CreateFile for example:

partial class Main
{
	[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode, SetLastError = true)]
	delegate IntPtr CreateFileDelegate(
		string fileName,
		uint desiredAccess,
		uint shareMode,
		IntPtr securityAttributes,
		uint creationDisposition,
		uint flagsAttributes,
		IntPtr templateFile
	);

	[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, CallingConvention = CallingConvention.StdCall)]
	static extern IntPtr CreateFile(
		string fileName,
		uint desiredAccess,
		uint shareMode,
		IntPtr securityAttributes,
		uint creationDisposition,
		uint flagsAttributes,
		IntPtr templateFile
	);

	static IntPtr CreateFile_Hooked(
		string fileName,
		uint desiredAccess,
		uint shareMode,
		IntPtr securityAttributes,
		uint creationDisposition,
		uint flagsAttributes,
		IntPtr templateFile
	)
	{
		Main main = (Main)HookRuntimeInfo.Callback;
		IntPtr handle = CreateFile(
			fileName,
			desiredAccess,
			shareMode,
			securityAttributes,
			creationDisposition,
			flagsAttributes,
			templateFile
		);

		try
		{
			main.HandleFilenames[handle] = fileName;
			main.Interface.OnCreateFile(RemoteHooking.GetCurrentProcessId(), handle, fileName);
		}
		catch (Exception ex)
		{
			main.Interface.OnException(ex);
		}

		return handle;
	}
}

Note in CreateFile_Hooked how I call through to the RPC object, this then gets sent from the hooked application to the injector application with all the information I need.

Finally I created the main injector application, which would just be a console application for simplicities sake. Within it I created an instance of the remoting object, as an IPC server, the injection library would talk to this, for example:

RemoteHooking.IpcCreateServer<MonitorInterface>(ref _channelName, WellKnownObjectMode.Singleton);

I could inject into a running instance of an application but instead I preferred to let the injection library start and manage that for me, so to do that I did:

var targetExe = @"C:\Program Files (x86)\WeatherSmart\WeatherSmart.exe";
var injectionLibrary = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "FileMonInject.dll");

RemoteHooking.CreateAndInject(targetExe, string.Empty, 0, InjectionOptions.DoNotRequireStrongName, injectionLibrary, injectionLibrary, out targetPid, _channelName);

Once I’d ironed out any small coding errors and bugs whenever I started my application it would launch WeatherSmart and I’d end up with a dump of information from it every time it tried any file I/O, for example:

Created and injected into process: 7252
Press any key to exit...
FileMon has been installed in target: 7252.

[CreateFile] PID: 7252, Handle: 1556, Filename: \\?\hid#vid_046d&pid_c52f&mi_01&col01#7&1061396a&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
[CreateFile] PID: 7252, Handle: 1556, Filename: \\?\hid#vid_046d&pid_c52f&mi_01&col02#7&1061396a&0&0001#{4d1e55b2-f16f-11cf-88cb-001111000030}
[CreateFile] PID: 7252, Handle: 1556, Filename: \\?\hid#vid_046d&pid_c52f&mi_01&col03#7&1061396a&0&0002#{4d1e55b2-f16f-11cf-88cb-001111000030}
[CreateFile] PID: 7252, Handle: 1556, Filename: \\?\hid#vid_047f&pid_0115&col01#6&17d5d770&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
[CreateFile] PID: 7252, Handle: 1556, Filename: \\?\hid#vid_047f&pid_0115&col02#6&17d5d770&0&0001#{4d1e55b2-f16f-11cf-88cb-001111000030}
[CreateFile] PID: 7252, Handle: 1556, Filename: \\?\hid#vid_047f&pid_0115&col03#6&17d5d770&0&0002#{4d1e55b2-f16f-11cf-88cb-001111000030}
[CreateFile] PID: 7252, Handle: -1, Filename: \\?\hid#vid_046d&pid_c52f&mi_00#7&2cee2d4&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
[CreateFile] PID: 7252, Handle: 1520, Filename: \\?\hid#vid_10c4&pid_8468#7&c0108cd&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
[WriteFile]
  Timestamp: 09:13:52.274
  PID: 7252
  Handle: 1520
  Size: 61
  Buffer: 
    02 09 01 10 09 1A 09 0D 34 02 
    80 00 00 00 00 00 00 00 00 00 
    00 00 00 00 00 00 00 00 00 00 
    00 00 00 00 00 00 00 00 00 00 
    00 00 00 00 00 00 00 00 00 00 
    00 00 00 00 00 00 00 00 00 00 
    00 
[ReadFile]
  Timestamp: 09:13:52.295
  PID: 7252
  Handle: 1520
  Size: 61
  Buffer: 
    01 05 F0 01 00 00 F1 07 34 42 
    02 17 06 18 43 01 F0 01 08 44 
    02 17 06 18 46 3C 00 27 47 48 
    00 00 48 27 D9 07 27 49 28 08 
    07 27 98 00 07 00 0C 4C 00 0D 
    06 15 4E 00 00 00 12 03 0A 55 
    00 
  Overlapped: True

Of course dumping it into the console wasn’t very useful so I quickly piped it to disk so I could read the results at leisure and begin picking apart the byte streams and comparing it to the specs Tycon sent me (if you recall).

So after many nights of toil I finally cracked it! I can now read from the WH2310 with impunity.

I contacted the Cumulus author offering my help but he seemed too busy so sadly that won’t be reading these devices anytime soon, however a guy on the WeeWx project, Matt Wall, was keen as he’d been working on this too but in Python and I sent him what I’d done. He was able to then get a working driver together for it as you can see here which will save anyone else a lot of our pain. But then where is the fun in that?

You can download the injection project code below.

1 Comment

  1. Dave Clapham · 22nd December 2016 Reply

    Thanks for that and for persevering. I’ve now got my FineOffset WH2310 running on my Pi instead of the PC using the new driver – ITAREE3 on wunderground.com
    Cheers
    Dave
    infinityab – Github
    PVoutput – Daves SMA

Leave a Reply