feat: find and enumerate the windows of other processes

This commit is contained in:
mirkobrombin committed 2026-11-04 17:01:05 +00:00
1 parent ceeaffdf42
commit 84dfa0e477
9 files changed
+209 -9

No files matched your search

+8
View File
@@ -3363,6 +3363,14 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$")
add_custom_target(dialog_font_fixture${width} ALL DEPENDS ${dialog_font_client})
add_test(NAME native_dialog_font_x${width} COMMAND raven --run-process --execution=native ${dialog_font_client})
set_tests_properties(native_dialog_font_x${width} PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION "Native entry returned 0 ")
set(cross_window_client ${CMAKE_CURRENT_BINARY_DIR}/cross-window${width}.exe)
add_custom_command(OUTPUT ${cross_window_client}
COMMAND ${webservices_compiler} -nostdlib -O1 -fno-builtin -fno-stack-protector -Wl,--entry,${webservices_entry},--no-insert-timestamp
-o ${cross_window_client} ${LOADER_SOURCES}/cross_window_client.c -luser32 -lkernel32
DEPENDS ${LOADER_SOURCES}/cross_window_client.c VERBATIM)
add_custom_target(cross_window_fixture${width} ALL DEPENDS ${cross_window_client})
add_test(NAME native_cross_window_x${width} COMMAND raven --run-process --execution=native --timeout-ms=30000 ${cross_window_client})
set_tests_properties(native_cross_window_x${width} PROPERTIES TIMEOUT 40 PASS_REGULAR_EXPRESSION "Native entry returned 0 ")
# mlang comes with the installer components, whose import library the client links against.
if(width EQUAL 64)
set(mlang_dir ${RAVEN_INSTALLER_COMPONENT_DIR})
+33 -3
View File
@@ -10,6 +10,8 @@
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <sstream>
#include <vector>
#include <mutex>
#include <signal.h>
@@ -115,7 +117,8 @@ void publish_window_traits(uint64_t window, const ForeignWindowTraits &traits)
const auto temporary = directory / (window_name(window) + ".traits." + std::to_string(::getpid()));
{
std::ofstream output(temporary, std::ios::trunc);
output << traits.style << ' ' << traits.ex_style << '\n' << traits.class_name;
output << traits.style << ' ' << traits.ex_style << ' ' << traits.parent << ' ' << (traits.visible ? 1 : 0) << '\n' << traits.class_name << '\n'
<< traits.title;
}
std::error_code error;
std::filesystem::rename(temporary, final_path, error);
@@ -151,13 +154,40 @@ std::optional<ForeignWindowTraits> foreign_window_traits(uint64_t window)
return std::nullopt;
std::ifstream input(window_directory() / (window_name(window) + ".traits"));
ForeignWindowTraits traits;
if (!(input >> traits.style >> traits.ex_style))
std::string first;
if (!std::getline(input, first))
return std::nullopt;
input.ignore(1);
std::istringstream fields(first);
int visible = 0;
if (!(fields >> traits.style >> traits.ex_style >> traits.parent >> visible))
return std::nullopt;
traits.visible = visible != 0;
std::getline(input, traits.class_name);
// The title is the rest of the file, line breaks and all.
traits.title.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
return traits;
}
std::vector<uint64_t> foreign_windows()
{
std::vector<uint64_t> found;
std::error_code error;
for (const auto &entry : std::filesystem::directory_iterator(window_directory(), error)) {
const auto path = entry.path();
if (path.extension() != ".window")
continue;
const auto stem = path.stem().string();
char *end = nullptr;
const auto low = std::strtoull(stem.c_str(), &end, 16);
if (stem.size() != 8 || !end || *end)
continue;
// A handle keeps its process tag in the bits above the low word the name carries; window handles fit in those 32 bits.
if (foreign_record(low))
found.push_back(low);
}
return found;
}
std::optional<uint32_t> foreign_window_owner(uint64_t window)
{
const auto record = foreign_record(window);
+8 -1
View File
@@ -6,6 +6,7 @@
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace raven {
@@ -25,13 +26,19 @@ std::optional<std::pair<uint32_t, uint32_t>> foreign_window_size(uint64_t window
// The process that owns a window another live process of the prefix created.
std::optional<uint32_t> foreign_window_owner(uint64_t window);
// What another process may ask of a window it does not own: its styles and the name of its class.
// What another process may ask of a window it does not own: its styles, its parent, whether it is shown, the name of its class and
// its title (both UTF-8).
struct ForeignWindowTraits {
uint32_t style = 0, ex_style = 0;
std::string class_name;
uint64_t parent = 0;
bool visible = false;
std::string title;
};
void publish_window_traits(uint64_t window, const ForeignWindowTraits &traits);
std::optional<ForeignWindowTraits> foreign_window_traits(uint64_t window);
// The windows other live processes of the prefix own, in no particular order.
std::vector<uint64_t> foreign_windows();
// Pixels of a window shared between the process that draws them and the owner: a header page, then 32-bit rows top down.
class ForeignSurface {
+6
View File
@@ -983,6 +983,12 @@ void NativeWindows::register_ext_api(ApiRegistry &registry)
call.ret(0);
return;
}
if (const auto foreign = std::as_const(objects).window(call[0]) ? std::nullopt : foreign_window_traits(call[0])) {
const auto text = to_ansi(from_utf8(foreign->title), 1252);
call.put_narrow(call[1], text, size_t(call.i32(2)));
call.ret(std::min<size_t>(text.size(), size_t(call.i32(2)) - 1));
return;
}
call.ret(send_ansi(call, call[0], 0x0d, call[2], call[1]));
});
registry.add("TrackPopupMenu", 7, [=, this](ApiCall &call) {
+1 -2
View File
@@ -187,8 +187,7 @@ void NativeWindows::register_long_api(ApiRegistry &registry)
std::memcpy(bytes.data() + 4, &next, 4);
process.write_memory(scratch, bytes);
send(call[0], 0x7d, index_word, scratch);
if (const auto *changed_window = std::as_const(objects).window(call[0]))
publish_window_traits(call[0], {changed_window->style, changed_window->ex_style, objects.window_class(changed_window->class_id)->name});
publish_traits(call[0]);
if (std::as_const(objects).window(call[0]) && objects.window_visible(call[0])) {
invalidate_tree(call[0], true);
drawing(call[0]);
+32 -1
View File
@@ -146,6 +146,10 @@ bool NativeWindows::show_window(uint64_t handle, uint32_t command)
window->visible = !hide;
window->style = (window->style & ~ws_visible) | (window->visible ? ws_visible : 0);
objects.update_window_dcs(window->parent ? window->parent : handle);
if (!window->parent) {
publish_traits(handle);
window = objects.window(handle);
}
if (show) {
// Showing a window paints its whole subtree, so the children's update regions are invalidated with it.
if (!objects.invalidate(handle, {0, 0, window->width, window->height}, true))
@@ -353,6 +357,21 @@ void NativeWindows::place_window(uint64_t handle, DesktopRect outer, uint64_t in
process.unmap_memory(scratch);
}
uint64_t NativeWindows::foreign_top_level(const std::u16string &class_name, bool titled, const std::u16string &title) const
{
for (const auto handle : foreign_windows()) {
const auto traits = foreign_window_traits(handle);
if (!traits || traits->parent)
continue;
if (!class_name.empty() && !equal_fold(from_utf8(traits->class_name), class_name))
continue;
if (titled && from_utf8(traits->title) != title)
continue;
return handle;
}
return 0;
}
void NativeWindows::register_manager_api(ApiRegistry &registry)
{
registry.add("FindWindowW", 2, [this](ApiCall &call) {
@@ -371,6 +390,10 @@ void NativeWindows::register_manager_api(ApiRegistry &registry)
call.ret(*root);
return;
}
if (const auto found = foreign_top_level(name, call[1] != 0, title)) {
call.ret(found);
return;
}
call.fail(1400);
});
registry.add("FindWindowA", 2, [this](ApiCall &call) {
@@ -390,6 +413,10 @@ void NativeWindows::register_manager_api(ApiRegistry &registry)
call.ret(*root);
return;
}
if (const auto found = foreign_top_level(name, call[1] != 0, title)) {
call.ret(found);
return;
}
call.fail(1400);
});
registry.add("FindWindowExW", 4, [this](ApiCall &call) {
@@ -654,7 +681,7 @@ void NativeWindows::register_manager_api(ApiRegistry &registry)
registry.add("WindowFromPoint", 1, [this](ApiCall &call) { call.ret(window_at(int32_t(call[0]), int32_t(call[0] >> 32))); });
const auto enumerate = [this](ApiCall &call, const std::vector<uint64_t> &handles, uint64_t procedure, uint64_t parameter) {
for (const auto handle : handles) {
if (!std::as_const(objects).window(handle))
if (!std::as_const(objects).window(handle) && !foreign_window_size(handle))
continue;
const uint64_t arguments[] = {handle, parameter};
const auto result = invoke(procedure, arguments);
@@ -668,6 +695,10 @@ void NativeWindows::register_manager_api(ApiRegistry &registry)
registry.add("EnumWindows", 2, [this, enumerate](ApiCall &call) {
auto roots = top_level_windows();
std::reverse(roots.begin(), roots.end());
// The top-level windows of the other processes of the prefix follow, as one desktop holds them all on Windows.
for (const auto handle : foreign_windows())
if (const auto traits = foreign_window_traits(handle); traits && !traits->parent)
roots.push_back(handle);
enumerate(call, roots, call[0], call[1]);
});
registry.add("EnumThreadWindows", 3, [this, enumerate](ApiCall &call) {
+27 -2
View File
@@ -511,6 +511,15 @@ void NativeWindows::watch_input()
}).detach();
}
void NativeWindows::publish_traits(uint64_t handle)
{
const auto *window=std::as_const(objects).window(handle);
if (!window || window->destroying) return;
const auto *definition=objects.window_class(window->class_id);
publish_window_traits(handle,{window->style,window->ex_style,definition ? definition->name : std::string(),window->parent,window->visible,
to_utf8(window->title)});
}
uint64_t NativeWindows::foreign_dc(uint64_t handle)
{
auto size=foreign_window_size(handle);
@@ -1605,6 +1614,8 @@ uint64_t NativeWindows::default_message(std::span<const uint64_t> args)
const auto address=message==0x81 ? (narrow ? read(process.read_memory(args[3],48),36,4) :
read(process.read_memory(args[3],80),56,8)) : args[3];
if (!objects.set_title(args[0],window_text(address))) throw uint32_t(8);
// Other processes look top-level windows up by title; a control's text changing often stays out of the shared directory.
if (!window->parent) publish_traits(args[0]);
if (objects.window_visible(args[0])) present(args[0]);
return 1;
}
@@ -1914,7 +1925,7 @@ uint64_t NativeWindows::create(std::span<const uint64_t> args)
if (child) parent_notify(handle,1);
if (!std::as_const(objects).window(handle)) return 0;
if (const auto *created=std::as_const(objects).window(handle)) publish_window(handle,uint32_t(std::max(created->width,0)),uint32_t(std::max(created->height,0)));
if (const auto *created=std::as_const(objects).window(handle)) publish_window_traits(handle,{created->style,created->ex_style,objects.window_class(created->class_id)->name});
publish_traits(handle);
if (effective_style & 0x10000000U) {
auto *window=objects.window(handle);window->visible=true;objects.update_window_dcs(child ? window->parent : handle);
if (objects.window_visible(handle)) present(handle);
@@ -2648,6 +2659,19 @@ bool NativeWindows::dispatch(std::string_view name)
process.write_memory(args[1],rect); result=1;
}
else if (name=="DefWindowProcW") result=default_message(args);
// The title of another process's window comes from what it published; its procedure is not asked.
else if (const auto foreign=(name=="GetWindowTextLengthW" || name=="GetWindowTextW") && !std::as_const(objects).window(args[0]) ?
foreign_window_traits(args[0]) : std::nullopt) {
const auto title=from_utf8(foreign->title);
if (name=="GetWindowTextLengthW") result=title.size();
else if (int32_t(args[2])>0) {
const auto count=std::min<size_t>(title.size(),size_t(uint32_t(args[2]))-1);
std::vector<uint8_t> bytes((count+1)*2);
std::memcpy(bytes.data(),title.data(),count*2);
process.write_memory(args[1],bytes);
result=count;
}
}
else if (name=="GetWindowTextLengthW") result=send(args[0],0x0e,0,0);
else if (name=="SetWindowTextW") result=send(args[0],0x0c,0,args[1]);
else if (name=="GetWindowTextW") {
@@ -2833,7 +2857,8 @@ bool NativeWindows::dispatch(std::string_view name)
if (withdraw_screen_dc(args[1])) publish_screen_dc(owner);
}
} else if (name=="IsWindowVisible") {
result=objects.window_visible(args[0]);
if (std::as_const(objects).window(args[0])) result=objects.window_visible(args[0]);
else if (const auto foreign=foreign_window_traits(args[0])) result=foreign->visible;
} else if (name=="ShowWindow") {
result=show_window(args[0],uint32_t(args[1]));
} else if (name=="UpdateWindow") {
+4
View File
@@ -185,6 +185,10 @@ private:
struct ForeignTarget { std::shared_ptr<ForeignSurface> surface; uint64_t bitmap=0; };
std::map<uint64_t,ForeignTarget> foreign_targets;
uint64_t foreign_dc(uint64_t window);
// Tells the other processes of the prefix what they may ask of a window: styles, parent, visibility, class and title.
void publish_traits(uint64_t window);
// A top-level window of another process of the prefix with this class (any when empty) and, when asked, this title; 0 when none.
uint64_t foreign_top_level(const std::u16string &class_name, bool titled, const std::u16string &title) const;
std::map<uint64_t,uint32_t> foreign_frames;
// Windows that received a frame from another process with something other than black on it.
std::set<uint64_t> foreign_content;
+90
View File
@@ -0,0 +1,90 @@
// The windows of one process are visible to another of the same prefix: a child finds its parent's window by class and title, reads
// its title and visibility, sees it in EnumWindows and learns which process owns it.
#include <windows.h>
static const wchar_t class_name[] = L"RavenCrossWindow";
static const wchar_t title[] = L"Cross window probe é";
struct search { HWND wanted; int seen; };
static BOOL CALLBACK visit(HWND window, LPARAM parameter)
{
struct search *search = (struct search *)parameter;
if (window == search->wanted)
search->seen = 1;
return TRUE;
}
static int same(const wchar_t *left, const wchar_t *right)
{
return lstrcmpW(left, right) == 0;
}
static int child(DWORD parent)
{
HWND window = FindWindowW(class_name, title);
if (!window)
return 10;
if (FindWindowW(class_name, NULL) != window || FindWindowW(NULL, title) != window || FindWindowA("RavenCrossWindow", NULL) != window)
return 11;
if (FindWindowW(class_name, L"Another title"))
return 12;
wchar_t text[64];
if (GetWindowTextLengthW(window) != lstrlenW(title) || GetWindowTextW(window, text, 64) != lstrlenW(title) || !same(text, title))
return 13;
if (GetWindowTextW(window, text, 6) != 5 || text[5] || text[0] != L'C')
return 14;
if (GetClassNameW(window, text, 64) != lstrlenW(class_name) || !same(text, class_name))
return 15;
if (!IsWindow(window) || !IsWindowVisible(window))
return 16;
DWORD owner = 0;
GetWindowThreadProcessId(window, &owner);
if (owner != parent)
return 17;
struct search search = {window, 0};
if (!EnumWindows(visit, (LPARAM)&search) || !search.seen)
return 18;
return 0;
}
int raven_entry(void)
{
const wchar_t *line = GetCommandLineW();
const wchar_t *marker = line;
for (; *marker; ++marker)
if (marker[0] == L'/' && marker[1] == L'c' && marker[2] == L':') {
DWORD parent = 0;
for (marker += 3; *marker >= L'0' && *marker <= L'9'; ++marker)
parent = parent * 10 + (DWORD)(*marker - L'0');
return child(parent);
}
WNDCLASSW definition = {0};
definition.lpfnWndProc = DefWindowProcW;
definition.hInstance = GetModuleHandleW(NULL);
definition.lpszClassName = class_name;
if (!RegisterClassW(&definition))
return 1;
HWND window = CreateWindowExW(0, class_name, title, WS_OVERLAPPEDWINDOW, 10, 10, 200, 120, NULL, NULL, definition.hInstance, NULL);
if (!window)
return 2;
ShowWindow(window, SW_SHOWNORMAL);
wchar_t path[MAX_PATH], command[MAX_PATH + 64];
if (!GetModuleFileNameW(NULL, path, MAX_PATH))
return 3;
wsprintfW(command, L"\"%s\" /c:%lu", path, GetCurrentProcessId());
STARTUPINFOW startup = {sizeof(startup)};
PROCESS_INFORMATION information;
if (!CreateProcessW(path, command, NULL, NULL, FALSE, 0, NULL, NULL, &startup, &information))
return 4;
if (WaitForSingleObject(information.hProcess, 20000) != WAIT_OBJECT_0)
return 5;
DWORD code = 6;
GetExitCodeProcess(information.hProcess, &code);
CloseHandle(information.hThread);
CloseHandle(information.hProcess);
DestroyWindow(window);
return (int)code;
}