# LyMemory **Repository Path**: lyshark/LyMemory ## Basic Information - **Project Name**: LyMemory - **Description**: Case study of CR read and write based on IOCTL implementation, helping readers understand the principles and communication mechanisms of memory read and write operations - **Primary Language**: C - **License**: GPL-3.0 - **Default Branch**: main - **Homepage**: https://github.com/lyshark/LyMemory - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-23 - **Last Updated**: 2026-07-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: kernel, Windows, ioctl, demo ## README
![image](https://user-images.githubusercontent.com/52789403/210537145-bbf8cc74-64e7-4477-bafd-109437de6131.png)
A Windows memory read-write driver developed in C language, which relies on the native kernel API to achieve unified memory access for processes. The driver integrates multi-level offset addressing, remote memory management, module base address query, kernel address resolution functions, and is equipped with a complete IOCTL communication protocol and a C++client. It completes cross memory read and write encapsulation, is native to WoW64 environment, and has built-in exception handling and CR0 write protection temporary control mechanism. The project can be applied to code injection technology analysis, Rootkit malicious feature detection, memory forensics, and research on kernel and application layer communication mechanisms. - Selection of compilation environment:Microsoft Visual Studio Ultimate 2013 - Kernel SDK development library:Windows Driver Kit for Windows 8.1 **Warning:** This project belongs to the learning case of underlying Windows kernel technology and is only used as a technical research carrier to understand the interaction logic between kernel drivers and client, as well as the underlying principles of memory read and write. ## Installing and uninstalling drivers This code utilizes the LyCacheLib library to implement step-by-step installation, startup, communication connection, and uninstallation cleaning of the LyMemory kernel driver in functions. The main function completes the entire process of driver loading and release, establishing the communication foundation between the program and the kernel driver. The program requires administrator privileges to run. ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") BOOL InstallDriver(LyMemoryLib Memory) { char szSysFile[MAX_PATH] = { 0 }; char szSvcLnkName[] = "LyMemory";; BOOL ref = FALSE; DWORD index = 0; Memory.GetAppPath(szSysFile); strcat_s(szSysFile, "LyMemory.sys"); printf("Drive Path: %s \n", szSysFile); index = index + 1; ref = Memory.Install(szSysFile, szSvcLnkName, szSvcLnkName); printf("Installation status: %d \n", ref); index = index + 1; ref = Memory.Start(); printf("Startup status: %d \n", ref); index = index + 1; ref = Memory.Open("\\\\.\\LyMemory"); printf("Open status: %d \n", ref); index = index + 1; if (index == 4 && ref == TRUE) { return TRUE; } return FALSE; } BOOL RemoveDriver(LyMemoryLib Memory) { BOOL ref = 0; ref = Memory.Stop(); printf("Closed status: %d \n", ref); ref = Memory.Remove(); printf("Remove status: %d \n", ref); return ref; } int main(int argc, char* argv[]) { LyMemoryLib DriveControl; BOOL ref = InstallDriver(DriveControl); if (ref == TRUE) { printf("[*] Driver loaded\n"); } RemoveDriver(DriveControl); system("pause"); return 0; } ``` After compiling the above code and running it with administrator privileges, the driver will automatically load and output the information shown in the following figure on the debugging board image ## Set process binding If read-write functions are needed, the first step is to set the process PID binding. Usually, the process PID can be bound by passing it in through the Set PID (WORD Pid) function. Once the process is bound, it does not need to be opened again in the future, and it can also prevent multiple detached attachments from causing application layer exceptions. If you need to use PID setting, you can write it this way ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } system("pause"); return 0; } ``` Running the above code will automatically bind to the process and output the binding status, as shown in the following figure image ## Kernel Read Module Base Address At present, the process has been attached to the driver. At this point, 'GetModelAddress()' can be called to obtain the base address of a specific module within the process. This function receives a module name ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } DWORD64 user32 = DriveControl.GetModuleAddress("user32.dll"); printf("user32 = 0x%p \n", user32); DWORD64 kernel32 = DriveControl.GetModuleAddress("kernel32.dll"); printf("kernel32 = 0x%p \n", kernel32); system("pause"); return 0; } ``` Compile and run the above code, then retrieve the module base addresses of 'user32.dll' and 'kernel32. dll' from the attached process. The output effect is shown below image ## Take the base address of the kernel function Similar to obtaining the base address of an application layer module, the function 'Gets System Routine Address' can be used to obtain the memory base address of a specific exported function in the kernel module. ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } CHAR *SzFunction[3] = { "NtReadFile", "NtClose", "NtSetEvent" }; for (size_t i = 0; i < 3; i++) { DWORD64 ptr = DriveControl.GetSystemRoutineAddress(SzFunction[i]); printf("Function = %s | Addr = 0x%p \n", SzFunction[i], ptr); } system("pause"); return 0; } ``` Run the code snippet shown above to automatically retrieve the memory addresses of three functions: 'NtReadFile', 'NtClose', and 'NtSetEvent'. The output effect is shown below image ## Allocate and release heap space Opening up a segment of memory in the peer memory can be achieved by calling the 'CreateRemoteMemory' function, while freeing heap space can be achieved by calling the 'DeleteRemoteMemory' function. By default, the allocated space comes with read-write execution properties. ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } DWORD64 address = DriveControl.CreateRemoteMemory(1024); printf("[+] Allocated memory = 0x%p \n", address); BOOL del = DriveControl.DeleteRemoteMemory(address, 1024); if (del == TRUE) { printf("[-] Memory Space 0x%p Free \n", address); } system("pause"); return 0; } ``` After running the above code snippet, the address of 'address' will be allocated in the peer memory, and it will be automatically released after allocation. The output effect is shown below image ## Read/write memory integer type The reading of integer types can be done by calling 'ReadCacheDWORD' to read 32-bit integers, while calling 'ReadCacheDWORD64' will read 64 bit integer types ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } DWORD read_value = 0; BOOL read_flag = DriveControl.ReadMemoryDWORD(0x0188F828, &read_value); if (read_flag == TRUE) { printf("[*] Read 32-bit data = %d \n", read_value); } DWORD64 read64_value = 0; BOOL read64_flag = DriveControl.ReadMemoryDWORD64(0x0188F828, &read64_value); if (read64_flag == TRUE) { printf("[*] Read 64-bit data = %d \n", read64_value); } system("pause"); return 0; } ``` Compile and run the above code snippet, and the integer type data at '0x0188F828' will be read. The output result is shown below image Write integer types, call 'WriteCacheDWORD' to write 32-bit integers, call 'WriteCacheDWORD64' to write 64 bit integers ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } BOOL write32 = DriveControl.WriteMemoryDWORD(0x0188F828, 1000); if (write32 == TRUE) { printf("[+] Write data completed \n"); } BOOL write64 = DriveControl.WriteMemoryDWORD64(0x0188F828, 2000); if (write64 == TRUE) { printf("[+] Write data completed \n"); } system("pause"); return 0; } ``` Compile and run the code, and write '1000' and '2000' to the target process respectively. The code output effect is shown in the following figure image ## Read/write memory byte set Read and write byte sets in memory can call the 'Readmemorybytes' function, while write byte sets can call the' Writememorybytes' function ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } // Read byte set BYTE buffer[8] = { 0 }; BYTE* bufferPtr = buffer; BOOL flag = DriveControl.ReadMemoryBytes(0x401000, &bufferPtr, sizeof(buffer)); if (flag == TRUE) { for (int x = 0; x < 8; x++) { printf("[+] Read bytes: 0x%x \n", buffer[x]); } } system("pause"); return 0; } ``` By running the above code snippet, you can start reading the byte set at memory 0x401000, read 8 bytes backwards, and store them in the buffer. The output effect is shown below image Writing a byte set is basically the same as reading it, and the function 'WriteCachebytes' is used to write byte set data. Writing requires passing a defined byte array ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } BYTE writebuff[4] = { 0x90, 0x90, 0x90, 0x90 }; BOOL flag = DriveControl.WriteMemoryBytes(0x401000, writebuff, sizeof(writebuff)); if (flag == TRUE) { printf("[+] Write byte set completed\n"); } system("pause"); return 0; } ``` Run the above code snippet to write the byte set to the '0x401000' memory, and the write effect is shown in the following figure image ## Read/Write Memory Floating Point Numbers Floating point numbers can be divided into single floating point and double floating point. Single floating point can be read and written using 'ReadCacheFloat', while double floating point can be implemented by calling 'ReadCacheDouble'. The implementation principles of the two are completely the same, except that there is an additional width of 4 bytes when reading and writing ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } // Read single floating point float read_float = 0; BOOL float_flag = DriveControl.ReadMemoryFloat(0x01894EF8, &read_float); if (float_flag == TRUE) { printf("[+] Read single floating point = %f \n", read_float); } // Read Double Floating Point double read_double = 0; BOOL double_flag = DriveControl.ReadMemoryDouble(0x01894EF8, &read_double); if (double_flag == TRUE) { printf("[+] Read Double Floating Point = %f \n", double_flag); } system("pause"); return 0; } ``` After running, output two floating-point numbers. Note that double precision is not an error here, but an output issue. The effect image is shown below image So to write data, simply call 'WriteCacheFloat' to achieve the purpose of writing floating-point numbers ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } BOOL ref = DriveControl.WriteMemoryFloat(0x01894EF8, 100.245); if (ref == TRUE) { printf("[+] Write data completed\n"); } system("pause"); return 0; } ``` Taking single precision floating-point numbers as an example, after writing the data, the following effect is output image ## Read/write multi-level dynamic offset The function 'ReadDeviationMemory32' can dynamically calculate multi-level offsets. This function can accept up to 32 levels of offset calculation, and after calculation, a dynamic address can be obtained. After obtaining the dynamic address, users can perform various operations such as reading and writing integers, bytes, byte sets, floating-point numbers, etc. on its address. We take integer reading and writing as an example ```c #include "LyMemory.hpp" #include #include #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(6536); if (set_pid == TRUE) { printf("[*] 设置PID = %d \n", set_pid); } // Calculate the dynamic address of the fourth level offset ProcessDeviationMemory read_offset_struct = { 0 }; read_offset_struct.Address = 0x6566e0; // base address read_offset_struct.OffsetSize = 4; // Offset length read_offset_struct.Data = 0; // Read in data read_offset_struct.Offset[0] = 0x18; // First level offset read_offset_struct.Offset[1] = 0x0; // Secondary offset read_offset_struct.Offset[2] = 0x14; // Third level offset read_offset_struct.Offset[3] = 0x0c; // Fourth level offset // Obtain dynamic address DWORD BaseAddress = DriveControl.ReadDeviationMemory32(&read_offset_struct); printf("[+] Obtain dynamic address = 0x%016lx \n", BaseAddress); // Read integers DWORD GetDWORD = 0; BOOL flag = DriveControl.ReadMemoryDWORD(BaseAddress, &GetDWORD); if (flag == TRUE) { printf("[+] Read integers = %d \n", GetDWORD); } system("pause"); return 0; } ``` The above code calculates the base address of the current dynamic address by calling 'ReadDeviationMemory32', and reads the WORD type of memory here through 'ReadCacheDWORD'. The output effect is shown below image ## Memory Read Write Disassembly We can use 'ReadCachebytes' to read byte sets for read-write functions, and by using the' capstone 'disassembly engine, we can perform disassembly operations on specific memory spaces ```c #include "LyMemory.hpp" #include #include #include #include #pragma comment(lib,"capstone64.lib") #pragma comment(lib,"advapi32.lib") int main(int argc, char *argv[]) { LyMemoryLib DriveControl; DriveControl.InstallAndRun(); BOOL set_pid = DriveControl.SetPid(5588); if (set_pid == TRUE) { printf("[*] PID = %d \n", set_pid); } // Read the first 1024 bytes BYTE MyArray[1024] = { 0 }; BYTE* bufferPtr = MyArray; BOOL flag = DriveControl.ReadMemoryBytes(0x401000, &bufferPtr, sizeof(MyArray)); if (flag == TRUE) { printf("[*] Read complete\n"); } csh handle; cs_insn *insn; size_t count; int size = 1023; // Open handle if (cs_open(CS_ARCH_X86, CS_MODE_32, &handle) != CS_ERR_OK) { return 0; } // Disassemble the code, starting from 0x1000, and return the total number of entries count = cs_disasm(handle, (unsigned char *)MyArray, size, 0x401000, 0, &insn); if (count > 0) { size_t index; for (index = 0; index < count; index++) { /* for (int x = 0; x < insn[index].size; x++) { printf("machine code: %d -> %02X \n", x, insn[index].bytes[x]); } */ printf("Addr: 0x%"PRIx64" | Size: %d Disassembly: %s %s \n", \ insn[index].address, insn[index].size, insn[index].mnemonic, insn[index].op_str); } cs_free(insn, count); } /* else { printf("Disassembly Sizeof \n"); } */ cs_close(&handle); system("pause"); return 0; } ``` After running, the memory area of 0x401000 in the process can be disassembled down 1024 bytes, and the output effect is shown below image