找回密码
 立即注册
首页 业界区 业界 Tauri新手向 - 基于LSB隐写的shellcode加载器 ...

Tauri新手向 - 基于LSB隐写的shellcode加载器

拓拔梨婷 2025-6-2 00:27:50
1.jpeg

此篇是记录自己初次学习tauri开发工具,包含遇到的一些问题以及基本的知识,也给想上手rust tauri的师傅们一些小小的参考。此项目为保持免杀性暂不开源,希望各位师傅多多支持,反响可以的话后续会放出代码大家一起交流学习。
ShadowMeld - 基于图像隐写技术的载荷生成框架
通过将加密的二进制指令集嵌入到常规图片的像素数据中,生成具备合法外观的载体文件。配套生成的加载程序(Loader)会自动解析图片中的隐藏数据,在内存中完成指令重组与执行,实现无文件形态的隐蔽通信。
项目地址: https://github.com/BKLockly/ShadowMeld,记录和开发不易,还望多多支持。

1. 1. 初始化

1.1 1.1 创建项目
  1. npm create tauri-app@latest
复制代码
2.png

安装依赖以初始化,接着调试。
  1. cd tauri-app
  2. npm install
  3. npm run tauri dev
复制代码

1.2 1.2 解决报错

遇到以下报错:
3.png

查到的方案:编辑.cargo/config.toml​ 文件,添加以下内容以抑制特定警告以及版本锁定,避免 ICU 库的符号冲突:
  1. [build]
  2. rustflags = [
  3.   "-C", "link-arg=/IGNORE:4078",  # 忽略 .drectve 警告
  4.   "-C", "link-arg=/NODEFAULTLIB:LIBCMT.lib" # 解决 ICU 库冲突
  5. ]
  6. [dependencies]
  7. icu_provider = "1.3.0"
  8. icu_locid = "1.3.0"
复制代码
还有报错,deepseek提示可能是MSVC工具链时缺少必要的Visual Studio构建工具。
4.png

重新更新一下生成工具,注意勾选:
5.png

最后再次尝试就完成了基本的初始化了。
6.png


2. 2. 前端构建

2.1 2.1 安装组件

UI组件我使用Ant Design Vue,在根目录下执行以安装:
  1. npm install ant-design-vue@4.x --save
复制代码
之后在main.ts​中使用并引入样式:
  1. import { createApp } from "vue";
  2. import App from "./App.vue";
  3. import { DatePicker } from 'ant-design-vue';
  4. import 'ant-design-vue/dist/reset.css';
  5. const app = createApp(App);
  6. app.use(DatePicker);
  7. app.mount("#app");
复制代码
接着安装图标:
  1. npm install --save @ant-design/icons-vue
复制代码
图标的使用也很简单,例如:
  1. import { GithubOutlined } from '@ant-design/icons-vue';
  2. <GithubOutlined title="点击打开项目主页" @click="openGitHub"/>
复制代码
于此处查看其他图标:传送门
7.png


0.1 2.2 按需引入

使用unplugin-vue-components​来导入所使用到的组件,就不需要全部打包进来来节省体积:
  1. npm install unplugin-vue-components -D
复制代码
之后编辑vite.config.js​,添加如下行数以启用:
  1. import { defineConfig } from "vite";
  2. import vue from "@vitejs/plugin-vue";
  3. + import Components from 'unplugin-vue-components/vite';
  4. + import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
  5. // @ts-expect-error process is a nodejs global
  6. const host = process.env.TAURI_DEV_HOST;
  7. // https://vitejs.dev/config/
  8. export default defineConfig(async () => ({
  9.   plugins: [
  10.       vue(),
  11. +      Components({
  12. +          resolvers: [
  13. +             AntDesignVueResolver({
  14. +                  importStyle: false, // css in js
  15. +              }),
  16. +          ],
  17. +      }),
  18.   ],
  19.   // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
  20.   //
  21.   // 1. prevent vite from obscuring rust errors
  22.   clearScreen: false,
  23.   // 2. tauri expects a fixed port, fail if that port is not available
  24.   server: {
  25.     port: 1420,
  26.     strictPort: true,
  27.     host: host || false,
  28.     hmr: host
  29.       ? {
  30.           protocol: "ws",
  31.           host,
  32.           port: 1421,
  33.         }
  34.       : undefined,
  35.     watch: {
  36.       // 3. tell vite to ignore watching `src-tauri`
  37.       ignored: ["**/src-tauri/**"],
  38.     },
  39.   },
  40. }));
复制代码
测试一下能否正常使用组件,直接在App.vue中添加,可以看到新添加的按钮就完成了。
  1. + test</a-button>
复制代码
8.png


0.2 2.3 路径指向

等会准备页面时将会拆分成各部分组件再引入,这里配置@来引用各部分组件,首先先安装@types/node​:
  1. npm install @types/node
复制代码
接着配置vite.config.ts​:
  1. +import { resolve } from 'path';
  2. const host = process.env.TAURI_DEV_HOST;
  3. export default defineConfig(async () => ({
  4.   plugins: [
  5.       vue(),
  6.       Components({
  7.           resolvers: [
  8.               AntDesignVueResolver({
  9.                   importStyle: false,
  10.               }),
  11.           ],
  12.       }),
  13.   ],
  14. +  resolve: {
  15. +   alias: [
  16. +      {
  17. +        find: '@',
  18. +          replacement: resolve(__dirname, './src')
  19. +     }
  20. +    ]
  21. +  },
复制代码
根目录下tsconfig.json​文件中接着配置:
  1. {
  2.    /*注意添加在这里*/
  3.   "compilerOptions": {
  4.     "target": "ES2020",
  5.     "useDefineForClassFields": true,
  6.     "module": "ESNext",
  7.     "lib": ["ES2020", "DOM", "DOM.Iterable"],
  8.     "skipLibCheck": true,
  9.     /* Bundler mode */
  10.     "moduleResolution": "bundler",
  11.     "allowImportingTsExtensions": true,
  12.     "resolveJsonModule": true,
  13.     "isolatedModules": true,
  14.     "noEmit": true,
  15.     "jsx": "preserve",
  16.     /* Linting */
  17.     "strict": true,
  18.     "noUnusedLocals": true,
  19.     "noUnusedParameters": true,
  20.     "noFallthroughCasesInSwitch": true,
  21. +   "baseUrl": ".",
  22. +   "paths": {
  23. +      "@/*": ["src/*"]
  24. +    }
  25.   },
  26.   "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  27.   "references": [{ "path": "./tsconfig.node.json" }]
  28. }
复制代码
验证一下,新建一个目录:/src/components​并将前面测试ui组件的代码单独放入一个组件:
  1. <template>
  2.   test</a-b
  3. utton>
  4. </template>
复制代码
然后在App.vue​中引入方式修改:
  1. - import { Button } from 'ant-design-vue';
  2. + import Test from "@/components/Test.vue";
  3. - <Button type="primary" html-type="submit">test</Button>
  4. + <Test />
复制代码
显示的效果是一样的,相比使用../这种相对路径引入,因为@指向绝对路径,从项目根目录开始解析,所以在以后移动文件时就不用修改路径。

0.3 2.4 页面布局

将App.vue​中的内容清空,先设定一个大概的布局,如下划分:
  1. <template>
  2.   
  3.    
  4.       Header
  5.     </a-layout-header>
  6.    
  7.       Content
  8.     </a-layout-content>
  9.    
  10.       Footer
  11.     </a-layout-footer>
  12.   </a-layout>
  13. </template>
复制代码
9.png

在components目录下创建三个组件: Footer.vue​,Content.vue​, Header.vue​,这里以footer为例:
  1. <template>
  2.   
  3.     By Lockly
  4.    
  5.   </a-flex>
  6. </template>
复制代码

0.4 2.5 全局主题

颜狗时刻,由于这个4.0 版本中默认提供了三套预设算法,这里就使用默认+紧凑的组合(当然还有暗色,看自己喜好,具体内容参见官方文档),让页面美观些:
  1.   
  2.        
  3. </a-configProvider>
复制代码


1. 3. 后端实现

1.1 3.1 命令调用

前面是将版本和作者都写死在前端,接下来看怎么和后端通讯来获取。还是以footer为例,版本号和作者在src-tauri/Cargo.toml​中是有定义的,修改环境变量:
  1. [package]
  2. name = "shadowmeld"
  3. version = "0.1.0"
  4. description = "A Tauri App"
  5. authors = ["Lockly"]
  6. edition = "2024"
复制代码
然后可以使用env! ​宏来获取 version​ 和 authors​ 字段,Tauri 官方推荐的核心通信方式是使用命令调用,即使用 #[tauri::command]​ 宏定义函数,并注册到 Tauri 上下文:
  1. #[tauri::command]
  2. fn get_version() -> String {
  3.     env!("CARGO_PKG_VERSION").to_string()
  4. }
  5. #[tauri::command]
  6. fn get_author() -> String {
  7.     env!("CARGO_PKG_AUTHORS").to_string()
  8. }
  9. #[cfg_attr(mobile, tauri::mobile_entry_point)]
  10. pub fn run() {
  11.     tauri::Builder::default()
  12.         .plugin(tauri_plugin_opener::init())
  13.         .invoke_handler(tauri::generate_handler![get_version, get_author]) // 在这里注册命令
  14.         .run(tauri::generate_context!())
  15.         .expect("error while running tauri application");
  16. }
复制代码
这样就可以在前端使用 invoke​ 方法来调用Rust函数,这是支持异步的:
  1. <template>
  2.   
  3.     By {{author}}
  4.    
  5.   </a-flex>
  6. </template>
复制代码
页面能正常得到后端数据并显示:
10.png


1.2 3.2 事件机制

涉及到状态栏的变化实现,首先头部应该是提示使用者完善表单,当校验无误后点击执行,此时头部应该显示为加载中,这一点就需要后端向前端发送事件,前端监听响应。这点就需要用到事件机制​,他是实现前端(Web 页面)与后端(Rust 代码)双向通信的核心方式。
首先在前端先监听响应,则需要用到@tauri-apps/api/event​中的listen​。例如假设show用于控制某个组件的显隐:
  1. import { listen } from '@tauri-apps/api/event';
  2. listen('backend-event', (event) => {
  3.   if (event.payload === 1 ) {
  4.         show.value == true;
  5.   } else {
  6.         show.value == false;
  7.   }
  8. });
复制代码
在这里定义了一个事件名称backend-event​,命名规范并且前后对应就好。然后来到后端在我们需要发送消息的地方使用:
  1. window.emit("backend-event", 1).unwrap();
复制代码
这里我是传一个1过去,此处可传空消息或者json序列化的内容。然后window怎么定义的呢,同前面的app一样,比如我这个调用方法为process_files​,如下:
  1. #[tauri::command]fn process_files(      app: tauri::AppHandle,+     window: tauri::Window,   
  2.        
  3. </a-configProvider>  image_path: String,           shellcode_path: String,      secret_key: String,) -> String {        window.emit("backend-event", "hi").unwrap();}
复制代码
以上是后向前,前向后的通信我此处用不上,但还是写一些。从前端发送就是注意它会自动序列化为json,比如:
  1. import { emit } from '@tauri-apps/api/event';
  2. // 无数据
  3. await emit('frontend-to-backend-event');
  4. // 自动序列化为 JSON
  5. await emit('user-login', { username: 'Lockly', token: '123' });
复制代码
后端通过 AppHandle​ 或事件循环监听:
  1. use tauri::{AppHandle, Manager, Event};
  2. // 监听所有frontend-to-backend-event
  3. app.listen_global("frontend-to-backend-event", |event| {
  4.     println!("收到前端事件,数据: {:?}", event.payload());
  5. });
  6. app.window().listen("user-login", |event| {
  7.     let payload: Option<LoginData> = event.payload(); // 自动反序列化
  8.     // ...
  9. });
复制代码
至于多窗口此处不涉及就不接着写了。

1.3 3.3 功能实现

1.3.1 3.3.1 打开外部链接

因为工具简单也没必要留个关于页面,索性就直接打开GitHub链接,这一点跟wails一样也已经有现成的api: shell​。先安装插件:
  1. npm run tauri add shell
复制代码
在foot.vue​中使用open​即可:
  1. import { open } from '@tauri-apps/plugin-shell';
  2. const openGitHub = async () => {
  3.   await open('https://github.com/BKLockly/ShadowMeld');
  4. }
复制代码


1.3.2 3.3.2 获取本地图片路径

要获取本地图片的绝对路径会用到dialog插件,先添加依赖:
  1. npm run tauri add dialog
复制代码
如官方给出的例子这样使用可获得路径:
  1. import { open } from '@tauri-apps/plugin-dialog';
  2. // Open a dialog
  3. const file = await open({
  4.   multiple: false,
  5.   directory: false,
  6. });
  7. console.log(file);
  8. // Prints file path and name to the console
复制代码
而对于在rust中使用dialog的例子如下:
  1. use tauri_plugin_dialog::DialogExt;
  2. let file_path = app.dialog().file().blocking_pick_file();
  3. // return a file_path `Option`, or `None` if the user closes the dialog
复制代码
这里可能会有疑问这个app是怎么定义的呢?他的类型为AppHandle ,是通过 Tauri 的运行时自动传递的,不能直接在函数中初始化。得通过 Tauri 的上下文来获取,那么直接传入即可例如:fn process_files(app: tauri::AppHandle, image_path: String, shellcode_path: String) -> String {...}​ 而在前端invoke时只需要传入后两个参数即可。

1.3.3 3.3.3 图片转换

得到图片的绝对路径后现在的问题是前端是无法访问的,但tauri中提供了api,即使用convertFileSrc,首先安装一下插件:
  1. npm add @tauri-apps/api
复制代码
他可以将一个将本地文件路径转换为可在 Webview 中安全加载的 URL。但是我此时搜到的在tauri.conf.json​中的配置都是过时的(用不了了),比如:
  1. {
  2.     "allowlist": {
  3.         "dialog": {
  4.             "all": true,
  5.             "open": true
  6.         },
  7.         "protocol": {
  8.             "all": false,
  9.             "asset": true,
  10.             "assetScope": [
  11.                 "$PICTURE"
  12.             ]
  13.         }
  14.     },
  15.     "security": {
  16.         "csp": "default-src 'self'; img-src 'self'; asset: https://asset.localhost"
  17.     }
  18. }
复制代码
这样配置必定爆: Error tauri.conf.json error on app: Additional properties are not allowed ('allowlist' was unexpected)​。说明如下:
11.png

首先是配置csp以允许加载本地资源和特定域名的图片
  1. "csp": "default-src 'self'; img-src 'self' data: blob: asset: https://asset.localhost"
复制代码
然后配置层级结果已经发生改变,应如下:
  1.   "app": {
  2.     "windows": [
  3.       {
  4.         "title": "ShadowMeld",
  5.         "width": 450,
  6.         "height": 600
  7.       }
  8.     ],
  9.     "security": {
  10.       "assetProtocol": {
  11.         "enable": true
  12.       },
  13.       "csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' asset://localhost data:; font-src 'self' asset://localhost data:; asset: https://asset.localhost"
  14.     }
  15.   },
复制代码
回到前端看:
  1. const filePath = await open({
  2.       multiple: false,
  3.       filters: [
  4.         { name: 'Image Files', extensions: ['png', 'jpeg', 'jpg'], },
  5.       ]
  6.     })
  7.     if (filePath) {
  8.       formState.imagePath = filePath
  9.       preview.value = convertFileSrc(filePath)
  10.       console.log(preview.value)
  11.     }
  12. // ...
  13. <img :src="preview"/>
复制代码
13.png


1.4 3.4 核心功能

这次自私一点,暂时不发布源码保证时效性。后续如果使用和支持的人多了再放出来一起学习交流。核心功能主要实现以下两点:

1.5 3.5 LSB隐写

主要实现将shellcode隐匿于图片之中,如果只是简单的直接将shellcode写入图片的RGBA通道的alpha通道中,那就会导致alpha通道的分布不均匀,容易被检测出来。
要提高隐蔽性的话,在这点上就得做出变动。不光只是使用alpha通道,而是把数据分散到所有四个通道,然后采用LSB(最低有效位)隐写。
以目前的效果来试着简单看看对比,用 Stegsolve 的 Image Combiner 来对比一下,xor(将两张图片的像素按位异或。若两张图片完全相同,所有像素异或结果为 0​,即全黑)效果如下:
14.png

这里看着几乎全黑,没有零星的白点。可能是修改的像素点比较少,太稀疏了看不出来。那换用sub,即用一张图减去另一张图的结果:
15.png

原本透明的背景变成了绿色,如果是同一图片,所有像素差值应为0并显示全黑。但这也有意外:

  • Alpha 通道干扰:如果图片包含透明区域(Alpha 通道为 0​),某些工具在减法计算时会忽略 Alpha 通道,导致透明区域的 RGB 差值被错误解析(例如显示绿色)。
  • 工具处理误差:Stegsolve 在处理完全相同的图片时,可能因像素格式转换误差(如 RGB 与 RGBA 混用)显示假色。
对比原图发现也是这样,目前通过这样比对还是看不出区别,还需要去提取数据和使用其他进行对比,这里不深究了。

1.6 3.6 模版生成

上面完成了将shellcode写入图片之中,接下来到loader的流程其实就是分离加载,提取出shellcode来执行。但shellcode不能直接写入要进行加密,加密和提取当前目录下的哪张图片是变量,如果每次都要使用者自己修改再编译一次也太过麻烦。
另外考虑到很多人都没有配置rust环境,故使用特殊字符包裹两个变量 image_name 和 key,例如 ###IMAGE_NAME###​ 和 ###KEY###​。编译模板 loader时,用这些特征值作为占位符。之后就读取模板 文件,查找特征值并替换为新的 image_name 和 key。预先确定一个足够的长度,确保替换后的数据长度与占位符长度一致,避免破坏 EXE 结构。不足的位数用空白填充使用时剥离即可。

2. 4. 其他

2.1 4.1 更换图标

准备一个尺寸为1240 x 1240 的 PNG(图片必须是正方形的,有必要转换一下可以跳转) ,命名为app-icon.png​,将图片文件放置在项目的根目录。
16.png

在根目录下执行后会生成相关尺寸的图标。
17.png

生成的图标将放置于src-tauri\icons\​,之后tauri会自动使用它。
18.png


2.2 4.2 1.2 编译优化

通用方案来缩小一下生成的loader的大小,在cargo.toml​中如下配置:
  1. [profile.release]
  2. codegen-units = 1
  3. # 允许 LLVM 执行更好的优化。
  4. lto = true
  5. # 启用链接时优化。
  6. opt-level = "s"
  7. # 优先考虑小的二进制文件大小。
  8. panic = "abort"
  9. # 通过禁用 panic 处理程序来提高性能。
  10. strip = true
  11. # 确保移除调试符号。
复制代码
最后使用--release​模式来编译:
  1. cargo build --release
复制代码

2.3 4.3 1.3 打包问题

编译打包成exe后运行发现有问题,但很奇怪但我同时开起npm run tauri dev​后,再刷新页面又正常了。
19.png

搜了半天没有结果,到后面发现是我根本没注意到打包命令有误,我直接用的cargo build --release​,这样只会对后端代码进行优化编译,而不会处理前端资源的打包。正确的应该使用:
  1. npm run tauri build
复制代码

2.3.1 4.3.1 1.3.1 Wix314

但运行后报错,“invalid peer certificate: UnknownIssuer” 提示Tauri构建过程中无法验证GitHub的SSL证书,导致下载wix314-binaries.zip​失败。
20.png

直接下载提示的链接内容,将其解压于$USERPROFILE\AppData\Local\tarui\WixTools314​:
21.png

清理掉残留的旧文件然后再次尝试打包,但因为要使用发布模式来减小生成物体积故加上参数'-- --release'​:
  1. cls; cargo clean; npm run tauri build '-- --release'
复制代码

2.3.2 4.3.2 1.3.2 NSIS

但遇到同样的问题如下:
22.png

同样的操作先自行下载链接内容,解压至最终如下(注意目录名为NSIS):
23.png

接着还需要下载NSIS-ApplicationID插件,解压至NSIS/Plugin​目录下:
24.png

接着将NSIS-ApplicationID\\ReleaseUnicode\\ApplicationID.dll​复制到NSIS/Plugins/x86-unicode​下。
然后下载nsis\\\_tauri\\\_utils.dll​复制到NSIS/Plugins/x86-unicode​下(这里的链接可能会更新,根据他报错提示的来就好,同样的报错我不贴图了),最后再试一次ok,大约6MB:
25.png


3. 5. 2. 效果

以下测试均不包含任何反沙箱手段,加载shellcode的方式也仅为传统直接的创建线程执行。loader本体不压缩不添加资源和签名,接下来loader和图片都直接裸奔测试。

3.1 2.1 沙箱

3.1.1 2.1.1 VirusTotal

vt检出loader为1/73​, 图片为0/61​:
26.png

27.png


360云沙箱

均未检出。
28.png

29.png


3.1.2 微步云沙箱

这里提到检测出url -> http://ns.adobe​, 问了deepseek说原因可能如下:

  • 图像元数据残留:使用了 image 库处理 PNG 文件。PNG 格式可能包含 XMP 元数据(Adobe 的标准元数据格式),这些字符串会被 image 库读取到内存中。
  • 依赖库的隐式行为:image 库在解码 PNG 时,可能触发对 Adobe 命名空间(如 http://ns.adobe.com/xap/1.0/)的引用,尤其是处理由 Photoshop 生成的 PNG 文件时。
30.png

31.png



3.1.3 安恒云沙箱

图片和loader均报告安全:
32.png

33.png


3.2 上线测试

尽管没有使用任何反沙箱手段,甚至都还弹黑框,但如下所示测试动静态均已通过。

3.2.1 腾讯电脑管家

34.png


3.2.2 火绒

35.png


3.2.3 360

36.png


3.2.4 Defender

37.png




来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册