吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2854|回复: 39
收起左侧

[原创] 回调的故事

  [复制链接]
Panel 发表于 2023-4-8 22:31
本帖最后由 Panel 于 2023-4-9 11:11 编辑

回调的故事

概述:

讲述回调,举例通过回调监控进程、线程、映像加载状态以及进程回调函数禁止进程的创建

实验环境:

Windows 7 x86

实验工具:

Windbg、vs2013

0x1:什么是回调函数?

.回调函数(Callback Function)是一种常见的编程技术,用于将一个函数作为参数传递给另一个函数,并在需要时由另一个函数调用。回调函数通常用于实现异步操作、事件处理、消息通知等场景,可以使程序更加灵活和可扩展。GPT这样说,严谨但是晦涩,我来举例解释一下,比如:你妈妈给你分配了一个买菜的任务,要求就是你买了菜回来且要向她报告你买菜完成才算完成任务。那么此时,买菜回来是事件A,向妈妈报告买菜完成是事件B,那么B就是A的回调事件,按照常理来说我们只要成功把菜买回来就算是我们认为的买菜成功了,但实际上要加上和妈妈汇报才算得上真正地完成了买菜任务。这就是回调函数的概念。

0x2:进程、线程、映像加载回调

.向上面举例说的一样,当我们注册了进程、线程、映像的回调函数以后,那么它们的标志性完成是回调函数也执行完才算得上一个完整的事件。

.注册进程回调的函数:PsSetCreateProcessNotifyRoutine或者PsSetCreateProcessNotifyRoutineEx

.注册线程回调的函数:PsSetCreateThreadNotifyRoutine

.注册映像的回调函数:PsSetLoadImageNotifyRoutine

0x3:函数以及重要结构体简介

.进程注册回调函数

//注册与移除函数定义
NTSTATUS
PsSetCreateProcessNotifyRoutineEx (
    _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
    _In_ BOOLEAN Remove
    );
//回调函数原型
VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE_EX) (
    _Inout_ PEPROCESS Process,
    _In_ HANDLE ProcessId,
    _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
    );
//PPS_CREATE_NOTIFY_INFO结构
typedef struct _PS_CREATE_NOTIFY_INFO {
    _In_ SIZE_T Size;
    union {
        _In_ ULONG Flags;
        struct {
            _In_ ULONG FileOpenNameAvailable : 1;
            _In_ ULONG Reserved : 31;
        };
    };
    _In_ HANDLE ParentProcessId;
    _In_ CLIENT_ID CreatingThreadId;
    _Inout_ struct _FILE_OBJECT *FileObject;
    _In_ PCUNICODE_STRING ImageFileName;
    _In_opt_ PCUNICODE_STRING CommandLine;
    _Inout_ NTSTATUS CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

.PsSetCreateProcessNotifyRoutineEx中,NotifyRoutine是指向回调函数的指针,Remove代表是否移除指定回调函数,我们注册的时候就FALSE,当我们不想使用回调函数的时候则TRUE,让系统移除回调函数,回调函数中包含了进程的EPROCESS结构、进程ID、创建信息(如果是创建进程则这个参数就是创建进程的信息,如果是销毁的话这个参数是NULL)。

.注册线程回调的函数

//注册函数
NTSTATUS
PsSetCreateThreadNotifyRoutine(
    _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
    );
//移除函数
NTSTATUS
PsRemoveCreateThreadNotifyRoutine (
    _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine
    );
//回调函数原型
VOID
(*PCREATE_THREAD_NOTIFY_ROUTINE)(
    _In_ HANDLE ProcessId,
    _In_ HANDLE ThreadId,
    _In_ BOOLEAN Create
    );

.NotifyRoutine是指向回调函数的指针,回调函数中包含了线程的所属进程ID、线程ID、创建状态还是销毁状态,TRUE为创建,FALSE为销毁。

.注册映像文件加载回调的函数

//注册函数
NTSTATUS
PsSetLoadImageNotifyRoutine(
    _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
    );
//移除函数
NTSTATUS
PsRemoveLoadImageNotifyRoutine(
    _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
    );
//回调函数原型
VOID
(*PLOAD_IMAGE_NOTIFY_ROUTINE)(
    _In_ PUNICODE_STRING FullImageName,
    _In_ HANDLE ProcessId,                // pid into which image is being mapped
    _In_ PIMAGE_INFO ImageInfo
    );
//IMAGE_INFO
typedef struct _IMAGE_INFO {
    union {
        ULONG Properties;
        struct {
            ULONG ImageAddressingMode  : 8;  // Code addressing mode
            ULONG SystemModeImage      : 1;  // System mode image
            ULONG ImageMappedToAllPids : 1;  // Image mapped into all processes
            ULONG ExtendedInfoPresent  : 1;  // IMAGE_INFO_EX available
            ULONG MachineTypeMismatch  : 1;  // Architecture type mismatch
            ULONG ImageSignatureLevel  : 4;  // Signature level
            ULONG ImageSignatureType   : 3;  // Signature type
            ULONG Reserved             : 13;
        };
    };
    PVOID       ImageBase;
    ULONG       ImageSelector;
    SIZE_T      ImageSize;
    ULONG       ImageSectionNumber;
} IMAGE_INFO, *PIMAGE_INFO;

.NotifyRoutine是指向回调函数的指针,回调函数中包含了所加载映像文件的全路径名称、加载映像文件的进程ID、映像文件信息。

0x3:通过PsSetCreateProcessNotifyRoutineEx时的注意点

.当我以下面的函数注册进程回调函数时会得到注册状态返回值为0xC0000022,上网查阅便发现是访问被拒绝的意思,那么我们进入内核代码查看一下是啥原因,IDA Pro拖入ntkrnlpa.exe,找到PspSetCreateProcessNotifyRoutineEx函数F5一下,得到以下伪代码:

int __stdcall PspSetCreateProcessNotifyRoutine(int a1, char a2, char a3)
{
  _KTHREAD *CurrentThread; // esi
  int v4; // ebx
  _DWORD *v5; // eax
  void *v6; // edi
  int v7; // ecx
  volatile signed __int32 *v9; // eax
  void *v10; // ebx
  char *v11; // esi
  unsigned int v12; // edi
  char *i; // [esp+18h] [ebp+Ch]

  if ( a2 )
  {
    CurrentThread = KeGetCurrentThread();
    --CurrentThread->KernelApcDisable;
    v4 = 0;
    for ( i = (char *)&PspCreateProcessNotifyRoutine; ; i += 4 )
    {
      v5 = (_DWORD *)ExReferenceCallBackBlock(i);
      v6 = v5;
      if ( v5 )
        break;
LABEL_11:
      if ( (unsigned int)++v4 >= 0x40 )
      {
        if ( !++CurrentThread->KernelApcDisable
          && ($402C2D26A9C1C09F55BBA925DBF9FC47 *)CurrentThread->ApcState.ApcListHead[0].Flink != &CurrentThread->64
          && !CurrentThread->SpecialApcDisable )
        {
          KiCheckForKernelApcDelivery();
        }
        return 0xC000007A;
      }
    }
    if ( ExGetCallBackBlockRoutine(v5[2]) == a1 )
    {
      if ( v7 )
      {
        if ( a3 )
          goto LABEL_9;
      }
      else if ( !a3 )
      {
LABEL_9:
        if ( (unsigned __int8)ExCompareExchangeCallBack(v6) )
        {
          v9 = &PspCreateProcessNotifyRoutineCount;
          if ( a3 )
            v9 = &PspCreateProcessNotifyRoutineExCount;
          _InterlockedExchangeAdd(v9, 0xFFFFFFFF);
          ExDereferenceCallBackBlock(v6);
          if ( !++CurrentThread->KernelApcDisable
            && ($402C2D26A9C1C09F55BBA925DBF9FC47 *)CurrentThread->ApcState.ApcListHead[0].Flink != &CurrentThread->64
            && !CurrentThread->SpecialApcDisable )
          {
            KiCheckForKernelApcDelivery();
          }
          ExWaitForCallBacks(v6);
          ExFreeCallBack(v6);
          return 0;
        }
      }
    }
    ExDereferenceCallBackBlock(v6);
    goto LABEL_11;
  }
  if ( a3 && !MmVerifyCallbackFunction(a1) )
    return 0xC0000022;
  v10 = (void *)ExAllocateCallBack(a1, a3 != 0);
  if ( !v10 )
    return 0xC000009A;
  v11 = (char *)&PspCreateProcessNotifyRoutine;
  v12 = 0;
  while ( !(unsigned __int8)ExCompareExchangeCallBack(0) )
  {
    v12 += 4;
    v11 += 4;
    if ( v12 >= 0x100 )
    {
      ExFreeCallBack(v10);
      return 0xC000000D;
    }
  }
  if ( a3 )
  {
    _InterlockedExchangeAdd(&PspCreateProcessNotifyRoutineExCount, 1u);
    if ( (PspNotifyEnableMask & 4) == 0 )
      _interlockedbittestandset(&PspNotifyEnableMask, 2u);
  }
  else
  {
    _InterlockedExchangeAdd(&PspCreateProcessNotifyRoutineCount, 1u);
    if ( (PspNotifyEnableMask & 2) == 0 )
      _interlockedbittestandset(&PspNotifyEnableMask, 1u);
  }
  return 0;
}

.其中a1是我们传入的第一个参数,也就是回调函数地址,a2是我们传入的第二个参数,也就是是否移除回调函数,a3不知道是哪来的,先不管。一看代码便看到了我们的错误码所在:

if ( a3 && !MmVerifyCallbackFunction(a1) )
    return 0xC0000022;

.那此时我们知道只要条件不成立就不会返回权限不足这个错误,那我们有两种思路不让它返回0xC0000022,第一种便是直接调用int __stdcall PspSetCreateProcessNotifyRoutine(int a1, char a2, char a3)函数,设置a3为0,第二种就是让!MmVerifyCallbackFunction(a1)为0,也就是MmVerifyCallbackFunction(a1)返回非零,第一种有点暴力,不优雅,咱们这里选择第二种,那此时就跟进MmVerifyCallbackFunction(a1)中去看看:

int __stdcall MmVerifyCallbackFunction(unsigned int a1)
{
  char v1; // al
  _KTHREAD *CurrentThread; // esi
  int v4; // eax
  int v5; // [esp+10h] [ebp-4h]

  if ( a1 >= MiSystemRangeStart )
  {
    v1 = MiSystemVaType[(int)(((a1 >> 18) & 0x3FF8) - (((unsigned int)MmSystemRangeStart >> 18) & 0x3FF8)) >> 3];
    if ( v1 == 1 || v1 == 11 )
      return 0;
  }
  CurrentThread = KeGetCurrentThread();
  v5 = 0;
  --CurrentThread->KernelApcDisable;
  ExAcquireResourceSharedLite(&PsLoadedModuleResource, 1u);
  v4 = MiLookupDataTableEntry(a1, 1);           // 查找我们函数地址所在模块的LDR_DATA_TABLE_ENTRY,驱动模块的话其实也就是DriverSection的地址
  if ( v4 && (*(_BYTE *)(v4 + 0x34) & 0x20) != 0 )// 判断函数是否在模块中且LDR_DATA_TABLE_ENTRY的Flag成员是否为0x20,条件满足则过了检测
    v5 = 1;
  ExReleaseResourceLite(&PsLoadedModuleResource);
  if ( !++CurrentThread->KernelApcDisable
    && ($402C2D26A9C1C09F55BBA925DBF9FC47 *)CurrentThread->ApcState.ApcListHead[0].Flink != &CurrentThread->64
    && !CurrentThread->SpecialApcDisable )
  {
    KiCheckForKernelApcDelivery();
  }
  return v5;
}

.上面代码注释的地方便是使得返回值v5为非零值的地方,那我们继续看看要使得v5为1的条件:

 v4 = MiLookupDataTableEntry(a1, 1);           
 if ( v4 && (*(_BYTE *)(v4 + 0x34) & 0x20) != 0 )

.MiLookupDataTableEntry是查找指定地址所在模块的LDR_DATA_TABLE_ENTRY结构地址,那此时我们的模块便是驱动模块,它取了该结构偏移0x34位置的值去与运算上0x20,首先我们知道v4一定不为0,那剩下的就是要(*(_BYTE *)(v4 + 0x34) & 0x20)不为0了,也就是说这个表达式的值对应bit位没有0x20的话那这个if条件就不成立了,也就导致v5不能成为非零值了,那我们去看看LDR_DATA_TABLE_ENTRY结构偏移+0x34的位置是啥:

//部分内存结构
typedef struct _LDR_DATA_TABLE_ENTRY
{
        LIST_ENTRY InLoadOrderLinks;                                    //0x0
        LIST_ENTRY InMemoryOrderLinks;                                  //0x8
        LIST_ENTRY InInitializationOrderLinks;                          //0x10
        VOID* DllBase;                                                  //0x18
        VOID* EntryPoint;                                               //0x1c
        ULONG SizeOfImage;                                              //0x20
        UNICODE_STRING FullDllName;                                     //0x24
        UNICODE_STRING BaseDllName;                                     //0x2c
        ULONG Flags;                                                    //0x34                              
}LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

.从上面我们得知+0x34PDRIVER_OBJECT-->DriverSection[LDR_DATA_TABLE_ENTRY]-->Flag,那我们只需要把这个值或运算0x20就可以了。

0x4:实现进程、线程、映像文件加载的监控

//进程回调,打印进程创建或销毁时的信息
VOID CreateProcessCallBack(
        _Inout_ PEPROCESS Process,
        _In_ HANDLE ProcessId,
        _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
        )
{
        if (CreateInfo)
        {
                DbgPrintEx(77, 0, "[进程回调]进程创建,进程id为%d,名字为:%wZ\n", ProcessId, CreateInfo->ImageFileName);     
        }
        else
        {
                DbgPrintEx(77, 0, "[进程回调]进程销毁,进程id为%d\n", ProcessId);        
        }
}
//线程回调,打印线程创建或销毁时的信息
VOID CreateThreadCallBack(
        _In_ HANDLE ProcessId,
        _In_ HANDLE ThreadId,
        _In_ BOOLEAN Create
        )
{
        if (Create)
        {
                DbgPrintEx(77, 0, "[线程回调]线程创建,进程id为%d,线程id为%d\n", ProcessId, ThreadId);
        }
        else
        {
                DbgPrintEx(77, 0, "[线程回调]线程销毁,进程id为%d,线程id为%d\n", ProcessId,ThreadId);
        }
}
//映像加载回调,打印映像问价加载时的信息
VOID LoadImageCallBack(
_In_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId,                // pid into which image is being mapped
_In_ PIMAGE_INFO ImageInfo
)
{
        if (ImageInfo)
        {
                DbgPrintEx(77, 0, "[映像回调]映像加载,进程id为%d,映像类型值为%d,路径为%wZ\n", ProcessId, ImageInfo->SystemModeImage, FullImageName);

        }
        else
        {
                DbgPrintEx(77, 0, "[映像回调]映像销毁,进程id为%d\n", ProcessId);
        }
}
//注册以上回调函数
NTSTATUS ntStatu = 0;
//进程回调
PLDR_DATA_TABLE_ENTRY pSection = pDriver->DriverSection;
pSection->Flags |= 0x20;
ntStatu = PsSetCreateProcessNotifyRoutineEx(CreateProcessCallBack, FALSE);
DbgPrintEx(77, 0, "[db]进程回调注册状态值:%x\n", ntStatu);
//线程回调
ntStatu = PsSetCreateThreadNotifyRoutine(CreateThreadCallBack);
DbgPrintEx(77, 0, "[db]线程回调注册状态值:%x\n", ntStatu);
//映像回调
ntStatu = PsSetLoadImageNotifyRoutine(LoadImageCallBack);
DbgPrintEx(77, 0, "[db]映像回调注册状态值:%x\n", ntStatu);
//不打算使用回调时的移除
//移除进程回调
PsSetCreateProcessNotifyRoutineEx(CreateProcessCallBack, TRUE);
//移除线程回调
PsRemoveCreateThreadNotifyRoutine(CreateThreadCallBack);
//移除映像回调
PsRemoveLoadImageNotifyRoutine(LoadImageCallBack);
//完整代码
#include <ntifs.h>
#include <string.h>

//0x78 bytes (sizeof)
typedef struct _LDR_DATA_TABLE_ENTRY
{
        LIST_ENTRY InLoadOrderLinks;                                    //0x0
        LIST_ENTRY InMemoryOrderLinks;                                  //0x8
        LIST_ENTRY InInitializationOrderLinks;                          //0x10
        VOID* DllBase;                                                  //0x18
        VOID* EntryPoint;                                               //0x1c
        ULONG SizeOfImage;                                              //0x20
        UNICODE_STRING FullDllName;                                     //0x24
        UNICODE_STRING BaseDllName;                                     //0x2c
        ULONG Flags;                                                    //0x34                              
}LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

//进程回调
VOID CreateProcessCallBack(
        _Inout_ PEPROCESS Process,
        _In_ HANDLE ProcessId,
        _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
        )
{
        if (CreateInfo)
        {
                DbgPrintEx(77, 0, "[进程回调]进程创建,进程id为%d,名字为:%wZ\n", ProcessId, CreateInfo->ImageFileName);
        }
        else
        {
                DbgPrintEx(77, 0, "[进程回调]进程销毁,进程id为%d\n", ProcessId);        
        }
}

//线程回调
VOID CreateThreadCallBack(
        _In_ HANDLE ProcessId,
        _In_ HANDLE ThreadId,
        _In_ BOOLEAN Create
        )
{
        if (Create)
        {
                DbgPrintEx(77, 0, "[线程回调]线程创建,进程id为%d,线程id为%d\n", ProcessId, ThreadId);
        }
        else
        {
                DbgPrintEx(77, 0, "[线程回调]线程销毁,进程id为%d,线程id为%d\n", ProcessId,ThreadId);
        }
}
//映像模块回调
VOID LoadImageCallBack(
_In_ PUNICODE_STRING FullImageName,
_In_ HANDLE ProcessId,                // pid into which image is being mapped
_In_ PIMAGE_INFO ImageInfo
)
{
        if (ImageInfo)
        {
                DbgPrintEx(77, 0, "[映像回调]映像加载,进程id为%d,映像类型值为%d,路径为%wZ\n", ProcessId, ImageInfo->SystemModeImage, FullImageName);

        }
        else
        {
                DbgPrintEx(77, 0, "[映像回调]映像销毁,进程id为%d\n", ProcessId);
        }
}

VOID UnloadDriver(PDRIVER_OBJECT pDriver)
{
        //移除进程回调
        PsSetCreateProcessNotifyRoutineEx(CreateProcessCallBack, TRUE);
        //移除线程回调
        PsRemoveCreateThreadNotifyRoutine(CreateThreadCallBack);
        //移除映像回调
        PsRemoveLoadImageNotifyRoutine(LoadImageCallBack);
}

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pRegPath)
{
        NTSTATUS ntStatu = 0;
        //进程回调
        PLDR_DATA_TABLE_ENTRY pSection = pDriver->DriverSection;
        pSection->Flags |= 0x20;
        ntStatu = PsSetCreateProcessNotifyRoutineEx(CreateProcessCallBack, FALSE);
        DbgPrintEx(77, 0, "[db]进程回调注册状态值:%x\n", ntStatu);
        //线程回调
        ntStatu = PsSetCreateThreadNotifyRoutine(CreateThreadCallBack);
        DbgPrintEx(77, 0, "[db]线程回调注册状态值:%x\n", ntStatu);
        //映像回调
        ntStatu = PsSetLoadImageNotifyRoutine(LoadImageCallBack);
        DbgPrintEx(77, 0, "[db]映像回调注册状态值:%x\n", ntStatu);
        pDriver->DriverUnload = UnloadDriver;
        return STATUS_SUCCESS;
}

.效果图

image-20230408222017223.png

0x5:实现禁止创建指定进程

.首先要知道PsSetCreateProcessNotifyRoutineEx函数的触发时间是在进程完全创建之前。具体来说,当系统正在创建新进程时,PsSetCreateProcessNotifyRoutineEx注册的回调函数会在进程对象创建完成之后、但在用户空间初始化程序执行之前被调用。在这个时刻,进程的一些基本属性已经确定,如进程ID、父进程ID、映像名称等,但是进程的虚拟地址空间还未初始化,也没有执行用户空间的初始化程序。

.其实我们上面说过,回调函数原型中的第三个参数_Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo中包含了创建进程的信息,其中有一个成员非常重要:CreationStatus,这个成员只有为STATUS_SUCCESS时才能让进程正常加载,所以我们只要让他为STATUS_UNSUCCESSFUL不就可以让他加载失败了吗?这里_Inout和P开头已经告诉了我们这个参数是个指针可以双向传递,所以我们直接CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL就实现了,这里我举例禁止创建记事本进程为例子,在进程回调函数里过滤出只要创建的进程是记事本进程就不让它加载,核心代码如下:

VOID CreateProcessCallBack(
        _Inout_ PEPROCESS Process,
        _In_ HANDLE ProcessId,
        _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo
        )
{
        if (CreateInfo)
        {
                DbgPrintEx(77, 0, "[进程回调]进程创建,进程id为%d,名字为:%wZ\n", ProcessId, CreateInfo->ImageFileName);
                UNICODE_STRING uniDstName = RTL_CONSTANT_STRING(L"\\??\\C:\\Windows\\System32\\notepad.exe");
                if (RtlEqualUnicodeString(&uniDstName, CreateInfo->ImageFileName, TRUE))
                {
                        CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL;
                }
        }
        else
        {
                DbgPrintEx(77, 0, "[进程回调]进程销毁,进程id为%d\n", ProcessId);        
        }
}

.效果:

image-20230408222833973.png

0x6:总结

.回调被很多杀软利用,看自己想的思路吧。

免费评分

参与人数 15威望 +1 吾爱币 +32 热心值 +12 收起 理由
peiki + 1 + 1 我很赞同!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
jamessteed + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
he1a2s0 + 1 谢谢 @Thanks!
Reality1 + 1 + 1 用心讨论,共获提升!
owouwu + 1 + 1 谢谢@Thanks!
laosi520 + 1 我很赞同!
skiss + 1 + 1 谢谢@Thanks!
mn126kk72 + 1 + 1 我很赞同!
抱薪风雪雾 + 1 + 1 谢谢@Thanks!
Boly + 1 用心讨论,共获提升!
cdeng4 + 1 + 1 我很赞同!
zzlya + 1 用心讨论,共获提升!
ovohuang + 1 + 1 我很赞同!
wanjingbo + 1 用心讨论,共获提升!

查看全部评分

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

zuoyizhongguo 发表于 2023-4-10 10:36
感谢分享,回调在相机sdk里用的很经典,一旦获取帧,就触发回调函数,回调函数里对帧做一些处理(而且是不怎么耗时的处理,一般是把帧拷贝到全局变量)
clkr 发表于 2023-4-8 22:49
JW5120 发表于 2023-4-8 23:33
zjh889 发表于 2023-4-9 00:07
楼主太厉害了,俺们菜鸟,只能慢慢消化!
lixiang286 发表于 2023-4-9 00:34
完全看不懂,但是最近正在需要公众号和系统做一个回调设置, 不知道怎么弄
8970665 发表于 2023-4-9 06:30
好像很好用的
qigemuq 发表于 2023-4-9 08:57
感谢分享
cN2Fdgewewe1t6q 发表于 2023-4-9 10:21
大佬好强,不过学渣表示看不懂。
wyb1109_2008 发表于 2023-4-9 10:25
谢谢楼主分享。明白什么叫回调
Wwp780620 发表于 2023-4-9 10:58
感谢楼主的分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

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

GMT+8, 2024-5-3 15:52

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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