SUSCTF2024-Redroid-出题笔记与解题思路

news2024/11/20 23:29:42

SUSCTF2024-Redroid-出题笔记与解题思路

    • Step1
    • Step2
    • Step3
    • Step4

描述:题目要求使用安卓13/14系统真机

Step1

Java层的逻辑比较简单,两个Activity
MainActivity读并验证password,正确即进入CheckActivity,同时会传递password
step1
password先后传入了util.init和util.calc,计算得到hash与已知的secret比较
Util实际上是HMAC-SHA1的标准哈希过程,只不过这里key和data同时为输入的password
step1
password长度为6,并且均为数字,于是可以爆破得到,为122608

import hashlib
import hmac
for i in range(1000000):
    s = str(i).encode()
    h = hmac.new(s, s, hashlib.sha1).hexdigest()
    if h == "59e7027803adaa7bcad8e40770c3c50f288ca5ac":
        print(i)
        break
# 122608

如果没有认出来HMAC-SHA1算法,也可以直接扣Java代码爆破,Util没有用到android类库

Step2

输入password进入CheckActivity
开头两个字符串,两个Native方法,加载libcheck.so
step2
onCreate通过intent读传入的password,然后和liblabel以及getAssets()一起传入init方法
只有init方法返回true,程序流才会继续向下去做flag的校验,因此要求password必须正确
init方法的第一个参数是个AssetManager,和资源相关,在assets目录下确实存在很多jpg,还不知道怎么处理
按钮onClick只是读flag并校验长度,然后传入check方法,返回true说明flag正确
step2
关键就是init和check两个native方法,因此开始分析libcheck.so
没有jni_onload,搜java只找到init方法,没有check方法
step2
这是因为check方法根本不在libcheck.so
libcheck.so实际上是一个壳so,init方法是在做解密以及模仿linker加载so
而通过libcheck.so加载的新so(librealcheck.so)才真正包含了check方法校验flag的逻辑
这也就是为什么Java层要判断init方法的返回值:因为加载librealcheck.so不成功的话就根本找不到check方法
且为什么要求使用安卓13/14系统真机:因为加载新so最后需要修复soinfo,soinfo结构成员在不同版本下偏移不同
于是这题要求对linker加载so的过程比较熟悉
参考:
[1]自定义Linker实现分析之路(一)
[2]基于linker实现so加壳技术基础上篇
[3]基于linker实现so加壳技术基础下篇
[4]自定义linker加固so
[5]获取soinfo+
配合源码食用更佳:
[6]AOSPXRef
回到题目,先看看源码正向的加密过程
编译出librealcheck.so后重命名为nevercheck114.jpg,放到assets目录下
为什么要改成jpg后缀:
一方面是隐藏在其他jpg里,顺便玩梗
另一方面是安卓认为jpg是已经压缩好的,它不会再去压缩,如果还是so后缀,它会压缩导致找不到资源文件
这里的key还是前面的password,作为加密密钥

// 加密:librealcheck.so => nevercheck114.jpg => nevercheck114.jpg.packed => nevercheck114.jpg
AAssetManager * aAssetManager = AAssetManager_fromJava(env, assetmanager);
AAsset * aAsset = AAssetManager_open(aAssetManager, "nevercheck114.jpg", AASSET_MODE_UNKNOWN);
off64_t start = 0, length = 0;
int fd = AAsset_openFileDescriptor64(aAsset, &start, &length);
lseek(fd, start, SEEK_CUR);
AAsset_close(aAsset);
if(fd!=-1){
    const char * key = env->GetStringUTFChars(libkey,0);
    shell ashell;
    if(ashell.pack(fd,length,key)){
        return JNI_TRUE;
    }
}

ashell.pack就是对so文件的变形以及加密,参考[4]
先定义一个CosELFHeader结构体,存储原始ELF头的偏移和大小,原始程序头表的偏移和大小
然后对原始ELF头和原始程序头表做RC4加密,顺便拷贝到加密so的末尾
对PT_LOAD的段做RC4加密,偏移不变,拷贝到加密so的对应位置
把自定义的CosELFHeader放到加密so开头,用来找原始ELF头和原始程序头表
最后填充随机字节到非PT_LOAD的其他部分,这对解密加载没有影响,只是不想让加密so看起来太奇怪(大片的0)

bool shell::pack(int libfd, size_t liblength, const char *libkey) {
    if(libkey!= nullptr){
        size_t libkeysize = strlen(libkey);
        size_t libsize = liblength;
        char * libdata = reinterpret_cast<char *>(malloc(libsize));
        if(libdata!= nullptr) {
            memset(libdata, 0, libsize);
            if (read(libfd, libdata, libsize) == libsize) {
                close(libfd);
                Elf64_Ehdr * oriELFHeader = reinterpret_cast<Elf64_Ehdr *>(libdata);
                CosELFHeader cosElfHeader;
                cosElfHeader.oriELFHeaderOff    = libsize + oriELFHeader->e_phnum * oriELFHeader->e_phentsize;
                cosElfHeader.oriELFHeaderSize   = oriELFHeader->e_ehsize;
                cosElfHeader.oriELFPHTOff       = libsize;
                cosElfHeader.oriELFPHTNum       = oriELFHeader->e_phnum;
                cosElfHeader.oriELFPHTSize      = oriELFHeader->e_phnum * oriELFHeader->e_phentsize;
                size_t libpacksize = libsize + cosElfHeader.oriELFPHTSize + cosElfHeader.oriELFHeaderSize;
                char * libpackdata = reinterpret_cast<char *>(calloc(libpacksize,1));
                if(libpackdata!= nullptr){
                    rc4(libpackdata+cosElfHeader.oriELFHeaderOff,libdata,cosElfHeader.oriELFHeaderSize,libkey,libkeysize);
                    rc4(libpackdata+cosElfHeader.oriELFPHTOff,libdata+oriELFHeader->e_phoff,cosElfHeader.oriELFPHTSize,libkey,libkeysize);
                    Elf64_Phdr * phdr = reinterpret_cast<Elf64_Phdr *>(libdata + oriELFHeader->e_phoff);
                    size_t phdrloadsize = 0;
                    for(size_t i = 0; i < oriELFHeader->e_phnum; i++){
                        if(phdr->p_type == PT_LOAD){
                            rc4(libpackdata+phdr->p_offset,libdata+phdr->p_offset,phdr->p_filesz,libkey,libkeysize);
                            phdrloadsize = phdrloadsize < phdr->p_offset + phdr->p_filesz ? phdr->p_offset + phdr->p_filesz : phdrloadsize;
                        }
                        phdr = reinterpret_cast<Elf64_Phdr *>((char*)phdr + sizeof(Elf64_Phdr));
                    }
                    memset(libpackdata,0, cosElfHeader.oriELFHeaderSize);
                    memcpy(libpackdata,&cosElfHeader,sizeof(Elf64_Ehdr));
                    srand(atoi(libkey));
                    for(char * ptr = libpackdata+phdrloadsize;ptr<libpackdata+cosElfHeader.oriELFPHTOff;ptr++){
                        *ptr = (char)rand()%0x100;
                    }
                    FILE * libpackfile = fopen("/data/user/0/com.susctf.redroid/cache/nevercheck114.jpg.packed","w");
                    if(libpackfile!= nullptr) {
                        fwrite(libpackdata, libpacksize, 1, libpackfile);
                        fclose(libpackfile);
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

然后看源码的解密过程,也就是init方法
这里的nevercheck114.jpg就是经过变形和加密的librealcheck.so
key仍然是password,label是固定的"check",也就是壳so的名称,后面找壳so的soinfo时会用到

AAssetManager * aAssetManager = AAssetManager_fromJava(env, assetmanager);
AAsset * aAsset = AAssetManager_open(aAssetManager, "nevercheck114.jpg", AASSET_MODE_UNKNOWN);
off64_t start = 0, length = 0;
int fd = AAsset_openFileDescriptor64(aAsset, &start, &length);
lseek(fd, start, SEEK_CUR);
AAsset_close(aAsset);
if(fd!=-1){
    const char * label = env->GetStringUTFChars(liblabel,0);
    const char * key = env->GetStringUTFChars(libkey,0);
    shell ashell;
    if(ashell.unpack(fd,length,label,key)){
        return JNI_TRUE;
    }
}

ashell.unpack是对加密so的解密和加载
由加密so开头数据定义并填充CosELFHeader
然后通过CosELFHeader找原始ELF头和原始程序头表并解密,这里解密后没有将它们恢复到原始位置
程序头表解密后即可对PT_LOAD的段原地解密

bool shell::unpack(int libfd, size_t liblength, const char *liblabel, const char *libkey) {
    if(libkey!= nullptr){
        size_t libkeysize = strlen(libkey);
        size_t libpacksize = liblength;
        char * libpackdata = reinterpret_cast<char *>(malloc(libpacksize));
        if(libpackdata!= nullptr) {
            memset(libpackdata, 0, libpacksize);
            if (read(libfd, libpackdata, libpacksize) == libpacksize) {
                close(libfd);
                CosELFHeader cosElfHeader = {};
                memcpy(&cosElfHeader,libpackdata,sizeof(cosElfHeader));
                rc4(libpackdata+cosElfHeader.oriELFHeaderOff,libpackdata+cosElfHeader.oriELFHeaderOff,cosElfHeader.oriELFHeaderSize,libkey,libkeysize);
                rc4(libpackdata+cosElfHeader.oriELFPHTOff,libpackdata+cosElfHeader.oriELFPHTOff,cosElfHeader.oriELFPHTSize,libkey,libkeysize);
                Elf64_Phdr * phdr = reinterpret_cast<Elf64_Phdr *>(libpackdata + cosElfHeader.oriELFPHTOff);
                for(int i = 0; i < cosElfHeader.oriELFPHTNum; i++){
                    if(phdr->p_type == PT_LOAD){
                        rc4(libpackdata+phdr->p_offset,libpackdata+phdr->p_offset,phdr->p_filesz,libkey,libkeysize);
                    }
                    phdr = reinterpret_cast<Elf64_Phdr *>((char*)phdr + sizeof(Elf64_Phdr));
                }
                ElfReader elfReader;
                elfReader.SetFile(libpacksize - cosElfHeader.oriELFPHTSize - cosElfHeader.oriELFHeaderSize,libpackdata);
                elfReader.ReadElfHeader(cosElfHeader.oriELFHeaderOff);
                elfReader.ReadProgramHeaders(cosElfHeader.oriELFPHTOff);
                if(elfReader.ReserveAddressSpace() && elfReader.LoadSegments()){
                    ElfW(Phdr) * oriPhdr = reinterpret_cast<ElfW(Phdr) *>(malloc(cosElfHeader.oriELFPHTSize));
                    memcpy(oriPhdr,libpackdata + cosElfHeader.oriELFPHTOff,cosElfHeader.oriELFPHTSize);
                    elfReader.SetPhdr(oriPhdr);
                    std::string libname = "";
                    libname.append("lib");
                    libname.append(liblabel);
                    libname.append(".so");
                    ElfLoader elfLoader(libname);
                    intptr_t shellSoInfo = elfLoader.GetShellSoInfo();
                    if (shellSoInfo != 0) {
                        ElfFixer elfFixer(elfReader, shellSoInfo);
                        if (elfFixer.fix_soinfo() && elfFixer.prelink_image() && elfFixer.link_image()) {
                            return true;
                        }
                    }
                }
            }
        }
    }
    return false;
}

随后的ElfReader、ElfLoader和ElfFixer就是在对解密so做加载、链接、重定位以及修复soinfo,参考[1][2][3]
需要特别说明的是,获取libcheck.so的soinfo的方法参考[5]
主动调用__dl__Z15solist_get_headv获取solist头,然后通过next成员遍历寻找目标so
区别在于安卓13/14系统的soinfo结构成员的偏移不太一样,一个是next的偏移,另一个是so名称字符串的偏移
GetLibBase就是去扫/proc/self/maps,第一次匹配到目标so,返回起始地址

void ElfLoader::SetShellSoInfo(std::string libname) {
    ELFIO::elfio linker;
    if (linker.load("/system/bin/linker64")){
        ELFIO::symbol_section_accessor symbols(linker, linker.sections[".symtab"]);
        for (unsigned int i = 0; i < symbols.get_symbols_num(); ++i) {
            std::string name;
            ELFIO::Elf64_Addr value;
            ELFIO::Elf_Xword size;
            unsigned char bind;
            unsigned char type;
            ELFIO::Elf_Half section_index;
            unsigned char other;
            symbols.get_symbol(i, name, value, size, bind, type,section_index, other);
            if (name == "__dl__Z15solist_get_headv") {
                intptr_t linkerbase = GetLibBase("linker64");
                using solist_get_head_t = void *(*)();
                static solist_get_head_t solist_get_head = reinterpret_cast<solist_get_head_t>(linkerbase + value);
                intptr_t si = reinterpret_cast<intptr_t>(solist_get_head());
                const char * libpath = nullptr;
                while (si) {
                    libpath = (const char *) *(intptr_t *)(si + 0xD8);
                    if(strstr(libpath,libname.c_str())){
                        shellSoInfo = si;
                        break;
                    }
                    si = *(intptr_t *) (si + 0x28);
                }
            }
        }
    }
}

Step3

现在开始逆向,libcheck.so和librealcheck.so在编译时都做了控制流平坦化和字符串加密的混淆
d810可以去掉大部分平坦化混淆,字符串不多,手动解也能看
去混淆后的init方法还算看得清楚,与源码差别不大
step3
进来unpack函数会发现它的控制流几乎没有被混淆,只有开头的一点字符串解密,然后就是读jpgdata
step3
从jpgdata开头读原始ELF头的偏移和大小以及原始程序头表的偏移和大小
step3
对原始ELF头的RC4解密
step3
对原始程序头表的RC4解密
step3
编译出来的libcheck.so是把RC4过程直接内联进了unpack函数,看起来很庞大,实际上看到256大概能猜到是RC4
然后是对p_type==1的也就是PT_LOAD的段做RC4解密
step3
elfreader
step3
elfloader
step3
elffixer
step3
解密so的加载、链接、重定位以及soinfo的修复的识别需要比较强的逆向功底和比较多的逆向经验
这里能够大概感受到是读jpgdata然后做RC4解密然后被加载即可
加载的细节可以不必深入,但要求能够手动还原librealcheck.so
(可以尝试调试提取解密数据,然后拼起来,但是笔者调试发现断不下来
(按理说jni_onload都能断下,init在onCreate调用却断不下来,8太懂

from struct import unpack
from Crypto.Cipher import ARC4
from elftools.elf.elffile import ELFFile

# 读jpgdata
with open("nevercheck114.jpg", "rb") as f:
    jpgdata = f.read()

# 读原始ELF头偏移和大小
oriELFHeaderOff = unpack("<Q", jpgdata[:8])[0]
oriELFHeaderSize = unpack("<H", jpgdata[8:10])[0]
# 读原始程序头表偏移和大小
oriELFPHTOff = unpack("<Q", jpgdata[16:24])[0]
oriELFPHTNum = unpack("<H", jpgdata[24:26])[0]
oriELFPHTSize = unpack("<H", jpgdata[26:28])[0]

# RC4密钥
key = b"122608"

# 解密原始ELF头
rc4 = ARC4.new(key)
oriELFHeaderData = jpgdata[oriELFHeaderOff:oriELFHeaderOff+oriELFHeaderSize]
oriELFHeaderData = rc4.decrypt(oriELFHeaderData)

# 解密原始程序头表
rc4 = ARC4.new(key)
oriELFPHTData = jpgdata[oriELFPHTOff:oriELFPHTOff+oriELFPHTSize]
oriELFPHTData = rc4.decrypt(oriELFPHTData)

# 把原始ELF头和原始程序头表先写到一个文件,方便后续通过ELFFile读程序头表条目
with open("tmpelf", "wb") as f:
    f.write(oriELFHeaderData)
    f.write(oriELFPHTData)

# 初始化ELFFile
f = open("tmpelf", "rb")
elffile = ELFFile(f)

# 原始ELF文件
orielf = open("orielf", "wb")
for seg in elffile.iter_segments():
    # 只处理PT_LOAD段
    if seg.header["p_type"] == "PT_LOAD":
        seg_p_offset = seg.header["p_offset"]
        seg_p_filesz = seg.header["p_filesz"]
        rc4 = ARC4.new(key)
        orielf.write(rc4.decrypt(
            jpgdata[seg_p_offset:seg_p_offset+seg_p_filesz]))

f.close()
orielf.close()

这里orielf的前0x40字节,也就是ELF头,是不正确的,因为第一个PT_LOAD段包含了ELF头的范围
手动修复为oriELFHeaderData,也就是从tmpelf复制前0x40字节到orielf
step3
orielf保存后可以用ida打开,此时可以看到check方法
step3

Step4

开始分析check方法,输入长度应为32
前16字节正常赋值,后16字节与前16字节依次异或再赋值
前后16字节经过wbaes函数变换后分别与enc1和enc2比较,都相等说明输入正确,即为flag
step4
wbaes函数虽然没被混淆,但是很长,这里实际上是一个标准白盒AES的循环展开过程
白盒AES攻击关键是在第九轮列混淆前破坏状态矩阵中的一个字节,使得最终的密文被破坏四字节
进而恢复最后一轮密钥的四字节,如果选择破坏的字节合适,可以完整恢复出最后一轮密钥,进而恢复初始密钥
破坏字节需要hook程序流,这里选择unidbg模拟执行,然后hook
unidbg模拟的好处是不用去找解密so的加载地址,解密so的加载地址是在加载过程中由壳so分配的,不易获取
apk后缀改成zip,重命名orielf为librealcheck.so,然后加到zip,zip再改回apk
这里给出关键的攻击代码

public static byte[] hexStringToBytes(String hexString) {
    if (hexString.isEmpty()) {
        return null;
    }
    hexString = hexString.toLowerCase();
    final byte[] byteArray = new byte[hexString.length() >> 1];
    int index = 0;
    for (int i = 0; i < hexString.length(); i++) {
        if (index > hexString.length() - 1) {
            return byteArray;
        }
        byte highDit = (byte) (Character.digit(hexString.charAt(index), 16)
                & 0xFF);
        byte lowDit = (byte) (Character.digit(hexString.charAt(index + 1),
                16) & 0xFF);
        byteArray[i] = (byte) (highDit << 4 | lowDit);
        index += 2;
    }
    return byteArray;
}

public static String bytesTohexString(byte[] bytes) {
    StringBuffer sb = new StringBuffer();
    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(bytes[i] & 0xFF);
        if (hex.length() < 2) {
            sb.append(0);
        }
        sb.append(hex);
    }
    return sb.toString();
}

public void callNative(){
    MemoryBlock inblock = androidEmulator.getMemory().malloc(16, true);
    UnidbgPointer inPtr = inblock.getPointer();
    byte[] stub = hexStringToBytes("30313233343536373839616263646566");
    assert stub != null;
    inPtr.write(0, stub, 0, stub.length);
    dalvikModule.getModule().callFunction(androidEmulator, 0x000000000007FC88, inPtr);
    String ret = bytesTohexString(inPtr.getByteArray(0, 0x10));
    System.out.println(ret);
    inblock.free();
}

/*
* 0x000000000008874C UC_ARM64_REG_X10
* 0x0000000000088450 UC_ARM64_REG_X8
* 0x00000000000876EC UC_ARM64_REG_X10
* 0x0000000000087B48 UC_ARM64_REG_X8
* */
public void attack(){
    androidEmulator.attach().addBreakPoint(dalvikModule.getModule().base + 0x0000000000087B48, new BreakPointCallback() {
        int count = 0;
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_X8,count);
            count+=1;
            return true;
        }
    });
}

public static void main(String[] args){
    Redroid redroid = new Redroid();
    redroid.callNative();
    redroid.attack();
    for(int i = 0;i<10;i++){
        redroid.callNative();
    }
}

0x000000000008874C、0x0000000000088450、0x00000000000876EC和0x0000000000087B48
是四个破坏一字节而最终破坏密文四字节的地址
这里因为循环被展开了,轮加密的结果存在中间变量,而不是直接写回,不太好找第九轮列混淆
有个技巧是,从最后写回密文用到的中间变量开始找
比如这里用v478,对它交叉引用,找它最后一次写操作
如果破坏这个写操作,导致最后的密文只破坏了一字节,说明破坏晚了(从最后找肯定是晚的
然后找它倒数第二次写操作,如果破坏这个写操作,导致密文破坏四字节,说明破坏成功,保留模拟的几条结果
step4
依次类推,找不同的中间变量,直到找到最终密文的十六字节都能被破坏的至少四处写操作(就是上面的几个地址
用phoenixAES恢复最后一轮密钥

import phoenixAES
path = "tracefile"
with open(path, "wb") as f:
    f.write("""
59c604bb9a72247f3ac8ee52a5fd3e4f
59c649bb9a7c247fb3c8ee52a5fd3e30
59c659bb9a8a247ff4c8ee52a5fd3ef4
596704bb1572247f3ac8ee8da5fd254f
596004bbd972247f3ac8ee12a5fdc64f
3ac604bb9a7224e23ac81d52a5013e4f
98c604bb9a7224403ac85a52a56d3e4f
59c604339a72b97f3a12ee527efd3e4f
59c604b39a72097f3ad0ee52e1fd3e4f
""".encode("utf8"))
phoenixAES.crack_file(path, verbose=1)
# Last round key #N found:
# 89274E962B51F09F449F5E59868C63BF

用Stark恢复初始密钥,为6A3C0545876924612949ED0B6CD34CDC

p1umh0@p1umh0:~/ctftools/Stark$ ./aes_keyschedule 89274E962B51F09F449F5E59868C63BF 10
K00: 6A3C0545876924612949ED0B6CD34CDC
K01: 0D1583158A7CA774A3354A7FCFE606A3
K02: 817A899F0B062EEBA833649467D56237
K03: 86D0131A8DD63DF125E5596542303B52
K04: 8A32133607E42EC7220177A260314CF0
K05: 5D1B9FE65AFFB12178FEC68318CF8A73
K06: F765104BAD9AA16AD56467E9CDABED9A
K07: D530A8F678AA099CADCE6E75606583EF
K08: 18DC772660767EBACDB810CFADDD9320
K09: C200C0B3A276BE096FCEAEC6C2133DE6
K10: 89274E962B51F09F449F5E59868C63BF

初始密钥实际上是一个md5
step4
最后解密aes即可

from Crypto.Cipher import AES
enc1 = [0x4d, 0xcd, 0xde, 0xb2, 0xc8, 0x35, 0x06, 0xdc,
        0x49, 0x2c, 0x7c, 0x35, 0x08, 0x9f, 0x46, 0x9b]
enc2 = [0x45, 0x9b, 0x79, 0x83, 0x6f, 0xc8, 0x62, 0xa1,
        0x87, 0x60, 0x3a, 0x9e, 0x56, 0x92, 0x61, 0x1c]
key = bytes.fromhex("6A3C0545876924612949ED0B6CD34CDC")
aes = AES.new(key, AES.MODE_ECB)
flag1 = list(aes.decrypt(bytes(enc1)))
aes = AES.new(key, AES.MODE_ECB)
flag2 = list(aes.decrypt(bytes(enc2)))
for i in range(len(flag2)):
    flag2[i] ^= flag1[i]
print("".join(chr(j) for j in flag1), end="")
print("".join(chr(j) for j in flag2))
# SUSCTF{u_4r3_m4573r_0f_4ndr01d!}

当然也可以用frida去hook,前提是将AndroidManifest.xml中的android:extractNativeLibs改为true并重打包
否则/proc/self/maps根本找不到libcheck.so模块,更别说找librealcheck.so的加载地址
AndroidManifest.xml编解码使用xml2axml
重打包后安装说没对齐

adb: failed to install redroid_Mod.apk: Failure [-124: Failed parse during installPackageLI: Targeting R+ (version 30 and above) requires the resources.arsc of installed APKs to be stored uncompressed and aligned on a 4-byte boundary]

参考zipalign对齐,再次安装说是没签名

adb: failed to install redroid_Mod_unsign.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Scanning Failed.: No signature found in package of version 2 or newer for package com.susctf.redroid]

使用MT管理器签名后安装成功
init是在onCreate被调用的,调用时机很早
一个可能hook到的时机是,在linker准备初始化libcheck.so,也就是准备调用call_constructors的时候去hook
用frida hook感觉很麻烦,感兴趣的读者可以一试

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

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

相关文章

铝壳电阻与普通电阻有什么区别?

铝壳电阻和普通电阻是电子元件中常见的两种电阻类型&#xff0c;它们在结构和性能上有一定的区别。铝壳电阻是具有金属外壳的电阻器&#xff0c;其外壳通常由铝合金制成&#xff0c;具有良好的散热性能。而普通电阻则没有金属外壳&#xff0c;只有一层绝缘材料包裹着电阻丝。 …

软件设计之JavaWeb(8)

软件设计之JavaWeb(8) 此篇应在MySQL之后进行学习: 路线图推荐&#xff1a; 【Java学习路线-极速版】【Java架构师技术图谱】 尚硅谷全新JavaWeb教程&#xff0c;企业主流javaweb技术栈 资料可以去尚硅谷官网免费领取 此章节最好学完JDBC观看 学习内容&#xff1a; promiseA…

力扣面试经典150题——合并两个有序数组

目录 题目链接&#xff1a; 题目描述 示例 提示&#xff1a; 解法一&#xff1a;合并数组排序 Java写法&#xff1a; 运行时间 C写法&#xff1a; 运行时间 时间复杂度和空间复杂度 解法二&#xff1a;双指针 Java写法&#xff1a; 运行时间 C写法&#xff1a; 运…

新闻文本分类识别系统Python+卷积神经网络算法+人工智能+深度学习+计算机毕设项目+TensorFlow+Django网页界面

一、介绍 文本分类识别系统。本系统使用Python作为主要开发语言&#xff0c;首先收集了10种中文文本数据集&#xff08;“体育类”, “财经类”, “房产类”, “家居类”, “教育类”, “科技类”, “时尚类”, “时政类”, “游戏类”, “娱乐类”&#xff09;&#xff0c;然…

024.PL-SQL进阶—游标

课 程 推 荐我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448;入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448;虚 拟 环 境 搭 建 &#xff1a;&#x1…

STM32 Modbus主从站实例程序-FreeRTOS

资料下载地址&#xff1a;STM32 Modbus主从站实例程序-FreeRTOS​​​​​​​ 基本设置 启用Freertos,添加任务 设置中断优先级 设置长生成MDK工程 工程里面添加Modbus库 修改main.c 修改freertos.c 编译下载到单片机,完美运行

黄奕前夫勇夺“全球金融圈奥斯卡”!与她离婚后,姜凯身价涨30倍

黄奕前夫勇夺“全球金融圈奥斯卡”&#xff01;与她离婚后&#xff0c;姜凯身价涨30倍 近日&#xff0c;小编被一则经济类新闻吸引。姜凯主理的凯资本Kai Capital荣获“2024年度全球最佳多策略对冲基金”。 这不是演员黄奕的第一任丈夫姜凯吗&#xff1f; 这个奖项可不一般。…

Ubuntu LLaMA-Factory实战

一、Ubuntu LLaMA-Factory实战安装&#xff1a; CUDA 安装 CUDA 是由 NVIDIA 创建的一个并行计算平台和编程模型&#xff0c;它让开发者可以使用 NVIDIA 的 GPU 进行高性能的并行计算。 首先&#xff0c;在 https://developer.nvidia.com/cuda-gpus 查看您的 GPU 是否支持CU…

html TAB切换按钮变色、自动生成table--使用函数优化结构

<!DOCTYPE html> <head> <meta charset"UTF-8"> <title>Dynamic Tabs with Table Data</title> <style> /* 简单的样式 */ .tab-content { display: none; border: 1px solid #ccc; padding: 1px; marg…

【专题】2024年中国白酒行业数字化转型研究报告合集PDF分享(附原数据表)

原文链接&#xff1a;https://tecdat.cn/?p37755 消费人群趋于年轻化&#xff0c;消费需求迈向健康化&#xff0c;消费场景与渠道走向多元化&#xff0c;这些因素共同驱动企业凭借数据能力来适应市场的变化。从消费市场来看&#xff0c;消费群体、需求、场景及渠道皆展现出与…

怎么制作线上报名表_解锁报名新体验

在这个数字化飞速发展的时代&#xff0c;每一次活动的举办都力求高效、便捷与个性化。线上报名表&#xff0c;作为连接主办方与参与者的桥梁&#xff0c;其重要性不言而喻。它不仅是信息收集的工具&#xff0c;更是品牌形象的展示窗口&#xff0c;能够直接影响参与者对活动的第…

用最新的C++技术,如何实现一个序列化工具库?

在现代C的发展中&#xff0c;新引入的语言特性为高效且易用的序列化和反序列化库的开发提供了强大的支持。我们今天一起来探索如何在现代C特性下写出更简洁、更易维护的序列化工具代码。 现有序列化库的挑战 传统的C序列化库&#xff0c;如Boost.Serialization和Cereal&#…

神经网络(四):UNet语义分割网络

文章目录 一、简介二、网络结构2.1编码器部分2.2解码器部分2.3完整代码 三、实战案例 一、简介 UNet网络是一种用于图像分割的卷积神经网络&#xff0c;其特点是采用了U型网络结构&#xff0c;因此称为UNet。该网络具有编码器和解码器结构&#xff0c;两种结构的功能如下&#…

Redis的数据类型常用命令

目录 前言 String字符串 常见命令 set get mget mset setnx incr incrby decr decyby append Hash哈希 常见命令 hset hget hexists hdel hkeys hvals hgetall hmget hlen hsetnx List 列表 常见命令 lpush lrange lpushx rpush rpushhx lpop…

postman下载安装和导入导出脚本一键执行

下载和安装 首先&#xff0c;下载并安装PostMan&#xff0c;请访问PostMan的官方下载网址&#xff1a;https://www.getpostman.com/downloads/ 下载所需的安装程序后&#xff0c;直接安装即可 第一次打开会要求登录账号密码&#xff0c;如果没有&#xff0c;直接关闭&#xf…

海报制作哪个软件好?建议试试这5个

2024年过得飞快&#xff0c;转眼间国庆佳节即将到来。 在这个举国欢庆的时刻&#xff0c;无论是商家还是个人&#xff0c;都希望通过海报来传递节日的喜悦和祝福。制作一张吸引人的海报&#xff0c;不仅能提升品牌形象&#xff0c;还能增强节日氛围。 那么&#xff0c;如何快…

【Python报错已解决】TypeError: can only concatenate str (not “int“) to str

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

【Java 问题】基础——Java 概述

Java 概述 1. 什么是 Java ?2. Java 语言有哪些特点3. JVM、JDK 和 JRE 有什么区别&#xff1f;4. 说说什么是跨平台性&#xff1f;原理是什么&#xff1f;5. 什么是字节码&#xff1f;采用字节码的好处是什么&#xff1f;6. 为什么说 Java 语言 "编译与解释并存"?…

汽车行业SAP全球模版导入方案【集团出海部署】

在汽车行业实施SAP系统是一个复杂且具挑战性的项目&#xff0c;涉及多个业务模块和跨部门协作。以下是一个汽车行业SAP实施的导入方案&#xff0c;包括关键步骤、模块选择、最佳实践和注意事项。 1. 项目启动及规划 项目启动 项目发起&#xff1a;确定项目范围、目标和业务需…

Spring源码-ConfigurationClassPostProcessor类解析spring相关注解

ConfigurationClassPostProcessor类的作用 此类是一个后置处理器的类&#xff0c;主要功能是参与BeanFactory的建造&#xff0c;主要功能如下 1、解析加了Configuration的配置类 2、解析ComponentScan扫描的包 3、解析ComponentScans扫描的包 4、解析Import注解 该类在springbo…