吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1328|回复: 22
上一主题 下一主题
收起左侧

[调试逆向] 反调试技巧:一种基于GUI线程环境快照的反调试方案

[复制链接]
跳转到指定楼层
楼主
忆魂丶天雷 发表于 2026-4-29 11:35 回帖奖励
本帖最后由 忆魂丶天雷 于 2026-5-6 20:00 编辑

一种基于GUI线程环境快照的反调试方案

摘要

大部分传统已公开反调试技术主要检查进程内部状态(PEB、DebugPort)或进程树关系(父进程检测),这些检测点已被 ScyllaHide 等反反调试插件系统性覆盖。本文提出一种将检测维度转移到桌面 GUI 环境的思路:在程序入口点通过对前台 GUI 线程信息做瞬时快照,判断自身是否由资源管理器等合法启动器启动。方案选择 GetGUIThreadInfo 作为核心 API,有意避开反反调试工具的常规 Hook 范围,提高了检测的隐蔽性。

1. 引言

当前 Windows 平台下的反反调试插件,如 ScyllaHide,已经对常见的反调试 API 提供了较为周全的绕过:

  • IsDebuggerPresent 返回 FALSE
  • NtQueryInformationProcess 检查调试端口时返回空
  • PEB 的 BeingDebugged 标志被清除
  • 父进程信息可随意伪造
  • GetForegroundWindowOutputDebugString 等 API 被 Hook

如果检测逻辑完全建立在上述 API 之上,那么在实际对抗中基本失效。为了摆脱这一困境,本文尝试一种新思路:不检查进程自身,而是去检查程序启动那一瞬间,桌面环境的前台焦点窗口属于谁

用户从资源管理器双击启动程序时,启动瞬间的前台 GUI 线程必然属于 explorer.exe。而由调试器启动时,这一条件不成立。这个差异更难被反反调试插件消除,因为它涉及的不是进程内部状态,而是运行环境的瞬时快照。

2. GetGUIThreadInfo 与 GUITHREADINFO 结构

GetGUIThreadInfo 是本文方案的核心 API,定义如下:

BOOL GetGUIThreadInfo(
  DWORD          idThread,
  PGUITHREADINFO pgui
);

参数 idThread 为 0 时,表示查询当前前台线程的 GUI 信息。函数将结果填充到 GUITHREADINFO 结构体中。根据微软文档,该结构体定义如下:

typedef struct tagGUITHREADINFO {
  DWORD cbSize;
  DWORD flags;
  HWND  hwndActive;
  HWND  hwndFocus;
  HWND  hwndCapture;
  HWND  hwndMenuOwner;
  HWND  hwndMoveSize;
  HWND  hwndCaret;
  RECT  rcCaret;
} GUITHREADINFO, *PGUITHREADINFO, *LPGUITHREADINFO;

本文重点关注 hwndFocus 字段。微软文档对其说明为:

A handle to the window that has the keyboard focus.

即拥有键盘焦点的窗口句柄。该窗口必定属于当前前台进程,这一特性使其可用于反推前台进程身份。

结构中其他字段,如 hwndActive(活动窗口)、hwndCapture(捕获鼠标的窗口)等,可作为辅助信息用于后续的多维交叉验证,增加攻击者伪造的难度。

3. 焦点窗口与前台窗口的区别

本节澄清两个概念:GetGUIThreadInfo 获取的焦点窗口(hwndFocus),与 GetForegroundWindow 获取的前台窗口。

前台窗口(Foreground Window) 是指当前与用户进行交互的顶层窗口。它是任务栏上高亮显示的应用程序主窗口。一个系统中同一时刻只有一个前台窗口。

焦点窗口(Focus Window) 是前台窗口(或其子窗口)中实际接收键盘输入的窗口。

两者的关键区别:

特性 前台窗口 焦点窗口
获取 API GetForegroundWindow GetGUIThreadInfohwndFocus
系统同时存在的数量 1 个 每个线程 1 个焦点窗口
层级 必为顶层窗口 可能是顶层窗口的子窗口
示例(资源管理器) 资源管理器主窗口 桌面 ListView(SysListView32

在本方案的启动检测场景中,用户双击桌面图标时,explorer.exe 是前台进程,hwndFocus 则可能指向其内部的桌面 ListView 控件。但无论 hwndFocus 指向的是顶层窗口还是子窗口,GetWindowThreadProcessId 获取到的进程 ID 都必然归属于 explorer.exe

正是这一点使得 GetGUIThreadInfo 不仅达成了检测目的,而且比 GetForegroundWindow 更具隐蔽优势——它不在主流反反调试插件的 Hook 列表之中。

4. 与父进程检测的对比

本方案与传统的父进程检测最终都可能涉及 explorer.exe,但两者的检测对象和对抗特性截然不同。下表从几个关键维度进行对比:

维度 父进程检测 本文方案(GUI环境快照)
检测对象 进程树的父子关系 启动瞬间的前台窗口归属
信息来源 进程对象中的父进程ID(静态数据) 桌面GUI线程的瞬时状态(动态快照)
被绕过的难度 低——修改父进程ID即可伪造 较高——需在极短时间内完成窗口抢占或伪装
主流插件覆盖 已被ScyllaHide等系统性处理 目前不在常规Hook清单中

父进程检测的弱点:父进程ID本质上是进程创建时写入的一个字段,存储在进程的EPROCESS结构中。调试器在调用 CreateProcess 时,可以通过 PROCESS_EXTENDED_BASIC_INFORMATION 等参数直接指定父进程。ScyllaHide 等插件已经将这一过程自动化,无需攻击者手动干预。

本文方案的区别:本文不问"谁创建了我",而是问"创建我的那一瞬间,屏幕最前面是谁"。这不是一个可以被预先设置的静态字段,而是一个需要在实际运行环境中实时呈现的状态。攻击者要在程序启动的毫秒级窗口内完成前台窗口的切换或伪装,操作复杂度远高于修改一个进程ID。

5. 核心原理

5.1 启动瞬间环境一致性

Windows 桌面是一个以 GUI 消息循环为基础的事件驱动系统。当用户通过 Windows Shell(explorer.exe)双击可执行文件时,系统会在前台线程处于 explorer.exe 的状态下创建新进程。从进程创建到入口点代码执行,其间虽然经历了加载器、系统 DLL 初始化等步骤,但在用户无额外操作的情况下,前台窗口不会发生改变。因此,在程序的入口点(WinMainmain)首次获得执行权时,前台 GUI 线程仍然归属于 explorer.exe

根据这一特性,可将启动场景划分为两类:

合法启动环境:程序由用户在桌面或资源管理器窗口中双击启动。在入口点调用 GetGUIThreadInfo(0, >i) 时,hwndFocus 所属进程为 explorer.exe(或白名单中的合法启动器,如 cmd.exe)。

异常启动环境:程序由调试器(如 x64dbg、OllyDbg)创建进程并启动(比如点击运行)。在入口点时,前台窗口通常属于调试器自身,hwndFocus 所属进程为调试器进程或其他非预期进程。

这两个环境状态的差异,构成了本方案检测的理论基础。攻击者若要消除这一差异,必须在进程创建的极短时间窗口内完成窗口切换或伪装,这在实践中比篡改进程内部数据更为困难。

5.2 检测流程

启动时刻的检测遵循以下链条:

  1. 用户双击桌面图标
  2. 系统创建进程,加载并执行到入口点
  3. 入口点第一时间调用 GetGUIThreadInfo(0, >i)
  4. gti.hwndFocus 获取窗口句柄然后通过窗口句柄获取其所属进程 ID
  5. 通过进程ID打开进程句柄,查询该进程的可执行文件路径
  6. 若路径指向 explorer.exe(或合法启动器),判定为正常启动;否则判定为可疑

整个链条的可靠性取决于第3步的执行时机。检测代码必须在入口点的最早期执行,以避免用户在此期间切换窗口导致焦点转移。

6. 方案设计

6.1 启动时刻检测

WinMain(或 main 的最早期)执行如下流程:

  1. 调用 GetGUIThreadInfo(0, >i)
  2. gti.hwndFocus 通过 GetWindowThreadProcessId 获取进程 ID
  3. 通过 OpenProcess + QueryFullProcessImageName 获取进程路径
  4. 检查路径是否为 explorer.exe 或白名单中的合法启动器(如 cmd.exe, powershell.exe, conhost.exe 等)

若不匹配,视为可疑。

6.2 运行时附加检测(拓展)

调试器还可以在程序运行后附加,此时启动检测早已过去。针对这一点,可以利用 SetWinEventHook 设置一个全局前台窗口变化钩子,维护"上一个前台窗口"的句柄。当发现自身窗口成为新前台窗口时,检查上一个窗口所属进程。若为已知调试器(如 x64dbg.exe),则可判定被附加。

6.3 检测结果的处理策略

当启动检测或运行时附加检测判定环境异常时,如何处理这一结果同样需要谨慎设计。一个常见的错误做法是直接针对已确认的调试器进程采取对抗动作,例如调用 TerminateProcess 结束调试器进程或者。这种方式虽然直接,但会立即暴露两个信息:

  1. 程序具备反调试能力;
  2. 反制触发的条件与时机(例如入口点或某个特定前台窗口变化时刻)。

攻击者可据此快速定位检测代码的位置和逻辑,进而绕过。

更隐蔽的处理策略包括以下几种:

延迟响应:不立即执行反制动作,而是在一个随机延时(如数分钟到数十分钟)后,或在程序执行到某个看似自然的业务逻辑节点时,再触发崩溃、退出或功能降级。这样可以将检测点与响应点在时间和空间上分离,增加逆向分析的难度。

静默记录与回传:将检测到的异常信息(如前台窗口句柄、时间戳、进程路径)以加密方式写入日志或通过网络回传至服务端,而不在本地做出任何明显反应。攻击者若无网络监控,难以察觉已被记录。

功能降级与误导:在检测到调试器后,不直接退出,而是将程序切换到一种功能受限但表面看来仍在正常运行的模式。例如,隐蔽地禁用核心算法、返回虚假计算结果,或故意引入难以察觉的逻辑错误,使逆向分析得到错误的理解。

利用已有句柄的间接干扰:当已获得调试器进程句柄(如通过 OpenProcess 打开的木马进程句柄),可以采取更温和的干扰手段而非直接终止。例如,向调试器窗口发送大量无关消息以干扰其消息循环,或尝试关闭其特定线程句柄,制造不稳定的运行环境,同时不易直接定位到自身程序。

崩溃伪装:触发一个表面看来是由于程序自身 bug 或系统异常导致的崩溃(例如在完全不相关的代码位置引发一个常见异常),使攻击者误以为程序不稳定,而非遭受反调试。

总的来说,反调试的结果处理应当遵循“检测时最小暴露,响应时最大混淆”的原则。在获得异常判定后,优先选择低交互痕迹、高延迟关联、多路径干扰的方式,而不是直接向调试器“宣战”。

7. 关键代码示例

注:以下代码为概念演示,代码均由 AI 生成,未经过安全审查,仅用于说明调用流程,请勿直接使用。

7.1 基础调用链

// 启动时刻检测
GUITHREADINFO gti = { sizeof(GUITHREADINFO) };
if (GetGUIThreadInfo(0, >i) && gti.hwndFocus) {
    DWORD pid = 0;
    GetWindowThreadProcessId(gti.hwndFocus, &pid);
    if (pid) {
        HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
        if (hProc) {
            WCHAR path[MAX_PATH] = { 0 };
            DWORD size = MAX_PATH;
            if (QueryFullProcessImageNameW(hProc, 0, path, &size)) {
                // 比较路径是否属于合法启动器
            }
            CloseHandle(hProc);
        }
    }
}

反附加示意(拓展):

HWND g_hPrevForeground = NULL;

void CALLBACK ForegroundHookProc(HWINEVENTHOOK hHook, DWORD event, HWND hwnd,
                                 LONG idObject, LONG idChild,
                                 DWORD dwEventThread, DWORD dwmsEventTime) {
    if (hwnd && idObject == 0 && idChild == 0) {
        if (hwnd == hOurMainWindow && g_hPrevForeground) {
            // 检查 g_hPrevForeground 所属进程是否为调试器
        }
        g_hPrevForeground = hwnd;
    }
}

void InstallHook() {
    SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
                    NULL, ForegroundHookProc, 0, 0,
                    WINEVENT_OUTOFCONTEXT);
}

7.2 API 被 Hook 的风险说明

上述调用链中,虽然 GetGUIThreadInfo 本身不在主流反反调试插件的 Hook 列表中,但链路上其他 API 并非完全安全。以下逐一说明:

GetWindowThreadProcessId:该函数用于从窗口句柄获取进程 ID。插件可能 Hook 此函数,当传入的窗口句柄属于调试器自身窗口时,返回一个伪造的进程 ID(例如返回 explorer.exe 的 PID)或者直接返回0。不过在本方案的使用场景中,hwndFocus 在正常启动时本身就属于 explorer.exe,插件若不加区分地对所有调用都进行伪造,反而可能导致正常桌面应用的误判。实际风险程度取决于插件的 Hook 策略是"针对调试器窗口返回假值"还是"对所有调用无差别返回假值"。前一种情况下本方案不受影响,后一种情况下则需要额外验证。

OpenProcess:插件可能阻止对受保护进程(如调试器自身)的 OpenProcess 调用,返回 NULL 或拒绝访问。在本方案中,如果 GetGUIThreadInfo 拿到的 hwndFocus 确实属于调试器,插件有可能拦截 OpenProcess 使其失败,从而阻止检测逻辑获取调试器的进程路径。这意味着调用失败本身就是一个可疑信号——一个正常的前台窗口进程不应该无法通过 PROCESS_QUERY_LIMITED_INFORMATION 打开。

QueryFullProcessImageName:即使 OpenProcess 成功,此函数也可能被 Hook 并返回伪造路径。例如,当查询的进程是调试器时,插件可能返回 explorer.exe 的路径。对抗方式包括:将返回的路径与硬编码的系统目录进行比对,或结合其他环境特征交叉验证。

GetClassName(用于辅助判断时):该函数可被 Hook 并返回任意类名。攻击者可将其自身窗口伪装成 ProgmanWorkerW 类名。建议不将类名作为唯一判据,而是与进程路径、光标位置等信息联合使用。

7.3 增强健壮性的示例

基于以上风险,实际部署时应考虑增加以下防御性检查:

// 增强版检测(概念示意)
BOOL IsLegitimateLaunch() {
    GUITHREADINFO gti = { sizeof(GUITHREADINFO) };
    if (!GetGUIThreadInfo(0, >i) || !gti.hwndFocus) {
        // 无法获取焦点窗口,可能是非交互启动
        return IsNonInteractiveSession();
    }

    DWORD pid = 0;
    if (!GetWindowThreadProcessId(gti.hwndFocus, &pid) || pid == 0) {
        // 无法获取进程ID,视为可疑
        return FALSE;
    }

    // 防御:检查 pid 是否为已知合法值(如当前会话的 explorer.exe)
    // 某些插件可能返回虚假的 pid,可与自身父进程列表交叉验证

    HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
    if (!hProc) {
        // 正常前台进程不应无法打开。无法打开本身即为可疑特征
        return FALSE;
    }

    WCHAR path[MAX_PATH] = { 0 };
    DWORD size = MAX_PATH;
    if (!QueryFullProcessImageNameW(hProc, 0, path, &size)) {
        CloseHandle(hProc);
        return FALSE;
    }
    CloseHandle(hProc);

    // 检查路径
    if (IsWhitelistedLauncher(path)) {
        return TRUE;
    }

    // 可选:交叉验证 GetForegroundWindow 所属进程
    // 若与 hwndFocus 的进程不一致,说明存在异常
    HWND hFg = GetForegroundWindow();
    if (hFg) {
        DWORD fgPid = 0;
        GetWindowThreadProcessId(hFg, &fgPid);
        if (fgPid != pid) {
            // 焦点窗口与前台窗口分属不同进程,环境异常
            return FALSE;
        }
    }

    return FALSE;
}

设计要点说明

  1. 失败即可疑:对于 OpenProcess 等调用,不应在失败时静默放行。一个正常的前台进程不应无法被查询基本信息,调用失败本身就是一个检测信号。
  2. 交叉验证:用 GetForegroundWindow 获取的前台窗口所属进程 ID,与 GetGUIThreadInfo 获取的焦点窗口进程 ID 进行比较。正常情况下两者应一致(焦点窗口属于前台进程),若不一致则说明存在 Hook 或异常窗口嵌套。
  3. 最小化 Hook 面:核心依赖仍然放在 GetGUIThreadInfo 上,其他 API 调用只作为辅助验证手段。即使辅助 API 被 Hook,只要 GetGUIThreadInfo 未被处理,仍然可以通过多条路径的不一致来发现异常。
  4. 不信任单一返回值:对任何从 API 获取的字符串或数值,都应假设其可能被篡改,并通过多条独立路径相互印证。

8. 局限与应对

8.1 时序窗口

本方案依赖于程序入口点执行时的前台窗口快照,这要求检测代码在初始化阶段尽可能早地执行。若程序在完成必要的运行时初始化(如加载依赖库、建立日志系统)之后才执行检测,其间用户可能已点击其他窗口或将焦点转移至其他应用程序,导致 GetGUIThreadInfo 捕获到非预期的焦点窗口,造成误判。缓解措施是尽量将检测逻辑置于入口函数的最前端,在非必要的初始化工作之前完成快照采集。

8.2 代码虚拟化保护(VM)的影响

当前,使用虚拟机保护技术(如 VMProtect、Themida 等)对关键代码进行混淆和虚拟化已经成为常见的软件保护手段。若将本文方案的检测代码置于虚拟化保护之下,其执行速度会显著降低——从进程创建到实际完成 GetGUIThreadInfo 调用的延迟可能从正常情况下的微秒级拉长到毫秒级甚至更高。在这段被拉长的延迟窗口内,用户有更多机会转移焦点或激活其他窗口,从而导致前台焦点窗口发生变化,引起误判。因此,若检测代码需要配合 VM 保护使用,建议仅对检测之外的其他逻辑进行虚拟化,或者将快照代码单独剥离、以明文形式尽早执行,以尽量降低时序不确定性带来的影响。

8.3 误报:合法启动渠道的多样性

8.3.1 误报的根源

本方案的核心判定逻辑基于一个朴素假设:程序正常启动时,前台焦点窗口应归属 explorer.exe(或少数合法启动器),因为用户通常是从桌面或资源管理器双击启动程序的。凡是不满足此条件的环境,均被视为“可疑”。

但现实中,“用户直接双击启动”这一行为可以通过多种合法渠道完成,并不总是经过 explorer.exe。当用户通过其他渠道启动程序时,前台窗口可能属于另一个完全正常的进程。此时严格套用上述判定逻辑,就会将合法用户标记为可疑。这便是误报产生的根源。

需要特别澄清的是,本方案检测的实质是“启动瞬间是否处于正常交互启动环境”。explorer.exe 只是这一环境最常见的前台进程载体,而非唯一的合法答案。因此,方案中的白名单并非判定逻辑的核心,而是对现实合法启动渠道多样性的必要补充。

8.3.2 常见误报场景
场景 前台窗口归属 特征
命令行启动(cmd/PowerShell/Windows Terminal) cmd.exepowershell.exewt.exe 进程非 explorer.exe,但属常见合法启动器
压缩包内双击运行(WinRAR/7-Zip/Bandizip) WinRAR.exe7zFM.exeBandizip.exe 文件先解压到 %TEMP%,从临时目录启动
浏览器下载后直接运行 msedge.exechrome.exefirefox.exe 下载栏点击“运行”,浏览器仍在前台
第三方文件管理器(Total Commander/Directory Opus) totalcmd.exedopus.exe 替代系统资源管理器,窗口类名与 explorer 不同
计划任务/开机自启 无前台窗口或系统服务进程 非交互式启动,GetGUIThreadInfo 可能返回空
多显示器/虚拟桌面 用户主屏上其他应用程序 副屏点击桌面,主屏前台窗口不切换
8.3.3 应对策略:白名单

上述场景的共同点在于,它们本身都是正常的用户行为,并非调试器启动。应对方式是将这些合法启动器加入白名单。

这里建议采用白名单模式而非黑名单模式,原因在于:黑名单(列出已知调试器进程并拦截)永远无法穷举所有调试器变种——攻击者可以重命名、定制或编写新的调试器,漏报不可避免。而白名单的语义更契合本方案的逻辑——“判断当前前台窗口是否可信”,而非“当前前台窗口是否已知恶意”。具体白名单建议如下:

  • 命令行类cmd.exepowershell.execonhost.exewt.exe
  • 压缩软件类WinRAR.exe7zFM.exeBandizip.exeHaoZip.exe
  • 浏览器类msedge.exechrome.exefirefox.exeiexplore.exe
  • 第三方文件管理器类totalcmd.exedopus.exe(可结合窗口类名 TTOTAL_CMDdopus.lister 辅助)
  • 系统资源管理器文件夹窗口:可通过窗口类名 CabinetWClassExploreWClass 辅助判断,无需额外加入进程级白名单

8.4 绕过

  • 调试器以无窗口方式启动目标进程,此时检测可能失效,需结合其他手段。
  • 攻击者可编写专门抢占前台并伪装 explorer 的工具,但极大增加绕过成本。

9. 结论

本方案将反调试的检测维度从进程内部转移到桌面 GUI 环境,利用 GetGUIThreadInfo 实现启动瞬间快照检测。它避开了当前反反调试插件的主战场,有效提高了反调试执行的成功率,一定程度上增加了攻击者的绕过成本。

附录

涉及相关 API 文档

开源反反调试插件

免费评分

参与人数 2吾爱币 +8 热心值 +2 收起 理由
willJ + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
杨辣子 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
无阻 发表于 2026-4-29 11:50

回帖奖励 +1 CB吾爱币

这种判断感觉会出现各种莫名其妙的bug  比如别的第三方美化桌面软件启动、音速启动等菜单软件。。
推荐
 楼主| 忆魂丶天雷 发表于 2026-4-29 11:59 |楼主
无阻 发表于 2026-4-29 11:50
这种判断感觉会出现各种莫名其妙的bug  比如别的第三方美化桌面软件启动、音速启动等菜单软件。。

所以重点还是在于拿到窗口句柄,进程句柄后如何去判断,本质上只是提供一个思路

有窗口句柄就可以进行一系列的检测,窗口标题,类名

拿到进程句柄后,也可以直接读内存特征,判断文件路径等一系列操作

对于需要加白名单的加白名单
4#
 楼主| 忆魂丶天雷 发表于 2026-4-29 11:36 |楼主
运用本方案的CM样本:
26.4动态调试工具反调试反附加CM
https://www.52pojie.cn/thread-2105302-1-1.html
(出处: 吾爱破解论坛)

感兴趣可以玩一玩
5#
田田爱崽崽 发表于 2026-4-29 11:41

回帖奖励 +1 CB吾爱币

感谢大佬分享
6#
sondycnc 发表于 2026-4-29 11:47

回帖奖励 +1 CB吾爱币

技术不到位,看不懂啊,
7#
CDCBB 发表于 2026-4-29 11:54

回帖奖励 +1 CB吾爱币

本帖最后由 CDCBB 于 2026-4-29 12:00 编辑

那如果把x32dbg.exe修改为explorer.exe,是否就可绕过楼主的检测?
8#
冥界3大法王 发表于 2026-4-29 13:23

回帖奖励 +1 CB吾爱币

搜集100种反调试的方法就可以自己造出新的插件了。。。
9#
m_h 发表于 2026-4-29 13:36

回帖奖励 +1 CB吾爱币

看着好复杂。。。
10#
萌哒哒小乐天 发表于 2026-4-29 14:11

回帖奖励 +1 CB吾爱币

看起来与检测父进程类似?判断父进程是否为合规程序什么的,不过用GUI快照倒是头一次听
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-10 16:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表