某会员商店App的api接口分析

news2024/11/24 16:36:21

1、目的

探索学习app接口的加解密机制,并通过api模拟调用的方式,发起业务请求。仅供学习。

2、工具准备

样本App版本:v5.0.80,v5.0.90

设备:Oppo R9s(Android7.1.1)+ MacOS Big Sur(Intel)

注入框架:xposed、frida(hluda 15.2.2)

反编译&其他:JEB、jadx、Charles

3、过程

大致分为抓包、脱壳、反编译、动态调试/加解密算法探索,构造模拟请求几个步骤,每个步骤都可能有不同的异常出现,本文主要记录在过程中的主体脉络和流程,过程中会附上关键代码。

3.1 抓包

首先尝试在手机上配置wifi代理,但Charles中无法看到相应的包记录。猜测是因为App屏蔽了网络代理,因此改用其他方式。手机上安装Drony,并开启手机全局网络代理(类型选择:socks5),代理地址指向Chares,此时就可以愉快的看到请求记录了。

如果是通过iOS抓包,直接通过小火箭抓包也是灰常方便。另外下载Drony App可能需要TZ,解决无法访问的问题。

在抓到的报文中,可以看到每次请求中,都包含了一些奇怪的header,比如t、spv、n、st,这些字段大概率与api接口的加密与签名有关。接下来,需要结合代码进一步分析。

3.2 脱壳&反编译

直接通过Xposed + 反射大师App,即可做到轻松脱壳,App未针对Xposed做检测。脱壳后得到7个dex文件,使用python脚本合并,将7个dex文件利用jadx全部反编译成Java文件到同一目录,即可直接翻阅App反编译后的源码。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

import os, sys

# 合并dex

# e.g:  python3 merge_dex.py ./source_dir/ output_dir

if __name__ == "__main__":

    if len(sys.argv) < 3:

        print("start error")

        sys.exit()

    source_dir = sys.argv[1]

    output_dir = sys.argv[2]

    print(source_dir, output_dir)

files = os.listdir(source_dir)

for file in files:

    if file.find(".dex") > 0:

        sh = '{your_path}/bin/jadx -Pdex-input.verify-checksum=no -j 1 -r -d ' + output_dir + " " + source_dir + file

        print(sh)

        os.system(sh)

这时直接在反编译的结果中搜索关键词"spv",却发现找不到。难道这些字段都隐藏到so中了,那就麻烦了。这时使用JEB再次反编译试试看,再次搜索"spv",找到了。

 

这里,要提醒一下:针对反编译,同样的dex文件,用不同的反编译工具,结果也会不一样,可读性差异很多,因此当使用一种工具反编译失败的话,可以尝试用不同的工具,比如,通用一段代码的反编译结果,使用jadx时,提示反编译失败,如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

/* JADX WARN: Code restructure failed: missing block: B:61:0x017a, code lost:

    r0 = r8.a("ssk");

    b.f.b.l.a(r0);

    r3 = r8.a("siv");

    b.f.b.l.a(r3);

    cn.xxxxclub.app.base.h.z.a(r0, r3);

 */

/* JADX WARN: Removed duplicated region for block: B:54:0x0168 A[Catch: Exception -> 0x018c, TryCatch #0 {Exception -> 0x018c, blocks: (B:42:0x0138, B:46:0x0154, B:48:0x015c, B:54:0x0168, B:56:0x0170, B:61:0x017a, B:45:0x014d), top: B:66:0x0138 }] */

/*

    Code decompiled incorrectly, please refer to instructions dump.

    To view partially-correct add '--show-bad-code' argument

*/

public okhttp3.ad intercept(okhttp3.w.a r19) {

    /*

        Method dump skipped, instructions count: 415

        To view this dump add '--comments-level debug' option

    */

    throw new UnsupportedOperationException("Method not decompiled: cn.xxxxclub.app.e.c.intercept(okhttp3.w$a):okhttp3.ad");

}

但是使用JEB时,结果则基本可用,如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

public ad intercept(w.a arg19) {

      ....(略)

      String v8_1 = String.valueOf(z.b());

      v4_1.b("t", v8_1);

      l.b("dc1ad18e-3e2d-4d49-a303-f637c6a5a3fb""randomUUID().toString()");

      String v9_1 = b.m.g.a("dc1ad18e-3e2d-4d49-a303-f637c6a5a3fb""-"""false4null);

      v4_1.b("n", v9_1);

      v4_1.b("sy""0");

      int v10_1 = v10 == 0 || !cn.xxxxclub.app.base.manager.d.a.i() ? 0 1;

      String v5_4 = this.a(((boolean)v10_1), v8_1 + v5_3 + v9_1 + g.a.b());

      if(((CharSequence)v5_4).length() > 0) {

          v4_1.b("st", v5_4);

      }

      v4_1.b("sny", (v10_1 == 0 "j" "c"));

      v4_1.b("rcs""1");

      v4_1.b("spv""1.1");

      if(v11) {

          String v5_5 = URLEncoder.encode(String.valueOf(cn.xxxxclub.app.base.manager.f.a.b()), "utf-8");

          l.b(v5_5, "encode(LocationManager.g…de().toString(), \"utf-8\")");

          v4_1.b("Local-Longitude", v5_5);

          String v5_6 = URLEncoder.encode(String.valueOf(cn.xxxxclub.app.base.manager.f.a.a()), "utf-8");

          l.b(v5_6, "encode(LocationManager.g…de().toString(), \"utf-8\")");

          v4_1.b("Local-Latitude", v5_6);

      }

      ....

      return v5_7;

  }

3.3 动态调试分析

拿到反编译源码后,接下来就需要结合frida动态分析代码调用链,找到api调用的核心算法逻辑并加以验证。

在App最新版本v5.0.90上,连接frida客户端。frida注入失败。随后换了hluda、xcube等方案均以失败告终,看了下app的加固方案,使用的腾讯的加固方案,对应的壳文件是libshell-super.cn.xxxxclub.app.so,尝试绕过壳的反注入逻辑,也没有效果。

这时偶然看到旧版本的app使用的壳文件是libshell-super.2019.so,灵光一闪,感觉旧版本的app上应该有机会,于是下载安装v5.0.80,frida注入成功了。app上开启了强制更新,于是在Charles上hook重写了app检查更新接口的返回结果,让app检查不到新版本,app仍然可以继续使用(后续有风险,历史接口可能下线)。

旧版本app上也可以使用frida工具集:Objection,通过调试和代码比对,基本确认了核心的算法签名逻辑位置:

 

签名的传入参数为分别为:t - 时间戳、data_json - 按json序列化后的业务对象参数、n - 去掉"-"符号后的uuid(32位字符串)、auth_token - 登录后用户令牌,按照如下规则排列所得:

1

"{t}{data_json}{n}{auth_token}"

返回字符串即为签名结果 - st

该签名算法有使用native方法,具体算法逻辑应该需要反汇编相应的so文件了。签名规则已经基本明确了,直接调用java层方法,走RPC调用即可得到我们想要的结果。偷懒了,就不去深挖汇编代码了,笔者也不确认一定能找到结果-_-||

3.4 RPC调用

1)创建js文件app_inject.js,声明rpc接口

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

var g_instance = null;

Java.enumerateClassLoaders({

    onMatch: function (loader) {

        try {

            if (loader.findClass("cn.xxxxclub.app.e.c")) {

                Java.classFactory.loader = loader;

                g_instance = Java.use("cn.xxxxclub.app.e.c").$new();

                console.log("target found!")

            }

        catch (error) {}

    }, onComplete: function () {

    }

});

// boolean z, String str

function sign(z, text){

    console.log("js7 start run: sign", g_instance, text)

    var result = g_instance.a.overload('boolean''java.lang.String').call(g_instance, z, text);

    console.log("result = ", result)

    return result

}

rpc.exports = {

    getsign: sign,

    hello: function () {

        return 'hello';

    }

}

console.log("injected.")

2)创建frida客户端,声明rpc调用。文件名:frida_client.py

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

import frida

import time

class FridaClient:

    class StartMode:

        attach = 'attach'

        spawn = 'spawn'

    def __init__(self, package_name, js_file, mode=StartMode.attach, delay_sec_4_spawn=2):

        self.results = {}

        self.script = None

        self.package_name = package_name

        self.delay_sec_4_spawn = delay_sec_4_spawn

        self.mode = mode

        self.js_file = js_file

    def on_message(self, message, data):

        if message['type'== 'send':

            payload = message['payload']

            print("[on_message]:", payload)

        else:

            print(message)

    def start(self):

        print(f"starting frida client with mode: {self.mode} ... pkg = [{self.package_name}]")

        if self.mode == FridaClient.StartMode.attach:

            session = frida.get_device_manager().add_remote_device("127.0.0.1:1234").attach(self.package_name)

        elif self.mode == FridaClient.StartMode.spawn:

            device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")

            pid = device.spawn([self.package_name])

            device.resume(pid)

            time.sleep(self.delay_sec_4_spawn)

            session = device.attach(pid)

        with open(self.js_file, 'r') as f:

            js_code = f.read()

        script = session.create_script(js_code)

        script.on('message'self.on_message)

        self.script = script

        script.load()

        print("load ready")

    def stop(self):

        if self.script:

            self.script.unload()

        self.script = None

    def get_sign(self, text: str):

        return self.script.exports.getsign(True, text)

3)构造参数,发起RPC调用。文件名:demo.py

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

# -*- coding: utf-8 -*-

import json

import time

import uuid

import requests

from frida_client import FridaClient

def _headers(auth_token, device_id, t, n, signed, lon, lat):

    return {

        'system-language''CN',

        'device-type''android',

        'tpg''1',

        'app-version''5.0.80',

        'device-id': device_id,

        'device-os-version''7.1.1',

        'device-name''OPPO_OPPO+R9s',

        'treq-id''1540d0ec530741abbab593af41966110.313.17103985647343144',

        'auth-token': auth_token,

        'longitude': lon,

        'latitude': lat,

        'p''1656120205',

        't': t,

        'n': n,

        'sy''0',

        'st': signed,

        'sny''c',

        'rcs''1',

        'spv''1.1',

        'Local-Longitude''0.0',

        'Local-Latitude''0.0',

        'Content-Type''application/json;charset=utf-8',

        'Host''api-xxxx.walmartmobile.cn',

        'User-Agent''okhttp/4.8.1'

    }

def work():

    frida_client = FridaClient(package_name='cn.xxxxclub.app', js_file='app_inject.js', mode=FridaClient.StartMode.spawn)

    frida_client.start()

    url = "https://api-xxxx.walmartmobile.cn/api/v1/xxxx/goods-portal/spu/search"

    device_id = 'b9fb859f7cfeb98ef39a31c410001f716c04'

    user_uid = '181864991321'

    auth_token = '740d926b981716f45de7a402b7b6761a46d9af48f752262b77a2cb0701d482f20c60e6345685b46681a1c23129bdffad022e2e75f60ac763'

    lon, lat = '114.151608''22.554734'

    # t = '1711440481379'

    = f"{int(time.time() * 1000)}"

    goods_name = '蛋糕'

    data = {

        "userUid": user_uid,

        "pageNum"1,

        "pageSize"20,

        "keyword": goods_name,

        "rewriteWord": goods_name,

        "filter": [],

        "storeInfoVOList": [

            {

                "storeId"9991,

                "storeType"32,

                "storeDeliveryAttr": [10]

            },

            {

                "storeId"6758,

                "storeType"256,

                "storeDeliveryAttr": [2345691213]

            },

            {

                "storeId"6580,

                "storeType"2,

                "storeDeliveryAttr": [713]

            },

            {

                "storeId"9992,

                "storeType"8,

                "storeDeliveryAttr": [1]

            }

        ],

        "addressVO": {

            "cityName": "",

            "countryName": "",

            "detailAddress": "",

            "districtName": "",

            "provinceName": ""

        },

        "uid": device_id,

        "uidType"3,

        "sort""0"

    }

    = str(uuid.uuid4()).replace('-', '')

    data_json = json.dumps(data, indent=None, separators=(','':'), ensure_ascii=False)

    signed = frida_client.get_sign(text=f"{t}{data_json}{n}{auth_token}")

    headers = _headers(auth_token=auth_token, device_id=device_id, t=t, n=n, signed=signed, lon=lon, lat=lat)

    response = requests.request("POST", url, headers=headers, data=data_json.encode('utf-8'))

    print(response.text)

work()

再看看结果,已经成功得到响应数据了。大功告成!

3.5 踩坑说明

在执行frida js注入时,Java.enumerateClassLoaders()仅支持Android 7.0及以上系统,若使用低版本的Android系统,如Android 6.1,则需要使用send(),进行消息异步通知。当采用异步通知时,在Python客户端的编码中,需要定义消息回调函数,同时将异步调用封装成同步调用,方便上游调用使用。对应的js代码和python代码如下:

app_inject_for_android_6.0.js:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

var g_instance = null;

// boolean z, String str

function sign(msgId, z, text){

    Java.perform(function(){

        console.log("js start run: sign", g_instance, text)

        try {

            if (g_instance == null) {

                g_instance = Java.use('cn.xxxxclub.app.e.c').$new();

                console.log("init instance success")

            }

            var result = g_instance.a.overload('boolean''java.lang.String').call(g_instance, z, text);

            send({'msgId': msgId, 'content': result})

        catch (e) {}

        return result

    });

}

rpc.exports = {

    getsign: sign,

    hello: function () {

        return 'hello';

    }

}

log("injected.")

frida_client_for_android_6.0.js

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

import uuid

import frida

import threading

import time

class FridaClient:

    class StartMode:

        attach = 'attach'

        spawn = 'spawn'

    def __init__(self, package_name, js_file, mode=StartMode.attach, delay_sec_4_spawn=2):

        self.results = {}

        self.script = None

        self.package_name = package_name

        self.event = threading.Event()

        self.result_queue = []

        self.delay_sec_4_spawn = delay_sec_4_spawn

        self.mode = mode

        self.js_file = js_file

    def on_message(self, message, data):

        if message['type'== 'send':

            payload = message['payload']

            msdId = payload['msgId']

            content = payload['content']

            print("[on_message]:", msdId, content)

            # 将结果存入队列

            self.result_queue.append((msdId, content))

            # 设置事件,通知主线程结果已经准备好

            self.event.set()

        else:

            print(message)

    def get_result(self, msgId):

        # 返回指定id的结果

        for idx, (id, result) in enumerate(self.result_queue):

            if id == msgId:

                del self.result_queue[idx]

                return result

        return None

    def start(self):

        print(f"starting frida client with mode: {self.mode} ... pkg = [{self.package_name}]")

        if self.mode == FridaClient.StartMode.attach:

            # session = frida.get_usb_device().attach(self.package_name)

            session = frida.get_device_manager().add_remote_device("127.0.0.1:1234").attach(self.package_name)

        elif self.mode == FridaClient.StartMode.spawn:

            device = frida.get_device_manager().add_remote_device("127.0.0.1:1234")

            pid = device.spawn([self.package_name])

            device.resume(pid)

            time.sleep(self.delay_sec_4_spawn)

            session = device.attach(pid)

        with open(self.js_file, 'r') as f:

            js_code = f.read()

        script = session.create_script(js_code)

        script.on('message'self.on_message)

        self.script = script

        script.load()

        print("load ready")

    def stop(self):

        # 停止脚本和会话

        if self.script:

            self.script.unload()

        self.script = None

    def get_sign_sync(self, text: str, timeout=5, poll_interval=0.1, max_polls=3):

        """

            因为rpc调用结果是异步返回的,因此通过线程等待唤醒的方式,得到结果后才返回,以此达到接口数据同步返回的效果

        """

        msgId = str(uuid.uuid4())

        self.script.exports.getsign(msgId, True, text)

        # 等待事件,设置超时

        self.event.wait(timeout=timeout)

        self.event.clear()  # 清除事件,以便下次使用

        # 返回结果

        result = self.get_result(msgId)

        if result is None:

            # 如果超时未收到结果,启动轮询

            start_time = time.time()

            poll_count = 0

            while time.time() - start_time < timeout and poll_count < max_polls:

                result = self.get_result(msgId)

                if result is not None:

                    break

                poll_count += 1

                time.sleep(poll_interval)

        return result

    def get_sign(self, text: str):

        return self.script.exports.getsign(True, text)

3.6 备注

通过测试验证,可以发现两个版本v5.0.80,v5.0.90的签名算法是一致的。因此可以直接利用v5.0.80做签名即可。

打完收工!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1626867.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Spring Boot的口腔管理平台设计与实现

基于Spring Boot的口腔管理平台设计与实现 开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/idea 系统部分展示 管理员登录界面图&#xff0c;管理员登录进入口腔管理平…

基于JAVA实现的推箱子小游戏

Java推箱子小游戏实现&#xff1a; 推箱子小游戏曾经在我们的童年给我们带来了很多乐趣。推箱子这款游戏现在基本上没人玩了&#xff0c;甚至在新一代人的印象中都已毫无记忆了。。。但是&#xff0c;这款游戏可以在一定程度上锻炼自己的编程能力。 窗口画面设计&#xff1a;i…

鸿蒙OpenHarmony【轻量系统 编译】 (基于Hi3861开发板)

编译 OpenHarmony支持hb和build.sh两种编译方式。此处介绍hb方式&#xff0c;build.sh脚本编译方式请参考[使用build.sh脚本编译源码]。 使用build.sh脚本编译源码 进入源码根目录&#xff0c;执行如下命令进行版本编译。 ./build.sh --product-name name --ccache 说明&…

今日arXiv最热大模型论文:大模型也来看球,还能判断是否犯规

在足球世界&#xff0c;裁判的哨声可谓“千金难买”&#xff0c;因为它能直接决定俱乐部的钱包是鼓是瘪。但球场变化莫测&#xff0c;非常考验裁判的水平。 2022年卡塔尔世界杯上&#xff0c;半自动越位识别技术&#xff08;SAOT&#xff09;闪亮登场&#xff0c;通过12台摄像…

Rabbitmq安装延迟插件rabbitmq_delayed_message_exchange失败

Docker里的Rabbitmq容器安装延迟插件rabbitmq_delayed_message_exchange失败 一启动插件Rabbitmq容器直接停止运行了 rabbitmq-plugins enable rabbitmq_delayed_message_exchange排除了版本问题和端口问题等&#xff0c;发现是虚拟机运行内存不够&#xff0c;增加虚拟机运行内…

游戏新手村18:游戏广告渠道与广告形式

上文我们说到&#xff0c;渠道为王&#xff0c;渠道可以为我们带来流量和用户&#xff0c;进而带来收入。我们可以通过哪些渠道导入用户呢&#xff1f;每个渠道有哪些优劣呢&#xff1f;在进行游戏营销推广的时候我们该如何选择呢&#xff1f; 根据付费性质&#xff0c;我们可…

Dbeaver客户端安装和使用

数据库管理软件&#xff08;DBMS&#xff09;&#xff1a; • 数据是数据库中存储的基本对象&#xff0c;种类包括文字、图形、图像、声音等等 • 数据库&#xff08;database&#xff09;是以某种有组织的方式存储的数据集合&#xff0c;保存有组织的数据的容器&#xff0c;通…

【C语言必刷题】7. 百钱百鸡

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 |《MySQL探索之旅》 |《Web世界探险家》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更…

大模型_ ChatGLM-Med推理及微调部署

文章目录 ChatGLM-Med是什么数据集构建推理部署python环境切换到安装好的conda环境下载github数据切换目录在infer.py修改模型路径启动推理解决infer.py报错修改后再次启动推理完成 微调部署安装evaluate包修改模型路径启动微调 ChatGLM-Med是什么 经过中文医学指令精调/指令微…

echarts地图叠加百度地图底板实现数据可视化

这里写自定义目录标题 echarts地图叠加百度地图实现数据可视化echarts地图叠加百度地图实现数据可视化 实现数据可视化时,个别情况下需要在地图上实现数据的可视化,echarts加载geojson数据可以实现以地图形式展示数据,例如分层设色或者鼠标hover展示指标值,但如果要将echa…

国产麒麟系统下打包electron+vue项目(AppImage、deb)

需要用到的一些依赖包、安装包以及更详细的打包方法word以及麒麟官网给出的文档都已放网盘&#xff0c;链接在文章最后&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&a…

【系统架构师】-选择题(四)

1、“41”视图 ①逻辑视图(Logical View)&#xff0c;设计的对象模型(使用面向对象的设计方法时)。 ②过程视图(Process View)&#xff0c;捕捉设计的并发和同步特征。系统集成师 ③物理视图(Physical View)&#xff0c;描述了软件到硬件的映射&#xff0c;反映了分布式特性。系…

SCI一区级 | Matlab实现BES-CNN-GRU-Mutilhead-Attention多变量时间序列预测

SCI一区级 | Matlab实现BES-CNN-GRU-Mutilhead-Attention秃鹰算法优化卷积门控循环单元融合多头注意力机制多变量时间序列预测 目录 SCI一区级 | Matlab实现BES-CNN-GRU-Mutilhead-Attention秃鹰算法优化卷积门控循环单元融合多头注意力机制多变量时间序列预测预测效果基本介绍…

命令行启动pytest自动化程序时,程序卡住不动了,不继续往下执行了

一、问题描述 在执行pytestallure自动化测试工具的时候&#xff0c;命令行启动程序时&#xff0c;程序卡住不继续往下执行&#xff0c;如下图所示。 代码主函数如下&#xff1a; 二、解决方法 测试客户项目时遇到2次此类问题&#xff0c;2次问题原因不一样。 原因一&#xf…

【JVM】从i++到JVM栈帧

【JVM】从i到JVM栈帧 本篇博客将用两个代码例子&#xff0c;简单认识一下JVM与栈帧结构以及其作用 从i与i说起 先不急着看i和i&#xff0c;我们来看看JVM虚拟机&#xff08;请看VCR.JPG&#xff09; 我们初学JAVA的时候一定都听到过JAVA“跨平台”的特性&#xff0c;也就是…

XiaodiSec day017 Learn Note 小迪安全学习笔记

XiaodiSec day017 Learn Note 小迪安全学习笔记 记录得比较凌乱&#xff0c;不尽详细 day 17 主要内容&#xff1a; php 框架 thinkPHPyiilaravel 使用 fofa 搜索 thinkphp 市面上 thinkphp5 版本较多 url 结构 域名/.php(文件名)/index(目录)/index(函数名)模块名-控…

律师口才训练技巧课程介绍?

律师口才训练技巧课程介绍 一、课程背景与目标 律师口才作为法律职业的核心能力之一&#xff0c;对于律师在**辩论、法律咨询、谈判协商等场合的表现具有至关重要的作用。然而&#xff0c;许多律师在口才方面存在不足&#xff0c;难以充分发挥自己的专业能力。因此&#xff0c;…

stm32开发之netxduo组件之mqtt客户端的使用记录

前言 1使用mqtt协议的简单示例记录 代码 MQTT服务端(C# 编写,使用MQTTnet提供的示例代码) 主程序 namespace ConsoleApp1;public class Program {public static async Task Main(string[] args){await Run_Server_With_Logging();}}public static async Task Run_Server_Wi…

HarmonyOS ArkUI实战开发—状态管理

一、状态管理 在声明式UI编程框架中&#xff0c;UI是程序状态的运行结果&#xff0c;用户构建了一个UI模型&#xff0c;其中应用的运行时的状态是参数。当参数改变时&#xff0c;UI作为返回结果&#xff0c;也将进行对应的改变。这些运行时的状态变化所带来的UI的重新渲染&…

电子负载仪的远端控制

前言 最近研究了电子负载仪的远端控制&#xff08;区别于前面板控制&#xff09;&#xff0c;主要是用于程序控制&#xff0c;避免繁琐复杂的人工控制&#xff0c;举了南京嘉拓和艾维泰科的例子。 有纰漏请指出&#xff0c;转载请说明。 学习交流请发邮件 1280253714qq.com …