使用unicorn模拟执行去除混淆

0. 前言

在分析某app的so时遇到了间接跳转类型的混淆,不去掉的话无法使用ida f5来静态分析,f5之后就长下面这样:

Alt text

本文记录一下使用python+unicorn模拟执行来去掉混淆的过程。

1. 分析混淆的模式

混淆的汇编代码如下:
 

Alt text


可以看到,这个代码块进行了一通运算,然后通过 br x8,跳转到寄存器x8中保存的地址,仔细分析这个x8的来源,可以观察到如下的固定模式:

1

2

3

4

5

6

7

8

9

10

11

12

CMP             X19, #0

MOV             W9, #0x40 ; '@'         ; X9 = 0x40

MOV             W10, #0x38 ; '8'        ; X10 = 0x38

ADRP            X25, #off_212C70@PAGE

CSEL            X9, X10, X9, EQ         ; if X19 == 0 then X9 = X10

ADD             X25, X25, #off_212C70@PAGEOFF ; X25 = 0x212C70

LDR             X8, [X25,X9]            ; X8 = qword[X25 + X9]

MOV             W9, #0xFE53             ; X9 = 0xFE53

MOV             W10, #0x82B4            ; X10 = 0x82B4

CSEL            X9, X10, X9, EQ         ; if X19 == 0 then X9 = X10

SUB             X8, X8, X9              ; X8 = X8 - X9

BR              X8                      ; 跳转到X8

先看 LDR X8, [X25,X9],X25寄存器是一张偏移表的基址, 这条指令从偏移表+X9出取出8字节数据放到了X8中,而X9的值来源于csel指令是0x38或者是0x40,由cmp的结果决定,如果X19等于0,则X9此时等于0x38,负责等于0x40。

再看 SUB X8, X8, X9,从偏移表取出一个8字节数据到X8之后,用X8减X9,结果放到X8,X9的值也是来源于csel指令,0x82B4或者是0xFE53

最后,通过br X8跳转到目标地址

也就是说,根据X9值的不同,最终跳转的地址会有两个,把正常的分支指令混淆成了上面这种模式,手动还原混淆可以把br X8 patch成:

1

2

beq addr1

b addr2

2. 去混淆的方式

本来想的是直接用python来匹配这种模式,然后手动计算出两个分支的地址,最后patch,但是后面发现,一个被混淆的函数中只有第一个混淆块会给X25赋值偏移表的地址,其他的块就直接用X25的值了,不会再次赋值,比如下面这个:

Alt text

这样的话就不能把这些被混淆的块单独的拿出来看,因为缺少计算分支地址的必要条件,必须从函数的第一个被混淆的块出发,获取到偏移表的地址才行。如果还想通过手动的计算出目的地址,那就需要手动去确定函数的边界,这样就太麻烦了。

所以最后还是选择使通过模拟执行的方式,从函数头开始执行,跑通每一个块,在执行到混淆块的时候,计算出分支地址,最后进行patch

这里模拟执行的框架选择unicorn,之前学习过无名侠大佬用unicorn去ollvm混淆的文章,这里借鉴一下思路

3. 写脚本

3.1 加载so

由于代码中有需要访问偏移表,这些偏移表是在so的第二个segment,这个segment的内存便宜和文件便宜不一样,跟windows加载pe一样存在一个拉伸的效果,所以为了模拟执行代码时可以正常访问到偏移表的数据,我们手动将so拉伸成内存视图:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

def load_elf(filename):

    global img_size

    global out_data

    segs = []

    with open(filename, 'rb') as f:

        out_data = f.read()

        for seg in ELFFile(f).iter_segments('PT_LOAD'):

            print('file_off:%s, va: %s, size: %s' %(hex(seg['p_offset']), hex(seg['p_vaddr']), hex(seg['p_filesz'])))

            segs.append((seg['p_offset'],seg['p_vaddr'], seg['p_filesz'], seg.data()))

    img_size = segs[-1][1] + segs[-1][2]

    byte_arr = bytearray([0] * img_size)

    for seg in segs:

        vaddr = seg[1]

        size = seg[2]

        data = seg[3]

        byte_arr[vaddr: vaddr + size] = bytearray(data)

    return byte_arr

3.2 初始化unicorn

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

def init_unicorn(file_name):

    global bin_data

    global uc

    #装载一下so到内存

    bin_data = bytes(load_elf(file_name))

    uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)

    uc.mem_map(0x80000000, 8 * 0x1000 * 0x1000)

    uc.mem_map(0, 8 * 0x1000 * 0x1000)

    # 写入so数据

    uc.mem_write(0, bin_data)

    #设置sp寄存器

    uc.reg_write(UC_ARM64_REG_SP, 0x80000000 + 0x1000 * 0x1000 * 6)

    #设置指令执行hook,执行每条指令都会走hook_code

    uc.hook_add(UC_HOOK_CODE, hook_code)

    #设置非法内存访问hook

    uc.hook_add(UC_HOOK_MEM_UNMAPPED, hook_mem_access)

    barr = uc.mem_read(0x144320, 8)

    print(barr)

3.3 如何跑通一个函数的所有路径

这里借鉴无名侠大佬的思路,先把入口节点放到队列中,然后不停从队列中取节点,以这个节点为起点模拟执行,直到下一个br reg,或者是ret。

一个节点包括地址和上下文环境(寄存器),在模拟执行之前,需要把寄存器的值设置好,同时在找到分支之后,也需要保存现场的上下文环境。

在找到下一个br reg之后,计算出分支地址,将分支地址放到队列中,继续循环即可。

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

def deobf():

    # 初始化unicorn

    filename = 'libxxxx.so'

    patched_filename = 'out.so'

    start_addr = 0x0103168

    init_unicorn(filename)

    q = queue.Queue()

    q.put((start_addr, None)) # 入口函数是第一个节点,放到队列中去,队列中是(地址,上下文)

    traced = {} # 跑过的节点

    while not q.empty(): #一直循环,直到队列为空

        addr, context = q.get()

        traced[addr] = 1 # 跑过了

        s = run(addr, context) #开始模拟执行,找br reg

        if s is None:

            continue

        if len(s) == 2: #单分支

            if s[0] not in traced:

                q.put(s) #将分支节点放到队列中

        else: #双分支

            if s[0] not in traced:

                q.put((s[0], s[2]))#将分支节点放到队列中

            if s[1] not in traced:

                q.put((s[1], s[2]))#将分支节点放到队列中

def run(addr, context):

    global uc

    global is_success

    global block_flow

    #开始模拟执行,函数返回说明在hook_code中执行了emu_stop

    set_context(uc, context)#设置寄存器环境

    uc.emu_start(addr, 0x10000)

    if is_success == True:

        is_success = False

        return block_flow[addr] #返回分支信息和context

3.4 hook_code

hook_code是在unicorn中注册的指令执行回调,当执行uc.emu_start(addr, 0x10000)之后,就会开始模拟执行指令,同时调用hook_code,在hook_code中有很多重要的逻辑。

保存指令栈

进入hook_code之后,需要保存执行的汇编指令以及上下文环境,供后续判断是否到达混淆块使用:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

def hook_code(uc, address, size, user_data):

    global ins_stack

    global is_success

    if is_success == True:

        uc.emu_stop()

        return

    ins_help = InsHelp()

    code = uc.mem_read(address, size)

    ins = list(ins_help.disasm(code, address, False))[0]

    print("[+] tracing instruction\t0x%x:\t%s\t%s" % (ins.address, ins.mnemonic, ins.op_str))

    #记录指令和上下文环境

    ins_stack.append((address, get_context(uc)))

遇到ret或者bl .__stack_chk_fail需要停止

这两个是函数的边界,执行到这里就需要停下,我没有找到如何优雅的判断执行到了bl .__stack_chk_fail,所以就判断bl后面的地址了。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

#遇到ret直接挺停止

if ins.mnemonic.lower() == 'ret':

    #uc.reg_write(UC_ARM64_REG_PC, 0)

    print("[+] encountered ret, stop")

    ins_stack.clear()

    uc.emu_stop()

    return

#遇到bl .__stack_chk_fail停止

if ins.mnemonic.lower() == 'bl' and ins.operands[0].imm == 0x237C0:

    #uc.reg_write(UC_ARM64_REG_PC, 0)

    print("[+] encountered bl .__stack_chk_fail, stop")

    ins_stack.clear()

    uc.emu_stop()

    return

跳过函数调用、非栈或者so本身的内存访问、svc

需要跳过这些指令,并不影响寻路

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

    #跳过bl、非栈、so本身内存访问、svc

    if ins.mnemonic.lower().startswith('bl') or is_ref_ilegel_emm(uc, ins) or ins.mnemonic.lower().startswith('svc'):

        print("[+] pass instruction 0x%x\t%s\t%s" % (ins.address, ins.mnemonic, ins.op_str))

        uc.reg_write(UC_ARM64_REG_PC, address + size)

        return

def is_ref_ilegel_emm(mu, ins):

    if ins.op_str.find('[') != -1:

        if ins.op_str.find('[sp') == -1# 不是通过sp访问内存

            for op in ins.operands:

                if op.type == ARM64_OP_MEM:

                    addr = 0

                    if op.value.mem.base != 0:

                        addr += mu.reg_read(reg_ctou(ins.reg_name(op.value.mem.base)))

                    if op.value.mem.index != 0:

                        addr += mu.reg_read(reg_ctou(ins.reg_name(op.value.mem.index)))

                    if op.value.mem.disp != 0:

                        addr += op.value.mem.disp

                    if 0x0 <= addr <= img_size: # 访问so中的数据,允许

                        return False

                    elif 0x80000000 <= addr < 0x80000000 + 0x1000 * 0x1000 * 8: #访问栈中的数据,允许

                        return False

                    else:

                        return True

        else:# 是通过sp的内存访问,允许

            return False

    else:

        return False

特征是否到达混淆块、计算分支地址

先判断是否是br指令,如果是,调用get_double_branch尝试去进一步匹配特征

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

if ins.mnemonic == "br":

    #判断是否到达间接跳转

    is_success = True

    block_base = ins_stack[0][0]

    jmp_addr = ins_stack[-1][0]

    ret = get_double_branch(uc, ins_stack)

    if ret != None:

        print('find double branch: %x => %x, %x' % (block_base, ret[0], ret[1]))

        block_flow[ins_stack[0][0]] = ret

        patch_double_branch(uc, jmp_addr, ret)

    else:

        ret = get_single_branch(uc, ins_stack)

        if ret == None:

            print("[+] find dest failed 0x%x\t%s\t%s" % (ins.address, ins.mnemonic, ins.op_str))

            is_success = False

        else:

            print('find single branch: %x => %x' % (block_base, ret[0]))

            block_flow[block_base] = ret

            patch_single_branch(jmp_addr, ret[0])

    ins_stack.clear()

    uc.emu_stop()

    return

进入get_double_branch,遍历指令栈,判断是否存在特定的指令,如果有则获取指定寄存器的值,最后计算出两个分支地址,这几个指令存在先后顺序,所以需要几个标志变量来控制。

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

def get_double_branch(uc, ins_stack):

    #变量声明略

    ins_help = InsHelp()

    cond = ''

    for tup in ins_stack[::-1]:

        addr = tup[0]

        context = tup[1]

        ins = list(ins_help.disasm(bin_data[addr: addr+5], addr, False))[0]

        # BR              X8

        if ins.mnemonic.lower() == 'br' and flag_br == False:

            flag_br = True

            br_reg = ins.operands[0].reg

        # SUB             X8, X8, X9

        if  flag_br == True and (ins.mnemonic.lower() == 'add' or ins.mnemonic.lower() == 'sub') \

                and ins.operands[0].reg == br_reg and flag_sub_add == False:

            if ins.operands[1].type == 1 and ins.operands[2].type == 1:

                op_reg1 = ins.operands[1].reg

                op_reg2 = ins.operands[2].reg

                flag_sub_add = True

        # CSEL            X9, X10, X9, EQ

        if flag_sub_add == True and ins.mnemonic.lower() == 'csel' and ins.operands[0].reg == op_reg2 \

                and flag_csel1 == False:

            cond = ins.op_str.split(', ')[-1]

            regname1 = ins.reg_name(ins.operands[1].reg)

            regname2 = ins.reg_name(ins.operands[2].reg)

            # index1 = reg_ctou(regname1) - arm64_const.UC_ARM64_REG_X0

            # index2 = reg_ctou(regname2) - arm64_const.UC_ARM64_REG_X0

            reg2_value1 = 0 if regname1.lower() == 'xzr' else context[reg_ctou(regname1) - arm64_const.UC_ARM64_REG_X0]

            reg2_value2 = 0 if regname2.lower() == 'xzr' else context[reg_ctou(regname2) - arm64_const.UC_ARM64_REG_X0]

            flag_csel1 = True

        #  LDR             X8, [X25,X9]

        if flag_sub_add == True and ins.mnemonic.lower() == 'ldr' and ins.operands[0].reg == op_reg1 \

                and flag_ldr == False:

            pattern = r'\[(.*?)\]'

            matches = re.findall(pattern, ins.op_str)

            assert len(matches) == 1, 'not find []: %x\t%s\t%s' % (addr, ins.mnemonic, ins.op_str)

            op2_str = matches[0]

            regs = op2_str.split(', ')

            assert len(regs) == 2, 'ins invalid!: %x\t%s\t%s' % (addr, ins.mnemonic, ins.op_str)

            table_base = context[reg_ctou(regs[0]) - arm64_const.UC_ARM64_REG_X0]

            op_reg3 = reg_ctou(regs[1])

            flag_ldr = True

        #  CSEL            X9, X10, X9, EQ

        if flag_ldr == True and ins.mnemonic.lower() == 'csel' and reg_ctou(ins.reg_name(ins.operands[0].reg)) == op_reg3 \

                and flag_csel2 == False:

            regname1 = ins.reg_name(ins.operands[1].reg)

            regname2 = ins.reg_name(ins.operands[2].reg)

            # index1 = reg_ctou(regname1) - arm64_const.UC_ARM64_REG_X0

            # index2 = reg_ctou(regname2) - arm64_const.UC_ARM64_REG_X0

            reg3_value1 = 0 if regname1.lower() == 'xzr' else context[reg_ctou(regname1) - arm64_const.UC_ARM64_REG_X0]

            reg3_value2 = 0 if regname2.lower() == 'xzr' else context[reg_ctou(regname2) - arm64_const.UC_ARM64_REG_X0]

            flag_csel2 = True

    if flag_csel1 == True and flag_csel2 == True:

        # 满足条件时走的分支

        barr1 = uc.mem_read(table_base + reg3_value1, 8) #直接从文件中读数据,注意内存偏移和文件偏移的转换

        base1 = struct.unpack('q',barr1)

        offset1 = base1[0] - reg2_value1

        # 不满足条件时走的分支

        barr2 = uc.mem_read(table_base + reg3_value2, 8)

        base2 = struct.unpack('q',barr2)

        offset2 = base2[0] - reg2_value2

        return (offset1, offset2, get_context(uc), cond)

    else:

        return None

除了双分支,还有单分支的情况:

Alt text

这种只有一个固定的分支,所以直接patch成 b 0xxxxxxxxx即可
所以如果上述特征匹配不成功,则认为是单分支

3.5 patch

当遇到混淆块,计算出分支地址之后,就要进行patch了,双分支的patch需要两条指令的空间,但是有时候混淆块的倒数第二个指令是原来的指令,不能被覆盖,那么能用的就只有一条指令的空间。

那就只能找代码段中别的的空闲空间,调b跳转到空闲空间,然后在跳转到两个分支,找一个跳板。

当我在so中搜索nop时,居然发现了一段很长的nop,那用这里不就行了吗,反正去混淆也只是为了静态分析,不需要塞回去让so正常跑。

patch代码:

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

def patch_double_branch(uc, addr, branch):

    global out_data

    nop_addr = find2nop(uc)

    assert nop_addr is not None, 'no find 2 nop'

    offset1 = branch[0]

    offset2 = branch[1]

    cond = branch[3]

    ks = keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)

    # 1. 把bx reg修改成跳转到nop_addr

    jmp1_asm = 'b ' + hex(nop_addr)

    jmp1_bin = ks.asm(jmp1_asm, addr)[0]

    # 2. bcond addr1

    jmp2_asm = 'b' + cond + ' ' + hex(offset1)

    jmp2_bin = ks.asm(jmp2_asm, nop_addr)[0]

    #3. b addr2

    jmp3_asm = 'b ' + hex(offset2)

    jmp3_bin = ks.asm(jmp3_asm, nop_addr + 4)[0]

    #print(jmp3_bin)

    #patching

    print('patch code: %x\t%s => %s' % (addr, list(out_data[addr: addr + 4]), jmp1_bin))

    out_data = patch_bytes(out_data, bytearray(jmp1_bin), addr, 4)

    print('patch code: %x\t%s => %s' % (nop_addr, list(out_data[nop_addr: nop_addr + 4]), jmp2_bin))

    out_data = patch_bytes(out_data, bytearray(jmp2_bin), nop_addr, 4)

    print('patch code: %x\t%s => %s' % (nop_addr + 4, list(out_data[nop_addr + 4: nop_addr + 8]), jmp3_bin))

    out_data = patch_bytes(out_data, bytearray(jmp3_bin) , nop_addr + 4, 4)

最后将so数据写回文件

1

2

3

#保存patch后的so

with open(patched_filename, 'wb') as f:

    f.write(out_data)

4. 效果

还存在一些虚假控制流,但是已经不影响分析了

5. 问题

  1. 如果没有找到那一长串nop,就需要给代码段扩展一下放跳板,不知道咋搞
  2. 混淆块的特征可能还需要进一步测试修改,只测了几个函数

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《计算机网络简易速速上手小册》第7章:云计算与网络服务(2024 最新版)

文章目录 7.1 云服务模型&#xff08;IaaS, PaaS, SaaS&#xff09;- 你的技术魔法盒7.1.1 基础知识7.1.2 重点案例&#xff1a;构建和部署 Python Web 应用实现步骤具体操作步骤1&#xff1a;创建 Flask Web 应用步骤2&#xff1a;准备应用部署 7.1.3 拓展案例1&#xff1a;使…

机器学习5-线性回归之损失函数

在线性回归中&#xff0c;我们通常使用最小二乘法&#xff08;Ordinary Least Squares, OLS&#xff09;来求解损失函数。线性回归的目标是找到一条直线&#xff0c;使得预测值与实际值的平方差最小化。 假设有数据集 其中 是输入特征&#xff0c; 是对应的输出。 线性回归的…

查看docker服务的IP地址

要查看Docker容器服务的IP地址&#xff0c;可以使用以下命令&#xff1a; 如果你知道容器名称或容器ID&#xff0c;直接通过容器ID或容器名称来获取IP地址&#xff1a; # 使用容器ID获取IP地址 docker inspect -f {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}} …

cesium-加载谷歌影像

cesium在开发的时候有可能会加载不同的影像&#xff0c;今天就先看一下加载谷歌的吧。 使用谷歌有个好处就是基本不会出现此区域无卫星图的情况 闲言话语不多说&#xff0c;看代码 <template><div id"cesiumContainer" style"height: 100vh;"&g…

【SpringBoot】application配置文件(4)

freemarker:cache: false 这是关于 freemarker 模板引擎的一个配置&#xff0c;用于控制模板的缓存行为 当cache 设置为 false 时&#xff0c;意味着每次请求时都会重新加载和编译模板&#xff0c;而不是从缓存中获取 编译模板。 将 cache 设置为 false 是为了在开发过程中获…

python求解中位数

首先将数组nums进行排序&#xff0c;然后找到中间位置的数值 如果数组长度n为奇数&#xff0c;则(n1)/2处对应值为中位数&#xff0c;如果数组下标从0开始&#xff0c;还需要减去1 如果数组长度n为偶数&#xff0c;则n/2,n/21两个位置数的平均值为中位数 假设中位数为x&#x…

机器学习复习(2)——线性回归SGD优化算法

目录 线性回归代码 线性回归理论 SGD算法 手撕线性回归算法 模型初始化 定义模型主体部分 定义线性回归模型训练过程 数据demo准备 模型训练与权重参数 定义线性回归预测函数 定义R2系数计算 可视化展示 预测结果 训练过程 sklearn进行机器学习 线性回归代码…

CSC联合培养博士申请亲历|联系外导的详细过程

在CSC申报的各环节中&#xff0c;联系外导获得邀请函是关键步骤。这位联培博士同学的这篇文章&#xff0c;非常详细且真实地记录了申请过程、心理感受&#xff0c;并提出有益的建议&#xff0c;小编特推荐给大家参考。 2024年国家留学基金委公派留学项目即将开始&#xff0c;其…

网络原理TCP/IP(2)

文章目录 TCP协议确认应答超时重传连接管理断开连接 TCP协议 TCP全称为"传输控制协议(Transmission Control Protocol").⼈如其名,要对数据的传输进⾏⼀个详细 的控制; TCP协议段格式 • 源/目的端口号:表⽰数据是从哪个进程来,到哪个进程去; • 32位序号/32位确认…

会声会影下载 Corel VideoStudio 2023 v26.1.0.268中文激活版

会声会影Corel VideoStudio 2023破解版是领先的视频编辑和转换软件&#xff01;提供直观友好的功能&#xff0c;让用户能够更快速便捷地制作独特的视频&#xff0c;高质量的效果&#xff0c;各种滤镜、贴纸、过渡、模板等都将让您事半功倍&#xff01;软件允许您导入自己的剪辑…

社区店加盟:如何选择适合的品牌和项目?

在当下创业热潮中&#xff0c;社区店加盟成为了许多创业者的首选。特别是鲜奶吧这种深受各年龄段人群喜爱的项目&#xff0c;更是备受关注。然而&#xff0c;面对众多品牌和项目&#xff0c;如何选择适合自己的社区店加盟品牌和项目呢&#xff1f; 作为一位资深的鲜奶吧创业者…

关于node.js奇数版本不稳定 将11.x.x升级至16.x.x不成功的一系列问题(一)

据说vue2用16稳定一些 vue3用18好一点&#xff08;但之前我vue3用的16.18.1也可以&#xff09; 为维护之前的老项目 先搞定node版本切换 下载nvm node版本管理工具 https://github.com/coreybutler/nvm-windows/releases 用这个nvm-setup.zip安装包 安之前最好先将之前的nod…

西瓜书学习笔记——k近邻学习(公式推导+举例应用)

文章目录 算法介绍实验分析 算法介绍 K最近邻&#xff08;K-Nearest Neighbors&#xff0c;KNN&#xff09;是一种常用的监督学习算法&#xff0c;用于分类和回归任务。该算法基于一个简单的思想&#xff1a;如果一个样本在特征空间中的 k k k个最近邻居中的大多数属于某个类别…

git命令上传本地项目到远程仓库的悲惨遭遇

git命令上传本地项目到远程仓库的悲惨遭遇。我想把前端后端合并到一个仓库下2个分支&#xff0c;结果呢&#xff0c;不仅合并没有成功&#xff0c;还把代码丢失了。 如图&#xff0c;原始我写好了完整的后端代码&#xff0c;都丢失了。 远程仓库里也都没有了。奇怪了。 难道远…

二、Java学习 数据类型与变量

目录 一、字面常量 二、数据类型 三、变量 语法格式 四、类型转换 隐式类型转换 强制类型转换 字符串类型 五、类型提升 1.int与long 2.byte与byte 小结 一、字面常量 常量即运行期间&#xff0c;固定不变的量。 字面常量的分类&#xff1a; 1.字符串常量&#xff…

TQ15EG开发板教程:开发板资源介绍

时钟资源 采用时钟芯片CDCM6208提供系统时钟 PL端时钟 PS 收发器时钟 PL收发器时钟 电源 BANK500 BANK501 BANK502 BANK503(专用) 1.8V 1.8V 1.8V 1.8V PS端外设 QSPI 采用2片MT25QU256 拼接成8bit的QSPI存储系统。采用1.8V供电 SD卡 SATA接口 PS端以太网接口 D…

Java宝典-数据类型

目录 1.变量与常量2.Java中的数据类型3.整型3.1 字节型byte3.2 短整型short3.3 整型int3.4 长整型long 4.浮点型4.1 单精度浮点型float4.2 双精度浮点型double 5.字符型6.布尔型7.类型转换7.1 隐式类型转换7.2 显示类型转换(强制类型转换) 8.类型提升 大家好,我是你们的Vampire…

了解UDP发送过快导致的问题和对应解决方案

在当今这个以数据为核心的时代&#xff0c;企业对于数据传输的速度和稳定性有着日益增长的需求。UDP凭借其低延迟和高效率的特性&#xff0c;在实时通信和大规模数据传输领域扮演着关键角色。然而&#xff0c;UDP的无连接特性和缺乏可靠性也给数据传输带来了挑战&#xff0c;尤…

【python错误】Pytorch1.9 ImportError: cannot import name ‘zero_gradients‘

错误&#xff1a;Pytorch1.9 ImportError: cannot import name ‘zero_gradients’ 错误提示&#xff1a; ImportError: cannot import name ‘zero_gradients’ from ‘torch.autograd.gradcheck’ (/root/miniconda3/envs/d2l/lib/python3.9/site-packages/torch/autograd/g…

3种JWT验证和续签的策略

3 种JWT验证和续签的策略 好文推荐&#xff1a;一文教你搞定所有前端鉴权与后端鉴权方案&#xff0c;让你不再迷惘 - 掘金 (juejin.cn) 3 种jwt 验证的策略 通过解析去验证&#xff1a;每次访问api时parse jwt 判断是否vaild jwt有效 正常调用api jwt无效 返回401 缺点&a…
最新文章