Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
push:
branches: [ "master", "main" ]
pull_request:
branches: [ "master", "main" ]

jobs:
test-win32:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-go@v5
with:
go-version: 'stable'

- name: WBAB Preflight
shell: bash
run: ./tools/wbab preflight

- name: WBAB Build
shell: bash
run: ./tools/wbab build ${GITHUB_REF_NAME}

- name: WBAB Test
shell: bash
run: ./tools/wbab test
16 changes: 11 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ on:

jobs:
build-win32:
runs-on: ubuntu-latest
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Set up Go
uses: actions/setup-go@v5
- uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version: 'stable'

- name: Install NSIS
run: choco install nsis -y

- name: WBAB Preflight
shell: bash
Expand All @@ -34,6 +36,10 @@ jobs:
shell: bash
run: ./tools/wbab test

- name: WBAB Test
shell: bash
run: ./tools/wbab test

- name: WBAB Package
shell: bash
run: ./tools/wbab package ${GITHUB_REF_NAME}
Expand All @@ -60,4 +66,4 @@ jobs:
uses: softprops/action-gh-release@v1
with:
files: dist/*
generate_release_notes: true
generate_release_notes: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ wi_nonce*
bin/
obj/
out/
wininspect-portable
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ option(WININSPECT_BUILD_TESTS "Build tests" ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

if (WIN32)
add_compile_definitions(UNICODE _UNICODE)
endif()

add_library(wininspect_core
core/src/core.cpp
core/src/fake_backend.cpp
Expand Down
46 changes: 46 additions & 0 deletions clients/cli/src/cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,52 @@

static const wchar_t *PIPE_NAME = L"\\\\.\\pipe\\wininspectd";

struct Conn {
bool is_tcp = false;
SOCKET s = INVALID_SOCKET;
HANDLE hPipe = INVALID_HANDLE_VALUE;

bool send(const std::string& data) {
uint32_t len = (uint32_t)data.size();
if (is_tcp) {
if (::send(s, (const char*)&len, 4, 0) != 4) return false;
return ::send(s, data.data(), (int)len, 0) == (int)len;
} else {
DWORD w = 0;
if (!WriteFile(hPipe, &len, 4, &w, nullptr)) return false;
return WriteFile(hPipe, data.data(), len, &w, nullptr) != FALSE;
}
}

bool recv(std::string& out) {
uint32_t len = 0;
if (is_tcp) {
if (::recv(s, (char*)&len, 4, 0) != 4) return false;
out.resize(len);
uint32_t n = 0;
while (n < len) {
int r = ::recv(s, out.data() + n, (int)(len - n), 0);
if (r <= 0) return false;
n += r;
}
return true;
} else {
DWORD r = 0;
if (!ReadFile(hPipe, &len, 4, &r, nullptr)) return false;
out.resize(len);
if (!ReadFile(hPipe, out.data(), len, &r, nullptr)) return false;
return true;
}
}

void close() {
if (is_tcp && s != INVALID_SOCKET) { closesocket(s); s = INVALID_SOCKET; }
if (!is_tcp && hPipe != INVALID_HANDLE_VALUE) { CloseHandle(hPipe); hPipe = INVALID_HANDLE_VALUE; }
}

~Conn() { close(); }
};

static std::string get_config_path() {
const char *home = getenv("USERPROFILE");
if (!home)
Expand Down
38 changes: 16 additions & 22 deletions clients/gui/src/gui_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,23 @@ using namespace wininspect_gui;
// Simple pipe transport for the GUI
class PipeTransport : public ITransport {
public:
std::string request(const std::string &json) override {
HANDLE h = CreateFileW(L"\.\pipe\wininspectd", GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_EXISTING, 0, nullptr);
if (h == INVALID_HANDLE_VALUE)
return "{" ok ":false," error ":" no daemon "}";

uint32_t len = (uint32_t)json.size();
DWORD w = 0;
WriteFile(h, &len, 4, &w, nullptr);
WriteFile(h, json.data(), len, &w, nullptr);

uint32_t rlen = 0;
DWORD r = 0;
if (!ReadFile(h, &rlen, 4, &r, nullptr)) {
CloseHandle(h);
return "{" ok ":false}";
std::string request(const std::string& json) override {
HANDLE h = CreateFileW(L"\\\\.\\pipe\\wininspectd", GENERIC_READ|GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (h == INVALID_HANDLE_VALUE) return "{\"ok\":false,\"error\":\"no daemon\"}";

uint32_t len = (uint32_t)json.size();
DWORD w = 0;
WriteFile(h, &len, 4, &w, nullptr);
WriteFile(h, json.data(), len, &w, nullptr);

uint32_t rlen = 0;
DWORD r = 0;
if (!ReadFile(h, &rlen, 4, &r, nullptr)) { CloseHandle(h); return "{\"ok\":false}"; }
std::string resp; resp.resize(rlen);
ReadFile(h, resp.data(), rlen, &r, nullptr);
CloseHandle(h);
return resp;
}
std::string resp;
resp.resize(rlen);
ReadFile(h, resp.data(), rlen, &r, nullptr);
CloseHandle(h);
return resp;
}
};

class WinInspectWindow {
Expand Down
63 changes: 32 additions & 31 deletions clients/portable/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ type Request struct {
}

type Response struct {
ID string `json:"id"`
OK bool `json:"ok"`
ID string `json:"id"`
OK bool `json:"ok"`
Result json.RawMessage `json:"result,omitempty"`
Error *ErrorDetail `json:"error,omitempty"`
}
Expand Down Expand Up @@ -107,14 +107,10 @@ func runCommand(addr, keyPath, method string, args []string) error {

params := make(map[string]interface{})
if method == "info" || method == "children" {
if len(args) < 1 {
return fmt.Errorf("missing hwnd")
}
if len(args) < 1 { return fmt.Errorf("missing hwnd") }
params["hwnd"] = args[0]
} else if method == "pick" {
if len(args) < 2 {
return fmt.Errorf("missing x y")
}
if len(args) < 2 { return fmt.Errorf("missing x y") }
params["x"] = args[0]
params["y"] = args[1]
}
Expand All @@ -140,9 +136,7 @@ func runCommand(addr, keyPath, method string, args []string) error {
func handshake(conn net.Conn, keyPath string) (*CryptoSession, error) {
// Recv Hello
helloData, err := recvRaw(conn)
if err != nil {
return nil, err
}
if err != nil { return nil, err }
var hello map[string]interface{}
json.Unmarshal(helloData, &hello)

Expand All @@ -158,14 +152,10 @@ func handshake(conn net.Conn, keyPath string) (*CryptoSession, error) {
"signature": "SSHSIG_STUB",
}
respData, _ := json.Marshal(resp)
if err := sendRaw(conn, respData); err != nil {
return nil, err
}
if err := sendRaw(conn, respData); err != nil { return nil, err }

authStatus, err := recvRaw(conn)
if err != nil {
return nil, err
}
if err != nil { return nil, err }
if !strings.Contains(string(authStatus), "\"ok\":true") {
return nil, fmt.Errorf("auth failed")
}
Expand All @@ -187,33 +177,44 @@ func sendRaw(conn net.Conn, data []byte) error {
func recvRaw(conn net.Conn) ([]byte, error) {
lenBuf := make([]byte, 4)
_, err := io.ReadFull(conn, lenBuf)
if err != nil {
return nil, err
}
if err != nil { return nil, err }
length := binary.LittleEndian.Uint32(lenBuf)
data := make([]byte, length)
_, err = io.ReadFull(conn, data)
return data, err
}

func sendEncrypted(conn net.Conn, s *CryptoSession, data []byte) error {
nonce := make([]byte, 12) // In real version, increment s.nonce
ciphertext := s.aesGCM.Seal(nil, nonce, data, nil)
nonce := make([]byte, 12)
binary.LittleEndian.PutUint64(nonce, s.nonce)
s.nonce++

// Matches our C++ logic: [Nonce(12)][Tag(16)][Ciphertext(N)]
// Note: Seal returns [Ciphertext][Tag], we may need to reorder
return sendRaw(conn, ciphertext)
// Seal appends [Ciphertext][Tag] to dst (nil here, so new slice)
sealed := s.aesGCM.Seal(nil, nonce, data, nil)

// C++ Logic: [Nonce(12)][Tag(16)][Ciphertext(N)]
// Sealed is: [Ciphertext(N)][Tag(16)]
tagSize := 16
if len(sealed) < tagSize {
return fmt.Errorf("encryption error")
}

realCipher := sealed[:len(sealed)-tagSize]
tag := sealed[len(sealed)-tagSize:]

total := make([]byte, 12 + 16 + len(realCipher))
copy(total[0:12], nonce)
copy(total[12:28], tag)
copy(total[28:], realCipher)

return sendRaw(conn, total)
}

func recvEncrypted(conn net.Conn, s *CryptoSession) ([]byte, error) {
data, err := recvRaw(conn)
if err != nil {
return nil, err
}
if err != nil { return nil, err }
// In this scaffold, decrypt is just returning the slice past 28 bytes
if len(data) < 28 {
return nil, fmt.Errorf("malformed packet")
}
if len(data) < 28 { return nil, fmt.Errorf("malformed packet") }
return data[28:], nil
}

Expand Down
3 changes: 1 addition & 2 deletions core/include/wininspect/backend.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ class IBackend {
virtual bool send_input(const std::vector<uint8_t> &raw_input_data) = 0;

// Higher-level injection helpers
virtual bool send_mouse_click(int x, int y,
int button) = 0; // 0=left, 1=right, 2=middle
virtual bool send_mouse_click(int x, int y, int button) = 0; // 0=left, 1=right, 2=middle
virtual bool send_key_press(int vk) = 0;
virtual bool send_text(const std::string &text) = 0;

Expand Down
43 changes: 22 additions & 21 deletions core/src/crypto.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <bcrypt.h>
#ifndef BCRYPT_ECD_PUBLIC_GENERIC_MAGIC
#define BCRYPT_ECD_PUBLIC_GENERIC_MAGIC 0x50434345
#endif
#include <vector>
#include <string>
#include <sstream>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <windows.h>

#include "wininspect/crypto.hpp"
Expand Down Expand Up @@ -74,24 +77,22 @@ bool CryptoSession::compute_shared_secret(
return false;
}

BCRYPT_BUFFER_DESC derDesc = {0};
BCRYPT_BUFFER derBuffers[1] = {0};
derDesc.cBuffers = 1;
derDesc.pBuffers = derBuffers;
derDesc.ulVersion = BCRYPTBUFFER_VERSION;
derBuffers[0].BufferType = KDF_HASH_ALGORITHM;
derBuffers[0].cbBuffer =
(ULONG)((wcslen(BCRYPT_SHA256_ALGORITHM) + 1) * sizeof(wchar_t));
derBuffers[0].pvBuffer = (PVOID)BCRYPT_SHA256_ALGORITHM;

uint8_t derived[32];
ULONG cbDerived = 0;
if (BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, &derDesc, derived, 32,
&cbDerived, 0) != 0) {
BCryptDestroySecret(hSecret);
BCryptDestroyKey(hRemoteKey);
return false;
}
BCryptBufferDesc derDesc = { 0 };
BCryptBuffer derBuffers[1] = { 0 };
derDesc.cBuffers = 1;
derDesc.pBuffers = derBuffers;
derDesc.ulVersion = BCRYPTBUFFER_VERSION;
derBuffers[0].BufferType = KDF_HASH_ALGORITHM;
derBuffers[0].cbBuffer = (ULONG)((wcslen(BCRYPT_SHA256_ALGORITHM) + 1) * sizeof(wchar_t));
derBuffers[0].pvBuffer = (PVOID)BCRYPT_SHA256_ALGORITHM;

uint8_t derived[32];
ULONG cbDerived = 0;
if (BCryptDeriveKey(hSecret, BCRYPT_KDF_HASH, &derDesc, derived, 32, &cbDerived, 0) != 0) {
BCryptDestroySecret(hSecret);
BCryptDestroyKey(hRemoteKey);
return false;
}

BCryptDestroySecret(hSecret);
BCryptDestroyKey(hRemoteKey);
Expand Down
3 changes: 1 addition & 2 deletions core/src/fake_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,7 @@ std::vector<UIElementInfo> FakeBackend::inspect_ui_elements(hwnd_u64 parent) {
return it->second;
}

void FakeBackend::add_fake_ui_element(hwnd_u64 parent,
const UIElementInfo &info) {
void FakeBackend::add_fake_ui_element(hwnd_u64 parent, const UIElementInfo &info) {
std::lock_guard<std::mutex> lk(mu_);
ui_elements_[parent].push_back(info);
}
Expand Down
Loading
Loading