Playing with ptrace, Part II

手把手教你玩转Shiro(1) 我们知道,shiro就是一款优秀的权限管理框架,那么到底什么是权限?什么是对权限管理?对权限管理到底又有哪些部分呢?Shiro这个框架到底能够提供我们怎么的权限管理呢?Shiro是不是能对我们实际开发提高安全和效率呢?我们带着这些疑问来慢慢学习Shiro,我觉得这样学习的效率会更加高。 一:权限的前导知识 描述:下面的内容主要是为了后面学习shiro了解一些基本知识,并且这些内容... 阅读详情
  In Part II of his series on ptrace, Pradeep tackles the more advanced topics of setting breakpoints and injecting code into running processes.

In Part I of this article [LJ, November 2002], we saw how ptrace can be used to trace system calls and change system call arguments. In this article, we investigate advanced techniques like setting breakpoints and injecting code into running programs. Debuggers use these methods to set up breakpoints and execute debugging handlers. As with Part I, all code in this article is i386 architecture-specific.

Attaching to a Running Process

In Part I, we ran the process to be traced as a child after calling ptrace(PTRACE_TRACEME, ..). If you simply wanted to see how the process is making system calls and trace the program, this would be sufficient. If you want to trace or debug a process already running, then ptrace(PTRACE_ATTACH, ..) should be used.

When a ptrace(PTRACE_ATTACH, ..) is called with the pid to be traced, it is roughly equivalent to the process calling ptrace(PTRACE_TRACEME, ..) and becoming a child of the tracing process. The traced process is sent a SIGSTOP, so we can examine and modify the process as usual. After we are done with modifications or tracing, we can let the traced process continue on its own by calling ptrace(PTRACE_DETACH, ..).

The following is the code for a small example tracing program:

int main()
{ int i;
for(i = 0;i < 10; ++i) {
printf("My counter: %d/n", i);
sleep(2);
}
return 0;
}

Save the program as dummy2.c. Compile and run it:

gcc -o dummy2 dummy2.c
./dummy2 &
Now, we can attach to dummy2 by using the code below:
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h> /* For user_regs_struct
etc. */
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct regs;
long ins;
if(argc != 2) {
printf("Usage: %s <pid to be traced>/n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, &regs);
ins = ptrace(PTRACE_PEEKTEXT, traced_process,
regs.eip, NULL);
printf("EIP: %lx Instruction executed: %lx/n",
regs.eip, ins);
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
The above program simply attaches to a process, waits for it to stop, examines its eip (instruction pointer) and detaches.

To inject code use ptrace(PTRACE_POKETEXT, ..) and ptrace(PTRACE_POKEDATA, ..) after the traced process has stopped.

Setting Breakpoints

How do debuggers set breakpoints? Generally, they replace the instruction to be executed with a trap instruction, so that when the traced program stops, the tracing program, the debugger, can examine it. It will replace the original instruction once the tracing program continues the traced process. Here's an example:

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <linux/user.h>
const int long_size = sizeof(long);
void getdata(pid_t child, long addr,
char *str, int len)
{ char *laddr;
int i, j;
union u {
long val;
char chars[long_size];
}data;
i = 0;
j = len / long_size;
laddr = str;
while(i < j) {
data.val = ptrace(PTRACE_PEEKDATA, child,
addr + i * 4, NULL);
memcpy(laddr, data.chars, long_size);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
data.val = ptrace(PTRACE_PEEKDATA, child,
addr + i * 4, NULL);
memcpy(laddr, data.chars, j);
}
str[len] = '/0';
}
void putdata(pid_t child, long addr,
char *str, int len)
{ char *laddr;
int i, j;
union u {
long val;
char chars[long_size];
}data;
i = 0;
j = len / long_size;
laddr = str;
while(i < j) {
memcpy(data.chars, laddr, long_size);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
}
}
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct regs, newregs;
long ins;
/* int 0x80, int3 */
char code[] = {0xcd,0x80,0xcc,0};
char backup[4];
if(argc != 2) {
printf("Usage: %s <pid to be traced>/n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, &regs);
/* Copy instructions into a backup variable */
getdata(traced_process, regs.eip, backup, 3);
/* Put the breakpoint */
putdata(traced_process, regs.eip, code, 3);
/* Let the process continue and execute
the int 3 instruction */
ptrace(PTRACE_CONT, traced_process, NULL, NULL);
wait(NULL);
printf("The process stopped, putting back "
"the original instructions/n");
printf("Press <enter> to continue/n");
getchar();
putdata(traced_process, regs.eip, backup, 3);
/* Setting the eip back to the original
instruction to let the process continue */
ptrace(PTRACE_SETREGS, traced_process,
NULL, &regs);
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}

Here we replace the three bytes with the code for a trap instruction, and when the process stops, we replace the original instructions and reset the eip to original location. Figures 1-4 clarify how the instruction stream looks when above program is executed.

Figure 1. After the Process Is Stopped

Figure 2. After the Trap Instruction Bytes Are Set

Figure 3. Trap Is Hit and Control Is Given to the Tracing Program

Figure 4. After the Original Instructions Are Replaced and eip Is Reset to the Original Location

Now that we have a clear idea of how breakpoints are set, let's inject some code bytes into a running program. These code bytes will print “hello world”.

The following program is a simple “hello world” program with modifications to fit our needs. Compile the following program with:

gcc -o hello hello.c
void main()
{
__asm__("
jmp forward
backward:
popl %esi # Get the address of
# hello world string
movl $4, %eax # Do write system call
movl $2, %ebx
movl %esi, %ecx
movl $12, %edx
int $0x80
int3 # Breakpoint. Here the
# program will stop and
# give control back to
# the parent
forward:
call backward
.string /"Hello World//n/""
);
}

The jumping backward and forward here is required to find the address of the “hello world” string.

We can get the machine code for the above assembly from GDB. Fire up GDB and disassemble the program:

(gdb) disassemble main
Dump of assembler code for function main:
0x80483e0 <main>: push %ebp
0x80483e1 <main+1>: mov %esp,%ebp
0x80483e3 <main+3>: jmp 0x80483fa <forward>
End of assembler dump.
(gdb) disassemble forward
Dump of assembler code for function forward:
0x80483fa <forward>: call 0x80483e5 <backward>
0x80483ff <forward+5>: dec %eax
0x8048400 <forward+6>: gs
0x8048401 <forward+7>: insb (%dx),%es:(%edi)
0x8048402 <forward+8>: insb (%dx),%es:(%edi)
0x8048403 <forward+9>: outsl %ds:(%esi),(%dx)
0x8048404 <forward+10>: and %dl,0x6f(%edi)
0x8048407 <forward+13>: jb 0x8048475
0x8048409 <forward+15>: or %fs:(%eax),%al
0x804840c <forward+18>: mov %ebp,%esp
0x804840e <forward+20>: pop %ebp
0x804840f <forward+21>: ret
End of assembler dump.
(gdb) disassemble backward
Dump of assembler code for function backward:
0x80483e5 <backward>: pop %esi
0x80483e6 <backward+1>: mov $0x4,%eax
0x80483eb <backward+6>: mov $0x2,%ebx
0x80483f0 <backward+11>: mov %esi,%ecx
0x80483f2 <backward+13>: mov $0xc,%edx
0x80483f7 <backward+18>: int $0x80
0x80483f9 <backward+20>: int3
End of assembler dump.

We need to take the machine code bytes from main+3 to backward+20, which is a total of 41 bytes. The machine code can be seen with the x command in GDB:

(gdb) x/40bx main+3
<main+3>: eb 15 5e b8 04 00 00 00
<backward+6>: bb 02 00 00 00 89 f1 ba
<backward+14>: 0c 00 00 00 cd 80 cc
<forward+1>: e6 ff ff ff 48 65 6c 6c
<forward+9>: 6f 20 57 6f 72 6c 64 0a
Now we have the instruction bytes to be executed. Why wait? We can inject them using the same method as in the previous example. The following is the source code; only the main function is given here:
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct regs, newregs;
long ins;
int len = 41;
char insertcode[] =
"/xeb/x15/x5e/xb8/x04/x00"
"/x00/x00/xbb/x02/x00/x00/x00/x89/xf1/xba"
"/x0c/x00/x00/x00/xcd/x80/xcc/xe8/xe6/xff"
"/xff/xff/x48/x65/x6c/x6c/x6f/x20/x57/x6f"
"/x72/x6c/x64/x0a/x00";
char backup[len];
if(argc != 2) {
printf("Usage: %s <pid to be traced>/n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, &regs);
getdata(traced_process, regs.eip, backup, len);
putdata(traced_process, regs.eip,
insertcode, len);
ptrace(PTRACE_SETREGS, traced_process,
NULL, &regs);
ptrace(PTRACE_CONT, traced_process,
NULL, NULL);
wait(NULL);
printf("The process stopped, Putting back "
"the original instructions/n");
putdata(traced_process, regs.eip, backup, len);
ptrace(PTRACE_SETREGS, traced_process,
NULL, &regs);
printf("Letting it continue with "
"original flow/n");
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
Injecting the Code into Free Space

In the previous example we injected the code directly into the executing instruction stream. However, debuggers can get confused with this kind of behaviour, so let's find the free space in the process and inject the code there. We can find free space by examining the /proc/pid/maps file of the traced process. The following function will find the starting address of this map:

long freespaceaddr(pid_t pid)
{
FILE *fp;
char filename[30];
char line[85];
long addr;
char str[20];
sprintf(filename, "/proc/%d/maps", pid);
fp = fopen(filename, "r");
if(fp == NULL)
exit(1);
while(fgets(line, 85, fp) != NULL) {
sscanf(line, "%lx-%*lx %*s %*s %s", &addr,
str, str, str, str);
if(strcmp(str, "00:00") == 0)
break;
}
fclose(fp);
return addr;
}

Each line in /proc/pid/maps represents a mapped region of the process. An entry in /proc/pid/maps looks like this:

map start-mapend    protection  offset     device
inode process file
08048000-0804d000 r-xp 00000000 03:08
66111 /opt/kde2/bin/kdeinit
The following program injects code into free space. It's similar to the previous injection program except the free space address is used for keeping our new code. Here is the source code for the main function:
int main(int argc, char *argv[])
{ pid_t traced_process;
struct user_regs_struct oldregs, regs;
long ins;
int len = 41;
char insertcode[] =
"/xeb/x15/x5e/xb8/x04/x00"
"/x00/x00/xbb/x02/x00/x00/x00/x89/xf1/xba"
"/x0c/x00/x00/x00/xcd/x80/xcc/xe8/xe6/xff"
"/xff/xff/x48/x65/x6c/x6c/x6f/x20/x57/x6f"
"/x72/x6c/x64/x0a/x00";
char backup[len];
long addr;
if(argc != 2) {
printf("Usage: %s <pid to be traced>/n",
argv[0], argv[1]);
exit(1);
}
traced_process = atoi(argv[1]);
ptrace(PTRACE_ATTACH, traced_process,
NULL, NULL);
wait(NULL);
ptrace(PTRACE_GETREGS, traced_process,
NULL, &regs);
addr = freespaceaddr(traced_process);
getdata(traced_process, addr, backup, len);
putdata(traced_process, addr, insertcode, len);
memcpy(&oldregs, &regs, sizeof(regs));
regs.eip = addr;
ptrace(PTRACE_SETREGS, traced_process,
NULL, &regs);
ptrace(PTRACE_CONT, traced_process,
NULL, NULL);
wait(NULL);
printf("The process stopped, Putting back "
"the original instructions/n");
putdata(traced_process, addr, backup, len);
ptrace(PTRACE_SETREGS, traced_process,
NULL, &oldregs);
printf("Letting it continue with "
"original flow/n");
ptrace(PTRACE_DETACH, traced_process,
NULL, NULL);
return 0;
}
Behind the Scenes

So what happens within the kernel now? How is ptrace implemented? This section could be an article on its own; however, here's a brief description of what happens.

When a process calls ptrace with PTRACE_TRACEME, the kernel sets up the process flags to reflect that it is being traced:

Source: arch/i386/kernel/ptrace.c
if (request == PTRACE_TRACEME) {
/* are we already being traced? */
if (current->ptrace & PT_PTRACED)
goto out;
/* set the ptrace bit in the process flags. */
current->ptrace |= PT_PTRACED;
ret = 0;
goto out;
}

When a system call entry is done, the kernel checks this flag and calls the trace system call if the process is being traced. The gory assembly details can be found in arch/i386/kernel/entry.S.

Now, we are in the sys_trace() function as defined in arch/i386/kernel/ptrace.c. It stops the child and sends a signal to the parent notifying that the child is stopped. This wakes up the waiting parent, and it does the ptrace magic. Once the parent is done, and it calls ptrace(PTRACE_CONT, ..) or ptrace(PTRACE_SYSCALL, ..), it wakes up the child by calling the scheduler function wake_up_process(). Some other architectures can implement this by sending a SIGCHLD to child.

Conclusion

ptrace may appear to be magic to some people, because it can examine and modify a running program. It is generally used by debuggers and system call tracing programs, such as ptrace. It opens up interesting possibilities for doing user-mode extensions as well. There have been a lot of attempts to extend the operating system on the user level. See Resources to read about UFO, a user-level extension to filesystems. ptrace also is used to employ security mechanisms.

All example code from this article and from Part I is available as a tar archive on the Linux Journal FTP site [ftp.ssc.com/pub/lj/listings/issue104/6210.tgz].

手把手教你玩转Shiro(2) 如果没有看第一篇关于Shiro的,可以先看看这篇文章哦,因为是一个前导知识  Shiro前导知识 ,,对于深层次理解会有很好的效果呢!而且本篇也有利用到前面写的知识点哦。~ 一:Shiro的认识        Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。 既然shiro将安 阅读详情

相关推荐

NGSIM数据集字段解析:从车辆轨迹到交通行为分析

本文深度解析NGSIM数据集的核心字段及其在交通行为分析中的应用。详细解读了车辆时空定位、状态属性及交互关系等关键字段,并通过高速路“幽灵堵车”和城市路口左转行为两个实战案例,展示了如何利用该数据集进行微观交通行为建模与驾驶特征提取,为交通流研究和自动驾驶算法测试提供数据基础。

weixin_29172963的博客 148

shiro使用(简单登录流程)

既然要做,就做的细致一点,对得起自己! shiro使用前配置: 1.pom文件中引入shiro-spring集成的jar包。 2.web.xml中配置shiro的过滤器,拦截所有请求。 <filter> <filter-name>shiroFilter</filter-name> <filter-class>...

本博客已迁移到 https://jacksparrow414.com 4005

车规级非易失存储选型指南:从AEC-Q100到ISO 26262的可靠性设计

非易失存储器是电子系统中负责长期保存关键数据与代码的核心组件,其可靠性直接决定了系统的长期稳定运行能力。在汽车电子领域,这一要求被提升到了极致,必须满足严苛的车规级标准。从原理上看,存储器的数据保持能力、擦写寿命等关键参数在高温、振动等恶劣环境下会急剧衰减,因此需要专门的设计与验证。其技术价值在于保障车辆全生命周期的功能安全与数据完整性,避免因存储失效导致的安全隐患或频繁维护。应用场景涵盖从发动机控制单元、自动驾驶域控制器到智能座舱的各类电子控制单元。本文聚焦于车规级非易失存储的选型,深入解析AEC-Q10

weixin_30938149的博客 459

CodeForces 176C Playing with Superglue(游戏)

题意:给你一个n*m的棋盘,第一个人的目的是让两个棋子合在一起,第二个人是阻止两个棋子合在一起,然后给你两个棋子,第一个人先走,然后第二个人在地上涂胶水,涂过胶水的位置棋子踩了上去就不能动了,问你谁能够胜利 思路:游戏题....慢慢找规律...在WA了快十次之后终于找到3 4这种数据 #include<bits/stdc++.h> using namespace s...

weixin_30480583的博客 362

Ansys Zemax实战:5步搞定多模光纤耦合效率优化(附IMAE操作数详解)

本文详细介绍了使用Ansys Zemax优化多模光纤耦合效率的5个关键步骤,包括几何图像分析设置、IMAE操作数应用及偏振效应处理。通过实战案例解析,帮助工程师快速掌握光纤耦合效率提升技巧,特别针对IMAE操作数的配置保存机制和菲涅尔损耗控制进行深度讲解。

potato的博客 691

36. 在 SAP Integration Suite 里打通入站 HTTP:iFlow 端点鉴权、角色授权与多节点 403 排障实战

本文探讨了SAP Cloud Integration中HTTP入站连接的认证与授权机制。在Cloud Foundry环境下,认证(证明身份)和授权(权限控制)需分开处理:认证可采用Basic、OAuth或mTLS等方式,授权则通过角色实现。文章详细解析了两种主流配置路径:基于IdP用户的Basic认证(需配置Role Collection)和基于OAuth Client/Service Instance的系统调用(通过Service Key管理权限)。特别指出ABAP系统在多节点环境下可能因cookie编码

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 323

Playing with ptrace

Playing with ptrace, Part II http://www.linuxjournal.com/article/6210 &lt;!-- begin content --&gt; By Pradeep Padala on Sun, 2002-12-01 02:00. SysAdmin In Part II of his series on ptrace, Pra...

jinghuainfo 186

【WLAN】【测试】WPS测试方法小结

手机 一般稍微老的手机会支持,新手机反而不支持。 比如小米6(系统版本新的话不支持)在设置——高级设置——WPS连接。 笔记本 有些笔记本(如Surface Laptop 3)天生支持WPS,比如在连接某SSID的时候,会有“也可以通过按路由器上的按钮连接”这样的字样,出来后按AP的WPS按键即可以进行连接。 Qss软件 AP mesh组网中,Controller和Agent之间通过WIFI组网,可使用WPS的方式进行连接。可利用这点用Agent来测试AP的WPS功能。 ...

一介草民,只为生活。 1万+

[转] Playing with ptrace, Part II — 玩转ptrace(二)

Playing with ptrace, Part IIby Pradeep Padala p_padala@yahoo.com http://www.cise.ufl.edu/~ppadalaCreated 2002-11-01 02:00 翻译: Magic.D E-mail: adamgic@163.com 在第一部分中我们已经看到ptrace怎么获取子进程的系统调用以及改变系统调用的参数。...

weixin_30814319的博客 316

ubuntu20.04搭建QGC4.2编译环境

QGC 4.2ubuntu18.04也可以编译QGC4.2,但是由于GLBC版本较低,无法使用最新版的QTCreator,Ubuntu20.04可以使用最新版QTCreator。

超维空间科技的博客 1万+

使用 MATLAB/Simulink 构建一个 压水堆核电机组的反应堆控制与功率调节系统 仿真模型

本文通过Simulink构建压水堆核电机组的反应堆控制与功率调节模型,详细介绍了建模流程:从中子点动力学核心方程、反应性反馈模型(控制棒、多普勒和慢化剂效应)、一回路热工模型到完整的闭环控制系统。教程包含系统结构设计、参数设置、模块连接方法,以及典型负荷变化场景的仿真分析,帮助读者理解核反应堆动态特性和控制原理。同时提供了进阶建模建议,如分布参数模型和先进控制策略的应用。该模型为教学简化版,强调核能系统的机理建模与Simulink实现方法,适合核工程与控制领域的学习者参考实践。

amy_mhd的博客 188

Playing with ptrace, Part II — 玩转ptrace(二)

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34004750的博客 162

利用 RSAU_LOG_API 把 SAP 安全审核日志接入企业 SIEM 的完整指南

在 SAP 世界里,安全审核日志(Security Audit Log,简称 SAL)是审计和合规的核心基础设施之一。系统会在后台把安全相关的关键事件记录下来,比如:(用户登录与登录失败用户锁定、解锁权限对象或角色变更远程调用(RFC)相关活动关键事务码的启动程序直接执行、批处理作业相关的敏感操作传统 ABAP 系统里,这些日志会写到本地应用服务器上的审计文件中(背后对应类似表RSAUENTR2的结构),包含事件标识、客户端、用户、终端、事务码、时间戳等信息。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 163

[转]Playing with ptrace,Part I/II

英文原文:http://www.linuxjournal.com/article/6100 http://www.linuxjournal.com/node/6210/print 中文译文:http://www.kgdb.info/gdb/playing_with_ptrace_part_i/ http://www.kgdb...

weixin_33825683的博客 104

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

玩转ptrace:【Playing with ptrace, Part 2】

在这篇文章的第一部分,我们了解了ptrace是怎么用来追踪系统调用并且修改系统调用参数的,在这篇文章中,我们研究更高级的技术如加入断点以及在正在运行的程序中插入代码。debuggers利用这些方法来设置断点以及运行调试handlers.与第一部分一样,我们这里讨论的都是针对i386体系结构的。 Attaching to a Running Process 在第一部分,我们在调用ptra

zgl07的专栏 708

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

playing with ptrace(Part I) 之一 --- 初识ptrace

文章出处:http://www.linuxjournal.com/article/6100 Nov 01, 2002  By Pradeep Padala in SysAdmin ptrace --- 允许在用户层进行系统调用的拦截与修改; 你是否想知道系统调用是如何被拦截的? 你是否尝试过通过修改系统调用的参数来愚弄内核? 你是否考虑过调试器是如

韦编二绝 2100

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Playing with ptrace, Part I

Using ptrace allows you to set up system call interception and modification at the user level. Have you ever wondered how system calls can be intercepted? Have you ever tried fooling the kern

CPP_CHEN的专栏 969
上一篇: Playing with ptrace, Part I
下一篇: Linux 中 x86 的内联汇编
iiprogram
博客等级 码龄25年 1068粉丝 1515原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值