前言

winaflaflwindows 的移植版, winafl 使用 dynamorio 来统计代码覆盖率,并且使用共享内存的方式让 fuzzer 知道每个测试样本的覆盖率信息。本文主要介绍 winafl 不同于 afl 的部分,对于 afl 的变异策略等部分没有介绍,对于 afl 的分析可以看

  1. https://paper.seebug.org/496/#arithmetic

源码分析

winafl 主要分为两个部分 afl-fuzz.cwinafl.c , 前者是 fuzzer 的主程序 ,后面的是收集程序运行时信息的 dynamorio 插件的源码。

afl-fuzz

main

winafl 的入口时 afl-fuzz.c , 其中的 main 函数的主要代码如下


  1. int main(int argc, char** argv) {
  2. // 加载变异数据修正模块
  3. setup_post();
  4. if (!in_bitmap) memset(virgin_bits, 255, MAP_SIZE); // MAP_SIZE --> 0x00010000
  5. setup_shm(); // 设置共享内存
  6. init_count_class16();
  7. setup_dirs_fds(); // 设置模糊测试过程中的文件存放位置
  8. read_testcases(); // 读取测试用例到队列
  9. // 首先跑一遍所有的测试用例, 记录信息到样本队列
  10. perform_dry_run(use_argv);
  11. // 模糊测试主循环
  12. while (1) {
  13. u8 skipped_fuzz;
  14. // 每次循环从样本队列里面取测试用例
  15. cull_queue();
  16. // 对测试用例进行测试
  17. skipped_fuzz = fuzz_one(use_argv);
  18. queue_cur = queue_cur->next;
  19. current_entry++;
  20. }
  21. }
  • 首先设置一些 fuzz 过程中需要的状态值,比如共享内存、输入输出位置。
  • 然后通过 perform_dry_run 把提供的所有测试用例让目标程序跑一遍,同时统计执行过程中的覆盖率信息。
  • 之后就开始进行模糊测试的循环,每次取样本出来,然后交给 fuzz_one 对该样本进行 fuzz .

post_handler

该函数里面最重要的就是 fuzz_one 函数, 该函数的作用是完成一个样本的模糊测试,这里面实现了 afl 中的模糊测试策略,使用这些测试策略生成一个样本后,使用采用 common_fuzz_stuff 函数来让目标程序执行测试用例。common_fuzz_stuff 的主要代码如下

  1. static u8 common_fuzz_stuff(char** argv, u8* out_buf, u32 len) {
  2. u8 fault;
  3. // 如果提供了数据修正函数,则调用
  4. if (post_handler) {
  5. out_buf = post_handler(out_buf, &len);
  6. if (!out_buf || !len) return 0;
  7. }
  8. write_to_testcase(out_buf, len);
  9. // 让目标程序执行测试用例,并返回执行结果
  10. fault = run_target(argv, exec_tmout);

函数首先会判断是否提供了 post_handler , 如果提供了 post_handler 就会使用提供的 post_handler 对变异得到的测试数据进行处理, post_handler 函数指针在 setup_post 函数中设置。

  1. static void setup_post(void) {
  2. HMODULE dh;
  3. u8* fn = getenv("AFL_POST_LIBRARY"); // 通过环境变量获取 post_handler 所在 dll 的路径
  4. u32 tlen = 6;
  5. if (!fn) return;
  6. ACTF("Loading postprocessor from '%s'...", fn);
  7. dh = LoadLibraryA(fn);
  8. if (!dh) FATAL("%s", dlerror());
  9. post_handler = (u8* (*)(u8*,u32*))GetProcAddress(dh, "afl_postprocess"); // 加载dll 获取函数地址
  10. if (!post_handler) FATAL("Symbol 'afl_postprocess' not found.");
  11. /* Do a quick test. It's better to segfault now than later =) */
  12. post_handler("hello", &tlen);
  13. OKF("Postprocessor installed successfully.");
  14. }

该函数首先从 AFL_POST_LIBRARY 环境变量里面拿到 post_handler 所在 dll 的路径, 然后设置 post_handlerdll 里面的 afl_postprocess 函数的地址。该函数在 fuzzer 运行的开头会调用。 post_handler 的定义如下

  1. static u8* (*post_handler)(u8* buf, u32* len);
  2. 参数: buf 输入内存地址, len 输入内存的长度
  3. 返回值: 指向修正后的内存的地址

所以 afl_postprocess 需要接收两个参数, 然后返回一个指向修正后的内存的地址。post_handler 这个机制用于对测试数据的格式做简单的修正,比如计算校验和,计算文件长度等。

run_target

post_handler 这一步过后,会调用 write_to_testcase 先把测试用例写入文件,默认情况下测试用例会写入 .cur_input (用户可以使用 -f 指定)

  1. out_file = alloc_printf("%s\\.cur_input", out_dir);

然后调用 run_target 让目标程序处理测试用例,其主要代码如下

  1. static u8 run_target(char** argv, u32 timeout) {
  2. // 如果进程还存活就不去创建新的进程
  3. if(!is_child_running()) {
  4. destroy_target_process(0);
  5. create_target_process(argv); // 创建进程并且使用 dynamorio 监控
  6. fuzz_iterations_current = 0;
  7. }
  8. if (custom_dll_defined)
  9. process_test_case_into_dll(fuzz_iterations_current);
  10. child_timed_out = 0;
  11. memset(trace_bits, 0, MAP_SIZE);
  12. result = ReadCommandFromPipe(timeout);
  13. if (result == 'K')
  14. {
  15. //a workaround for first cycle in app persistent mode
  16. result = ReadCommandFromPipe(timeout);
  17. }
  18. // 当 winafl.dll 插桩准备好以后, 会通过命名管道发送 P
  19. if (result != 'P')
  20. {
  21. FATAL("Unexpected result from pipe! expected 'P', instead received '%c'\n", result);
  22. }
  23. // 让 winafl.dll 那端开始继续执行
  24. WriteCommandToPipe('F');
  25. result = ReadCommandFromPipe(timeout);
  26. // 接收到 K 就表示该用例运行正常
  27. if (result == 'K') return FAULT_NONE;
  28. if (result == 'C') {
  29. destroy_target_process(2000);
  30. return FAULT_CRASH;
  31. }
  32. destroy_target_process(0);
  33. return FAULT_TMOUT;
  34. }

首先会去判断目标进程是否还处于运行状态,如果不处于运行状态就新建目标进程,因为在 fuzz 过程中为了提升效率 ,会使用 dynamorio 来让目标程序不断的运行指定的函数,所以不需要每次 fuzz 都起一个新的进程。

然后如果需要使用用户自定义的方式发送数据。 就会使用 process_test_case_into_dll 发送测试用例,比如 fuzz 的目标是网络应用程序。

  1. static int process_test_case_into_dll(int fuzz_iterations)
  2. {
  3. char *buf = get_test_case(&fsize);
  4. result = dll_run_ptr(buf, fsize, fuzz_iterations); /* caller should copy the buffer */
  5. free(buf);
  6. return 1;
  7. }

这个 dll_run_ptr 在用户通过 -l 提供了dll 的路径后,winafl 会通过 load_custom_library 设置相关的函数指针

  1. void load_custom_library(const char *libname)
  2. {
  3. int result = 0;
  4. HMODULE hLib = LoadLibraryA(libname);
  5. dll_init_ptr = (dll_init)GetProcAddress(hLib, "_dll_init@0");
  6. dll_run_ptr = (dll_run)GetProcAddress(hLib, "_dll_run@12");
  7. }

winafl 自身也提供了两个示例分别是 tcp 服务和 tcp 客户端。在 dll_run_ptr 中也可以实现一些协议的加解密算法,这样就可以 fuzz 数据加密的协议了。

在一切准备好以后 winafl 往命名管道里面写入 F ,通知 winafl.dllwinafl 中实现代码覆盖率获取的dynamorio 插件)运行测试用例并记录覆盖率信息。 winafl.dll 执行完目标函数后会通过命名管道返回一些信息, 如果返回 K 表示用例没有触发异常,如果返回 C 表明用例触发了异常。

run_target 函数执行完毕之后, winafl 会对用例的覆盖率信息进行评估,然后更新样本队列。

winafl.c

这个文件里面包含了 winafl 实现的 dynamorio 插件,里面实现覆盖率搜集以及一些模糊测试的效率提升机制。

dr_client_main

该文件的入口函数是 dr_client_main

  1. DR_EXPORT void
  2. dr_client_main(client_id_t id, int argc, const char *argv[])
  3. {
  4. drmgr_init();
  5. drx_init();
  6. drreg_init(&ops);
  7. drwrap_init();
  8. options_init(id, argc, argv);
  9. dr_register_exit_event(event_exit);
  10. drmgr_register_exception_event(onexception);
  11. if(options.coverage_kind == COVERAGE_BB) {
  12. drmgr_register_bb_instrumentation_event(NULL, instrument_bb_coverage, NULL);
  13. } else if(options.coverage_kind == COVERAGE_EDGE) {
  14. drmgr_register_bb_instrumentation_event(NULL, instrument_edge_coverage, NULL);
  15. }
  16. drmgr_register_module_load_event(event_module_load);
  17. drmgr_register_module_unload_event(event_module_unload);
  18. dr_register_nudge_event(event_nudge, id);
  19. client_id = id;
  20. if (options.nudge_kills)
  21. drx_register_soft_kills(event_soft_kill);
  22. if(options.thread_coverage) {
  23. winafl_data.fake_afl_area = (unsigned char *)dr_global_alloc(MAP_SIZE);
  24. }
  25. if(!options.debug_mode) {
  26. setup_pipe();
  27. setup_shmem();
  28. } else {
  29. winafl_data.afl_area = (unsigned char *)dr_global_alloc(MAP_SIZE);
  30. }
  31. if(options.coverage_kind == COVERAGE_EDGE || options.thread_coverage || options.dr_persist_cache) {
  32. winafl_tls_field = drmgr_register_tls_field();
  33. if(winafl_tls_field == -1) {
  34. DR_ASSERT_MSG(false, "error reserving TLS field");
  35. }
  36. drmgr_register_thread_init_event(event_thread_init);
  37. drmgr_register_thread_exit_event(event_thread_exit);
  38. }
  39. event_init();
  40. }

函数的主要逻辑如下

  • 首先会初始化一些 dynamorio 的信息, 然后根据用户的参数来选择是使用基本块覆盖率(instrument_bb_coverage)还是使用边覆盖率(instrument_edge_coverage)。
  • 然后再注册一些事件的回调。
  • 之后就是设置命名管道和共享内存以便和 afl-fuzz 进行通信。

覆盖率记录

通过 drmgr_register_bb_instrumentation_event 我们就可以在每个基本块执行之前调用我们设置回调函数。这时我们就可以统计覆盖率信息了。具体的统计方式如下:

instrument_bb_coverage 的方式

  1. // 计算基本块的偏移并且取 MAP_SIZE 为数, 以便放入覆盖率表
  2. offset = (uint)(start_pc - mod_entry->data->start);
  3. offset &= MAP_SIZE - 1; // 把地址映射到 map中
  4. afl_map[offset]++

instrument_edge_coverage 的方式

  1. offset = (uint)(start_pc - mod_entry->data->start);
  2. offset &= MAP_SIZE - 1; // 把地址映射到 map中
  3. afl_map[pre_offset ^ offset]++
  4. pre_offset = offset >> 1

afl_map 适合 afl-fuzz 共享的内存区域, afl-fuzz 和 winafl.dll 通过 afl_map 来传递覆盖率信息。

效率提升方案

event_module_load会在每个模块被加载时调用,这个函会根据用户的参数为指定的目标函数设置一些回调函数,用来提升模糊测试的效率。主要代码如下:

  1. static void
  2. event_module_load(void *drcontext, const module_data_t *info, bool loaded)
  3. {
  4. if(options.fuzz_module[0]) {
  5. if(strcmp(module_name, options.fuzz_module) == 0) {
  6. if(options.fuzz_offset) {
  7. to_wrap = info->start + options.fuzz_offset;
  8. } else {
  9. //first try exported symbols
  10. to_wrap = (app_pc)dr_get_proc_address(info->handle, options.fuzz_method);
  11. if(!to_wrap) {
  12. DR_ASSERT_MSG(to_wrap, "Can't find specified method in fuzz_module");
  13. to_wrap += (size_t)info->start;
  14. }
  15. }
  16. if (options.persistence_mode == native_mode)
  17. {
  18. drwrap_wrap_ex(to_wrap, pre_fuzz_handler, post_fuzz_handler, NULL, options.callconv);
  19. }
  20. if (options.persistence_mode == in_app)
  21. {
  22. drwrap_wrap_ex(to_wrap, pre_loop_start_handler, NULL, NULL, options.callconv);
  23. }
  24. }
  25. module_table_load(module_table, info);
  26. }

在找到 target_module 中的 target_method 函数后,根据是否启用 persistence 模式,采用不同的方式给 target_method 函数设置一些回调函数,默认情况下是不启用 persistence 模式 , persistence 模式要求目标程序里面有不断接收数据的循环,比如一个 TCP 服务器,会循环的接收客户端的请求和数据。下面分别分析两种方式的源代码。

不启用 persistence

会调用

  1. drwrap_wrap_ex(to_wrap, pre_fuzz_handler, post_fuzz_handler, NULL, options.callconv);

这个语句的作用是在目标函数 to_wrap 执行前调用 pre_fuzz_handler 函数, 在目标函数执行后调用 post_fuzz_handler 函数。

下面具体分析

  1. static void
  2. pre_fuzz_handler(void *wrapcxt, INOUT void **user_data)
  3. {
  4. char command = 0;
  5. int i;
  6. void *drcontext;
  7. app_pc target_to_fuzz = drwrap_get_func(wrapcxt);
  8. dr_mcontext_t *mc = drwrap_get_mcontext_ex(wrapcxt, DR_MC_ALL);
  9. drcontext = drwrap_get_drcontext(wrapcxt);
  10. // 保存目标函数的 栈指针 和 pc 指针, 以便在执行完程序后回到该状态继续运行
  11. fuzz_target.xsp = mc->xsp;
  12. fuzz_target.func_pc = target_to_fuzz;
  13. if(!options.debug_mode) {
  14. WriteCommandToPipe('P');
  15. command = ReadCommandFromPipe();
  16. // 等待 afl-fuzz 发送 F , 收到 F 开始进行 fuzzing
  17. if(command != 'F') {
  18. if(command == 'Q') {
  19. dr_exit_process(0);
  20. } else {
  21. DR_ASSERT_MSG(false, "unrecognized command received over pipe");
  22. }
  23. }
  24. } else {
  25. debug_data.pre_hanlder_called++;
  26. dr_fprintf(winafl_data.log, "In pre_fuzz_handler\n");
  27. }
  28. //save or restore arguments, 第一次进入时保存参数, 以后都把保存的参数写入
  29. if (!options.no_loop) {
  30. if (fuzz_target.iteration == 0) {
  31. for (i = 0; i < options.num_fuz_args; i++)
  32. options.func_args[i] = drwrap_get_arg(wrapcxt, i);
  33. } else {
  34. for (i = 0; i < options.num_fuz_args; i++)
  35. drwrap_set_arg(wrapcxt, i, options.func_args[i]);
  36. }
  37. }
  38. memset(winafl_data.afl_area, 0, MAP_SIZE);
  39. // 把 覆盖率信息保存在 tls 里面, 在统计边覆盖率时会用到
  40. if(options.coverage_kind == COVERAGE_EDGE || options.thread_coverage) {
  41. void **thread_data = (void **)drmgr_get_tls_field(drcontext, winafl_tls_field);
  42. thread_data[0] = 0;
  43. thread_data[1] = winafl_data.afl_area;
  44. }
  45. }
  • 首先保存一些上下文信息,比如寄存器信息,然后通过命名管道像 afl-fuzz 发送 P 表示这边已经准备好了可以执行用例,然后等待 afl-fuzz 发送 F 后,就继续向下执行。
  • 然后如果是第一次执行,就保存函数的参数,否则就把之前保存的参数设置好。
  • 然后重置表示代码覆盖率的共享内存区域。

然后在 post_fuzz_handle 会根据执行的情况向 afl-fuzz 返回执行信息,然后根据情况判断是否恢复之前保存的上下文信息,重新准备开始执行目标函数。通过这种方式可以不用每次执行都新建一个进程,提升了 fuzz 的效率。


  1. static void
  2. post_fuzz_handler(void *wrapcxt, void *user_data)
  3. {
  4. dr_mcontext_t *mc;
  5. mc = drwrap_get_mcontext(wrapcxt);
  6. if(!options.debug_mode) {
  7. WriteCommandToPipe('K'); // 程序正常执行后发送 K 给 fuzz
  8. } else {
  9. debug_data.post_handler_called++;
  10. dr_fprintf(winafl_data.log, "In post_fuzz_handler\n");
  11. }
  12. /*
  13. We don't need to reload context in case of network-based fuzzing.
  14. 对于网络型的 fuzz , 不需要reload.执行一次就行了,这里直接返回
  15. */
  16. if (options.no_loop)
  17. return;
  18. fuzz_target.iteration++;
  19. if(fuzz_target.iteration == options.fuzz_iterations) {
  20. dr_exit_process(0);
  21. }
  22. // 恢复 栈指针 和 pc 到函数的开头准备下次继续运行
  23. mc->xsp = fuzz_target.xsp;
  24. mc->pc = fuzz_target.func_pc;
  25. drwrap_redirect_execution(wrapcxt);
  26. }

启用 persistence

fuzz 网络应用程序时,应该使用该模式

  1. -persistence_mode in_app

在这个模式下,对目标函数的包装就没有 pre_fuzz....post_fuzz..... 了, 此时就是在每次运行到目标函数就清空覆盖率, 因为程序自身会不断的调用目标函数。

  1. /* 每次执行完就简单的重置 aflmap, 这种模式适用于程序自身就有循环的情况 */
  2. static void
  3. pre_loop_start_handler(void *wrapcxt, INOUT void **user_data)
  4. {
  5. void *drcontext = drwrap_get_drcontext(wrapcxt);
  6. if (!options.debug_mode) {
  7. //let server know we finished a cycle, redundunt on first cycle.
  8. WriteCommandToPipe('K');
  9. if (fuzz_target.iteration == options.fuzz_iterations) {
  10. dr_exit_process(0);
  11. }
  12. fuzz_target.iteration++;
  13. //let server know we are starting a new cycle
  14. WriteCommandToPipe('P');
  15. //wait for server acknowledgement for cycle start
  16. char command = ReadCommandFromPipe();
  17. if (command != 'F') {
  18. if (command == 'Q') {
  19. dr_exit_process(0);
  20. }
  21. else {
  22. char errorMessage[] = "unrecognized command received over pipe: ";
  23. errorMessage[sizeof(errorMessage)-2] = command;
  24. DR_ASSERT_MSG(false, errorMessage);
  25. }
  26. }
  27. }
  28. else {
  29. debug_data.pre_hanlder_called++;
  30. dr_fprintf(winafl_data.log, "In pre_loop_start_handler\n");
  31. }
  32. memset(winafl_data.afl_area, 0, MAP_SIZE);
  33. if (options.coverage_kind == COVERAGE_EDGE || options.thread_coverage) {
  34. void **thread_data = (void **)drmgr_get_tls_field(drcontext, winafl_tls_field);
  35. thread_data[0] = 0;
  36. thread_data[1] = winafl_data.afl_area;
  37. }
  38. }

总结

通过对 afl-fuzz.c 的分析,我们知道 winafl 提供了两种有意思的功能,即数据修正功能自定义数据发送功能。这两种功能可以辅助我们对一些非常规目标进行 fuzz, 比如网络协议、数据加密应用。通过对 winafl.c 可以清楚的知道如何使用 dynamorio 统计程序的覆盖率, 并且明白了 winafl 通过多次在内存中执行目标函数来提升效率的方式, 同时也清楚了在程序内部自带循环调用函数时,可以使用 persistence 模式来对目标进行 fuzz,比如一些网络服务应用。

参考

https://paper.seebug.org/496/#arithmetic

http://riusksk.me/2019/02/02/winafl%E4%B8%AD%E5%9F%BA%E4%BA%8E%E6%8F%92%E6%A1%A9%E7%9A%84%E8%A6%86%E7%9B%96%E7%8E%87%E5%8F%8D%E9%A6%88%E5%8E%9F%E7%90%86/

https://paper.seebug.org/323/#3-winafl-fuzzer

winafl 源码分析的更多相关文章

  1. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  2. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  3. nginx源码分析之网络初始化

    nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网 ...

  4. zookeeper源码分析之五服务端(集群leader)处理请求流程

    leader的实现类为LeaderZooKeeperServer,它间接继承自标准ZookeeperServer.它规定了请求到达leader时需要经历的路径: PrepRequestProcesso ...

  5. zookeeper源码分析之四服务端(单机)处理请求流程

    上文: zookeeper源码分析之一服务端启动过程 中,我们介绍了zookeeper服务器的启动过程,其中单机是ZookeeperServer启动,集群使用QuorumPeer启动,那么这次我们分析 ...

  6. zookeeper源码分析之三客户端发送请求流程

    znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个功能是zookeeper对于应用最重要的特性,通过这个特性可以实现的功能包括配置的 ...

  7. java使用websocket,并且获取HttpSession,源码分析

    转载请在页首注明作者与出处 http://www.cnblogs.com/zhuxiaojie/p/6238826.html 一:本文使用范围 此文不仅仅局限于spring boot,普通的sprin ...

  8. ABP源码分析二:ABP中配置的注册和初始化

    一般来说,ASP.NET Web应用程序的第一个执行的方法是Global.asax下定义的Start方法.执行这个方法前HttpApplication 实例必须存在,也就是说其构造函数的执行必然是完成 ...

  9. ABP源码分析三:ABP Module

    Abp是一种基于模块化设计的思想构建的.开发人员可以将自定义的功能以模块(module)的形式集成到ABP中.具体的功能都可以设计成一个单独的Module.Abp底层框架提供便捷的方法集成每个Modu ...

随机推荐

  1. Spring Cloud 微服务技术整合

    微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些 ...

  2. Linux字符设备驱动基本结构

    1.Linux字符设备驱动的基本结构 Linux系统下具有三种设备,分别是字符设备.块设备和网络设备,Linux下的字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中某一数据,读取数据 ...

  3. pvs显示unknown device

    一 .不要unknown的那块pv盘的解决办法 [root@gezi ~]# pvs WARNING: Device for PV D1LLfT-3Hle-NbrP-5165-Q6WR-2UWF-2x ...

  4. layui switch 确定之后才变更状态

    let x = data.elem.checked; data.elem.checked = !x; form.render(); 完整代码 form.on('switch(is_enable)', ...

  5. LeetCode 453. 最小移动次数使数组元素相等(Minimum Moves to Equal Array Elements) 47

    453. 最小移动次数使数组元素相等 453. Minimum Moves to Equal Array Elements 题目描述 给定一个长度为 n 的非空整数数组,找到让数组所有元素相等的最小移 ...

  6. SQL Server 2019 新函数Approx_Count_Distinct

    2019年11月4日微软发布了2019正式版,该版本有着比以往更多强大的新功能和性能上的优势,可参阅SQL Server 2019 新版本. SQL Server 2019具有一组丰富的增强功能和新功 ...

  7. Linux目录结构(目录结构详解是重点)

    1.Linux目录与Windows目录对比 1.Windows目录结构 2.Linux目录结构 深刻理解Linux 树状文件目录是非常重要的,只有记住他们,你才能在命令行中任意切换,想去哪里去哪里 2 ...

  8. 《JAVA高并发编程详解》-wait和sleep

  9. 【Spring-AOP-学习笔记】

    http://outofmemory.cn/java/spring/spring-DI-with-annotation-context-component-scan https://www.cnblo ...

  10. System.InvalidOperationException:“寄宿 HWND 必须是子窗口。”

    原文:System.InvalidOperationException:"寄宿 HWND 必须是子窗口." 当试图在 WPF 窗口中嵌套显示 Win32 子窗口的时候,你有可能出现 ...