Read/write memory với CGo trên Windows



  • Cụm từ read memory hay write memory chắc hẳn không còn xa lạ gì với những ai đã từng hack cheat game bằng Cheat Engine hay sử dụng code thông qua các ngôn ngữ như C#, C/C++, AutoIt...

    Và tất nhiên là Go vẫn làm được! Trong bài viết này, mình sẽ không sử dụng các package hay thư viện Go bên thứ ba nhé, mà thay vào đó là sử dụng CGo và các Windows API.

    Chuẩn bị

    • Cần có kiến thức về C, Go căn bản, bao gồm cả pointer, bộ nhớ...
    • Cheat Engine dùng để lấy process ID, lấy address, check xem có hiệu nghiệm hay không...
    • Trình biên dịch C, nên sử dụng GCC và phải đúng với phiên bản của Go (x86 hoặc x64).

    Code thôi

    Đầu tên là tạo một file .go mới, mình đặt tên là main.go.

    package main
    
    inport (
        "fmt"      // sử dụng để in kết quả
        "unsafe"   // sử dụng cho cast unsafe pointer
    )
    
    func main() {
    
    }
    

    Để sử dụng CGo, mình sẽ viết code C trực tiếp vào luôn cho tiện.

    /*
    // code C ...
    */
    import "C"
    

    Nên nhớ là code C phải nằm trong phần comment, và dưới phần kết thúc comment là import "C".

    /*
    #cgo CFLAGS: -Wincompatible-pointer-types
    #include <stdint.h>
    #include <windows.h>
    // C code
    */
    import "C"
    

    dòng #cgo CFLAGS... để tắt một số warning khó chịu từ GCC
    stdint.h để khai báo một số kiểu cần thiết
    windows.h cho các hàm cần thiết

    Hàm read memory

    Để đọc được bộ nhớ của một process thì ta sẽ sử dụng hàm ReadProcessMemory, mẫu code trên MSDN như sau:

    BOOL WINAPI ReadProcessMemory(
        _In_  HANDLE  hProcess,
        _In_  LPCVOID lpBaseAddress,
        _Out_ LPVOID  lpBuffer,
        _In_  SIZE_T  nSize,
        _Out_ SIZE_T  *lpNumberOfBytesRead
    );
    

    Nếu bạn không phải là dân lập trình Windows thì chắc chắn chẳng hiểu code trên miêu tả gì. Ta cứ xem các _In_ _Out_ là môt tả đặc tính để sử dụng các tham số, bỏ qua chúng, ta được như sau:

    BOOL WINAPI ReadProcessMemory(
        HANDLE  hProcess,
        LPCVOID lpBaseAddress,
        LPVOID  lpBuffer,
        SIZE_T  nSize,
        SIZE_T  *lpNumberOfBytesRead
    );
    

    Vậy là trở về cách khai báo hàm thông thường trong C. Mìnlh sẽ giải thích luôn các kiểu dữ liệu trên luôn

    • HANDLE là một con trỏ (pointer) trỏ đến một cái gì đó, trong trường hợp này là trỏ đến một process đang chạy, mình sẽ quy nó về void * cho tiện.
    • LPCVOIDconst void *, LPVOIDvoid *
    • SIZE_T là số nguyên không dấu (4 byte cho x86 và 8 byte cho x64).

    Xử lý các tham số

    Đầu tiên là #1 (hProcess), nó là handle của một process, vậy lấy từ đâu?
    Trên trang MSDN lúc nãy, lăn xuống cuối ta sẽ thấy hàm OpenProcess, mình sẽ sử dụng nó để lấy process handle.

    HANDLE OpenProcess(
        DWORD dwDesiredAccess,
        BOOL  bInheritHandle,
        DWORD dwProcessId
    );
    

    #1 là khai báo quyền để truy cập process, mình sẽ dùng flag PROCESS_ALL_ACCESS để có full quyền.
    #2 thì cái này không cần quan tâm, cứ gán là 0
    #3 là process ID, cứ Task Manager lên là có hoặc sử dụng Cheat Engine

    Ok, mình sẽ tóm gọn hàm này lại:

    void *open_process(int pid) {
        return (void*)OpenProcess(PROCESS_ALL_ACCESS, 0, pid);
    }
    

    Nhưng khoan, để tránh leak memory sau khi không còn sử dụng process handle nữa thì phải đóng handle ngay.

    void close_process(void *proc) {
        CloseHandle((HANDLE)proc);
    }
    

    Như vậy là đã có hàm để lấy process handle, ta trở lại với hàm read memory.

    int read_memory(void *proc, size_t addr, void *buf, size_t size) {
        return ReadProcessMemory((HANDLE)proc, (void*)addr, buf, size, NULL);
    }
    

    Vậy là xong phần C, giờ đến code Go thôi.

    • Để sử dụng hàm từ CGo, ta chỉ cần sử dụng namespace C, chẳng hạn C.open_process().

    Đầu tiên là cần có một process id mình sẽ lấy từ CE.

    52da3879-8988-4dfc-8835-57ff66c12691-image.png

    name process id
    chrome.exe 0x01520
    func main() {
        proc := C.open_process(0x01520) //  0x01520- chrome.exe
        if proc == nil {
            fmt.Print("cannot open process!\n") // check lỗi
        }
    

    Tiếp theo ta cần một địa chỉ của một giá trị cần đọc, mình tiếp tục sử dụng CE và scan một value bất kỳ, chẳng hạn 123. Nhớ click đúp vào address đó, lưu nó lại để tiện cho việc quan sát.

    1c7a8d06-b541-4363-8934-1b627f8ec899-image.png

    address value
    0x000E29D0 123
        } else {
            addr := C.ulonglong(0x000E29D0)
    
    

    Nên nhớ, poiter ([type] *) trong C là một số nguyên không dấu, tương ứng với Go thì addr sẽ := C.ulong (cho x86) hoặc := C.ulonglong (cho x64, mình đang dùng kiến trúc này).

    Tiếp tục tham số #2 là một pointer trỏ đến một ô nhớ, giá trị mình chọn là một số nguyên nên mình sẽ cấp phát cho nó một block chứa int (int * - 4 byte), ta sử dụng hàm new() (có thể sử dụng malloc(sizeof(int)) trong C, nhưng new sẽ an toàn hơn và không cần phải free).

            addr := C.ulonglong(0x000E29D0)
            buf := new(int)
    
    

    Tiếp đến là hàm C.read_memory():

            addr := C.ulonglong(0x000E29D0)
            buf := new(int)
            //                              | cast về void *
            ret := C.read_memory(proc, addr, unsafe.Pointer(buf), C.sizeof_int) 
            
            // check lỗi
            if ret != 0 {
                fmt.Printf("value: %d\n", *buf) // đọc giá trị int
            } else {
                fmt.Printf("cannot read on address: %08X\n", addr)
            }
    
            C.close_process(proc) // đóng process
        }
    

    Kết quả

    $ go run main.go
    value: 123
    

    Nếu đọc các giá trị khác thì phải sử dụng các kiểu tương ứng nhé. Chẳng hạn số thực thì có float32 (C.sizeof_float, 4 byte), float64 (C.sizeof_double, 8 byte).

    Đối với chuỗi sẽ khác tí, chuỗi ASCII thì sử dụng new([N kí tự]byte) (size = N kí tự), chuỗi unicode thì tương tự với type là uint16 và size sẽ gấp đôi. Có thể tạo một mảng byte khác rồi copy từng phần tử vào, sau đó dùng string(byte_array) để chuyển chúng thành chuỗi thuần trong Go hoặc sử dụng pointer tạo từ new lúc nãy để pass vào C dưới dạng char * hoặc wchar_t *.

    Hàm write memory

    BOOL WINAPI WriteProcessMemory(
        _In_  HANDLE  hProcess,
        _In_  LPVOID  lpBaseAddress,
        _In_  LPCVOID lpBuffer,
        _In_  SIZE_T  nSize,
        _Out_ SIZE_T  *lpNumberOfBytesWritten
    );
    

    Thực sự mà nói thì nó cũng tương tự như hàm read thôi, bạn để ý kỹ tham số thứ 3 sẽ thấy nó là _In_ trong khi hàm read ở trên kia thì là _Out_. Có nghĩa là hàm read sẽ trả về pointer đến ô nhớ chứa giá trị, còn hàm này sẽ dùng giá trị lấy từ pointer để thay vào ô giá trị trong process.

    Ta có thể biểu diễn trong C như sau:

    int a = 15; // giá trị ngoài
    int b = 0; // giá trị của process
    
    // lấy pointer của hai ô giá trị
    int *_a = &a;
    int *_b = &b;
    
    // gán giá trị
    *_b = *_a; // b = 15
    

    Tương tự với Go:

    // setup giá trị
    a, b := 15, 0
    // lấy pointer
    _a, _b := &a, &b
    // gán
    *_b = *_a // b = 15
    

    Mình sẽ rút gọn hàm write memory trong C

    int write_memory(void *proc, size_t addr, void *buf, size_t size) {
        return WriteProcessMemory((HANDLE)proc, (void*)addr, buf, size, NULL);
    }
    

    Và gọi hàm trong GO để thay đổi giá trị.

        // tạo giá trị
        val := 6969
        //                         | lấy pointer của giá trị
        ret = C.write_memory(proc, addr, unsafe.Pointer(&val), C.sizeof_int)
    

    Bạn có thể thêm code trên vào trước hàm read hoặc check trên CE để xem giá trị thay đổi (nếu ret != 0).

    Xem code hoàn chỉnh tại gist này.

    Challenge

    • Kết hợp EnumProcesses và một số Win API khác để có thể lấy process ID bằng tên cửa sổ hoặc tên chương trình (process name *.exe, *.sys...)
    • Thật sự thì read/write memory rất đơn giản, bạn có thể kết hợp các thuật toán, code nâng cao để scan memory hay tạo một công cụ cheat riêng cho mình 😂

    Bài viết đến đây là kết thúc rồi, nếu thấy hay hãy cho 0x01 like nhóe!
    Thân chào!


  • Trùm cuối

    @wuuyi Bài hay quá bác ạ 💪


Hãy đăng nhập để trả lời
 

Có thể bạn cũng quan tâm

.
DMCA.com Protection Status