此篇是记录自己初次学习tauri开发工具,包含遇到的一些问题以及基本的知识,也给想上手rust tauri的师傅们一些小小的参考。此项目为保持免杀性暂不开源,希望各位师傅多多支持,反响可以的话后续会放出代码大家一起交流学习。

ShadowMeld - 基于图像隐写技术的载荷生成框架

通过将加密的二进制指令集嵌入到常规图片的像素数据中,生成具备合法外观的载体文件。配套生成的加载程序(Loader)会自动解析图片中的隐藏数据,在内存中完成指令重组与执行,实现无文件形态的隐蔽通信。

项目地址: https://github.com/BKLockly/ShadowMeld,记录和开发不易,还望多多支持。

1. 1. 初始化

1.1 1.1 创建项目

npm create tauri-app@latest

安装依赖以初始化,接着调试。

cd tauri-app
npm install
npm run tauri dev

1.2 1.2 解决报错

遇到以下报错:

查到的方案:编辑.cargo/config.toml​ 文件,添加以下内容以抑制特定警告以及版本锁定,避免 ICU 库的符号冲突:

[build]
rustflags = [
"-C", "link-arg=/IGNORE:4078", # 忽略 .drectve 警告
"-C", "link-arg=/NODEFAULTLIB:LIBCMT.lib" # 解决 ICU 库冲突
] [dependencies]
icu_provider = "1.3.0"
icu_locid = "1.3.0"

还有报错,deepseek提示可能是MSVC工具链时缺少必要的Visual Studio构建工具。

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

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

2. 2. 前端构建

2.1 2.1 安装组件

UI组件我使用Ant Design Vue,在根目录下执行以安装:

npm install ant-design-vue@4.x --save

之后在main.ts​中使用并引入样式:

import { createApp } from "vue";
import App from "./App.vue";
import { DatePicker } from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css'; const app = createApp(App);
app.use(DatePicker);
app.mount("#app");

接着安装图标:

npm install --save @ant-design/icons-vue

图标的使用也很简单,例如:

import { GithubOutlined } from '@ant-design/icons-vue';
<GithubOutlined title="点击打开项目主页" @click="openGitHub"/>

于此处查看其他图标:传送门

2.2 2.2 按需引入

使用unplugin-vue-components​来导入所使用到的组件,就不需要全部打包进来来节省体积:

npm install unplugin-vue-components -D

之后编辑vite.config.js​,添加如下行数以启用:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
+ import Components from 'unplugin-vue-components/vite';
+ import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'; // @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST; // https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [
vue(),
+ Components({
+ resolvers: [
+ AntDesignVueResolver({
+ importStyle: false, // css in js
+ }),
+ ],
+ }),
], // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
// 1. prevent vite from obscuring rust errors
clearScreen: false,
// 2. tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: "ws",
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**"],
},
},
}));

测试一下能否正常使用组件,直接在App.vue中添加,可以看到新添加的按钮就完成了。

+ <a-button type="primary" html-type="submit">test</a-button>

2.3 2.3 路径指向

等会准备页面时将会拆分成各部分组件再引入,这里配置@来引用各部分组件,首先先安装@types/node​:

npm install @types/node

接着配置vite.config.ts​:

+import { resolve } from 'path';

const host = process.env.TAURI_DEV_HOST;

export default defineConfig(async () => ({
plugins: [
vue(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false,
}),
],
}),
],
+ resolve: {
+ alias: [
+ {
+ find: '@',
+ replacement: resolve(__dirname, './src')
+ }
+ ]
+ },

根目录下tsconfig.json​文件中接着配置:

{
/*注意添加在这里*/
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true, /* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve", /* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, + "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

验证一下,新建一个目录:/src/components​并将前面测试ui组件的代码单独放入一个组件:

<script setup lang="ts">
</script> <template>
<a-button type="primary" html-type="submit">test</a-b
utton>
</template> <style scoped> </style>

然后在App.vue​中引入方式修改:

- import { Button } from 'ant-design-vue';
+ import Test from "@/components/Test.vue"; - <Button type="primary" html-type="submit">test</Button>
+ <Test />

显示的效果是一样的,相比使用../这种相对路径引入,因为@指向绝对路径,从项目根目录开始解析,所以在以后移动文件时就不用修改路径。

2.4 2.4 页面布局

App.vue​中的内容清空,先设定一个大概的布局,如下划分:

<script setup lang="ts">
const headerStyle = { backgroundColor: 'aquamarine' }
const contentStyle = { backgroundColor: 'aqua', padding: '10px' }
const footerStyle = { backgroundColor: 'chartreuse' }
</script> <template>
<a-layout style="height: 100vh;">
<a-layout-header :style="headerStyle">
Header
</a-layout-header> <a-layout-content :style="contentStyle">
Content
</a-layout-content> <a-layout-footer :style="footerStyle">
Footer
</a-layout-footer>
</a-layout>
</template>

在components目录下创建三个组件: Footer.vue​,Content.vue​, Header.vue​,这里以footer为例:

<script setup lang="ts">
</script> <template>
<a-flex gap="middle" justify="space-between">
<span>By Lockly</span>
<a-badge status="success" text="v1.0.0"/>
</a-flex>
</template> <style scoped>
</style>

2.5 2.5 全局主题

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

  <a-configProvider :theme="{
algorithm: [defaultAlgorithm, compactAlgorithm],
}"
>
<!-- ... 自己的内容 -->
</a-configProvider>

3. 3. 后端实现

3.1 3.1 命令调用

前面是将版本和作者都写死在前端,接下来看怎么和后端通讯来获取。还是以footer为例,版本号和作者在src-tauri/Cargo.toml​中是有定义的,修改环境变量:

[package]
name = "shadowmeld"
version = "0.1.0"
description = "A Tauri App"
authors = ["Lockly"]
edition = "2024"

然后可以使用env! ​宏来获取 version​ 和 authors​ 字段,Tauri 官方推荐的核心通信方式是使用命令调用,即使用 #[tauri::command]​ 宏定义函数,并注册到 Tauri 上下文:

#[tauri::command]
fn get_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
} #[tauri::command]
fn get_author() -> String {
env!("CARGO_PKG_AUTHORS").to_string()
} #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![get_version, get_author]) // 在这里注册命令
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

这样就可以在前端使用 invoke​ 方法来调用Rust函数,这是支持异步的:

<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { invoke } from "@tauri-apps/api/core"; const version = ref('')
const author = ref('') const formattedVersion = computed(() => `V ${version.value}`) onMounted(async () => {
try {
author.value = await invoke('get_author')
version.value = await invoke('get_version')
} catch (error) {
console.error('Error fetching version or author:', error)
}
})
</script> <template>
<a-flex gap="middle" justify="space-between">
<span>By {{author}}</span>
<a-badge status="success" :text="formattedVersion"/>
</a-flex>
</template> <style scoped> </style>

页面能正常得到后端数据并显示:

3.2 3.2 事件机制

涉及到状态栏的变化实现,首先头部应该是提示使用者完善表单,当校验无误后点击执行,此时头部应该显示为加载中,这一点就需要后端向前端发送事件,前端监听响应。这点就需要用到事件机制​,他是实现前端(Web 页面)与后端(Rust 代码)双向通信的核心方式。

首先在前端先监听响应,则需要用到@tauri-apps/api/event​中的listen​。例如假设show用于控制某个组件的显隐:

import { listen } from '@tauri-apps/api/event';

listen('backend-event', (event) => {
if (event.payload === 1 ) {
show.value == true;
} else {
show.value == false;
}
});

在这里定义了一个事件名称backend-event​,命名规范并且前后对应就好。然后来到后端在我们需要发送消息的地方使用:

window.emit("backend-event", 1).unwrap();

这里我是传一个1过去,此处可传空消息或者json序列化的内容。然后window怎么定义的呢,同前面的app一样,比如我这个调用方法为process_files​,如下:

#[tauri::command]
fn process_files(
app: tauri::AppHandle,
+ window: tauri::Window,
image_path: String,
shellcode_path: String,
secret_key: String,
) -> String {
window.emit("backend-event", "hi").unwrap();
}

以上是后向前,前向后的通信我此处用不上,但还是写一些。从前端发送就是注意它会自动序列化为json,比如:

import { emit } from '@tauri-apps/api/event';

// 无数据
await emit('frontend-to-backend-event'); // 自动序列化为 JSON
await emit('user-login', { username: 'Lockly', token: '123' });

后端通过 AppHandle​ 或事件循环监听:

use tauri::{AppHandle, Manager, Event};

// 监听所有frontend-to-backend-event
app.listen_global("frontend-to-backend-event", |event| {
println!("收到前端事件,数据: {:?}", event.payload());
}); app.window().listen("user-login", |event| {
let payload: Option<LoginData> = event.payload(); // 自动反序列化
// ...
});

至于多窗口此处不涉及就不接着写了。

3.3 3.3 功能实现

3.3.1 3.3.1 打开外部链接

因为工具简单也没必要留个关于页面,索性就直接打开GitHub链接,这一点跟wails一样也已经有现成的api: shell​。先安装插件:

npm run tauri add shell

foot.vue​中使用open​即可:

import { open } from '@tauri-apps/plugin-shell';

const openGitHub = async () => {
await open('https://github.com/BKLockly/ShadowMeld');
}

3.3.2 3.3.2 获取本地图片路径

要获取本地图片的绝对路径会用到dialog插件,先添加依赖:

npm run tauri add dialog

如官方给出的例子这样使用可获得路径:

import { open } from '@tauri-apps/plugin-dialog';

// Open a dialog
const file = await open({
multiple: false,
directory: false,
});
console.log(file);
// Prints file path and name to the console

而对于在rust中使用dialog的例子如下:

use tauri_plugin_dialog::DialogExt;

let file_path = app.dialog().file().blocking_pick_file();
// 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时只需要传入后两个参数即可。

3.3.3 3.3.3 图片转换

得到图片的绝对路径后现在的问题是前端是无法访问的,但tauri中提供了api,即使用convertFileSrc,首先安装一下插件:

npm add @tauri-apps/api

他可以将一个将本地文件路径转换为可在 Webview 中安全加载的 URL。但是我此时搜到的在tauri.conf.json​中的配置都是过时的(用不了了),比如:

{
"allowlist": {
"dialog": {
"all": true,
"open": true
},
"protocol": {
"all": false,
"asset": true,
"assetScope": [
"$PICTURE"
]
}
},
"security": {
"csp": "default-src 'self'; img-src 'self'; asset: https://asset.localhost"
}
}

这样配置必定爆: Error tauri.conf.json error on app: Additional properties are not allowed ('allowlist' was unexpected)​。说明如下:

首先是配置csp以允许加载本地资源和特定域名的图片

"csp": "default-src 'self'; img-src 'self' data: blob: asset: https://asset.localhost"

然后配置层级结果已经发生改变,应如下:

  "app": {
"windows": [
{
"title": "ShadowMeld",
"width": 450,
"height": 600
}
],
"security": {
"assetProtocol": {
"enable": true
},
"csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' asset://localhost data:; font-src 'self' asset://localhost data:; asset: https://asset.localhost"
}
},

回到前端看:

 const filePath = await open({
multiple: false,
filters: [
{ name: 'Image Files', extensions: ['png', 'jpeg', 'jpg'], },
]
}) if (filePath) {
formState.imagePath = filePath preview.value = convertFileSrc(filePath)
console.log(preview.value)
}
// ...
<img :src="preview"/>

3.4 3.4 核心功能

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

3.5 3.5 LSB隐写

主要实现将shellcode隐匿于图片之中,如果只是简单的直接将shellcode写入图片的RGBA通道的alpha通道中,那就会导致alpha通道的分布不均匀,容易被检测出来。

要提高隐蔽性的话,在这点上就得做出变动。不光只是使用alpha通道,而是把数据分散到所有四个通道,然后采用LSB(最低有效位)隐写。

以目前的效果来试着简单看看对比,用 Stegsolve 的 Image Combiner 来对比一下,xor(将两张图片的像素按位异或。若两张图片完全相同,所有像素异或结果为 0​,即全黑)效果如下:

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

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

  • Alpha 通道干扰:如果图片包含透明区域(Alpha 通道为 0​),某些工具在减法计算时会忽略 Alpha 通道,导致透明区域的 RGB 差值被错误解析(例如显示绿色)。
  • 工具处理误差:Stegsolve 在处理完全相同的图片时,可能因像素格式转换误差(如 RGB 与 RGBA 混用)显示假色。

对比原图发现也是这样,目前通过这样比对还是看不出区别,还需要去提取数据和使用其他进行对比,这里不深究了。

3.6 3.6 模版生成

上面完成了将shellcode写入图片之中,接下来到loader的流程其实就是分离加载,提取出shellcode来执行。但shellcode不能直接写入要进行加密,加密和提取当前目录下的哪张图片是变量,如果每次都要使用者自己修改再编译一次也太过麻烦。

另外考虑到很多人都没有配置rust环境,故使用特殊字符包裹两个变量 image_name 和 key,例如 ###IMAGE_NAME###​ 和 ###KEY###​。编译模板 loader时,用这些特征值作为占位符。之后就读取模板 文件,查找特征值并替换为新的 image_name 和 key。预先确定一个足够的长度,确保替换后的数据长度与占位符长度一致,避免破坏 EXE 结构。不足的位数用空白填充使用时剥离即可。

4. 4. 其他

4.1 4.1 更换图标

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

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

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

4.2 4.2 1.2 编译优化

通用方案来缩小一下生成的loader的大小,在cargo.toml​中如下配置:

[profile.release]
codegen-units = 1
# 允许 LLVM 执行更好的优化。
lto = true
# 启用链接时优化。
opt-level = "s"
# 优先考虑小的二进制文件大小。
panic = "abort"
# 通过禁用 panic 处理程序来提高性能。
strip = true
# 确保移除调试符号。

最后使用--release​模式来编译:

cargo build --release

4.3 4.3 1.3 打包问题

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

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

npm run tauri build

4.3.1 4.3.1 1.3.1 Wix314

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

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

清理掉残留的旧文件然后再次尝试打包,但因为要使用发布模式来减小生成物体积故加上参数'-- --release'​:

cls; cargo clean; npm run tauri build '-- --release'

4.3.2 4.3.2 1.3.2 NSIS

但遇到同样的问题如下:

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

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

接着将NSIS-ApplicationID\\ReleaseUnicode\\ApplicationID.dll​复制到NSIS/Plugins/x86-unicode​下。

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

5. 5. 2. 效果

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

5.1 2.1 沙箱

5.1.1 2.1.1 VirusTotal

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

5.1.2 微步云沙箱

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

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

5.1.3 安恒云沙箱

图片和loader均报告安全:

5.2 上线测试

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

5.2.1 腾讯电脑管家

5.2.2 火绒

5.2.3 360

5.2.4 Defender

卡巴斯基

Tauri新手向 - 基于LSB隐写的shellcode加载器的更多相关文章

  1. LSB隐写加密MISC

    没有做过LSB隐写加密的题目,在buuoj上面做到了就记录一下,估计后面很长的时间都会在这个平台上面训练自己的MISC和WEB,是很好的平台,把很多比赛的原题和安恒的周赛的复现了. 题目是MISC里面 ...

  2. 《你必须知道的.NET》读书实践:一个基于OO的万能加载器的实现

    此篇已收录至<你必须知道的.Net>读书笔记目录贴,点击访问该目录可以获取更多内容. 一.关于万能加载器 简而言之,就是孝顺的小王想开发一个万能程序,可以一键式打开常见的计算机资料,例如: ...

  3. 基于JRebel开发的MybatisPlus热加载插件

    前言 前天项目中使用了mybatis-plus,但是搭配Jrebel开发项目时,发现修改mapper的xml,或者mapper方法中的注解,Jrebel并没有能够reload mapper.于是就有了 ...

  4. MUI - 基于plus.downloader的图片懒加载功能,支持本地缓存

    基于plus.downloader的图片懒加载功能,支持本地缓存 简单说一下 在app中,对一些变动不频繁的图片数据(如个人头像等),是需要存储在本地的.我相信这对大多数的app都是强需求的. 怎么使 ...

  5. C++ 基于libbfd实现二进制加载器

    构建工具解析二进制文件,基于libbfd实现,提取符号和节 BFD库 文档参考: LIB BFD, the Binary File Descriptor Library BFD及Binary File ...

  6. AngularJS进阶(三十九)基于项目实战解析ng启动加载过程

    基于项目实战解析ng启动加载过程 前言 在AngularJS项目开发过程中,自己将遇到的问题进行了整理.回过头来总结一下angular的启动过程. 下面以实际项目为例进行简要讲解. 1.载入ng库 2 ...

  7. 写个重新加载 ocelot 配置的接口

    写个重新加载 ocelot 配置的接口 Intro 我们想把 ocelot 的配置放在自己的存储中,放在 Redis 或者数据库中,当修改了 Ocelot 的配置之后希望即时生效,又不想在网关这边定时 ...

  8. lsb隐写

    所使用工具:wbs43open-win32 案例文件:https://files.cnblogs.com/files/xishaonian/lsb.rar 直接图片加载进去next即可. 使用工具处理 ...

  9. 基于Zepto移动端下拉加载(刷新),上拉加载插件开发

    写在前面:本人水平有限,有什么分析不到位的还请各路大神指出,谢谢. 这次要写的东西是类似于<今日头条>的效果,下拉加载上啦加载,这次做的效果是简单的模拟,没有多少内容,下面是今日头条的移动 ...

  10. 基于requirejs+bluebird,50行代码实现轻巧实用的前端CMD加载器

    首先是github地址,可以用git克隆命令也可以直接在git页面下载 https://github.com/kazetotori/js-requireAsync 下载下来后目录结构是这样的 -pac ...

随机推荐

  1. Qt音视频开发22-通用GPU显示

    一.前言 采用GPU来绘制实时视频一直以来都是个难点,如果是安防行业的做视频监控开发这块的人员,这个坎必须迈过去,本人一直从事的是安防行业的电子围栏这个相当小众的细分市场的开发,视频监控这块仅仅是周边 ...

  2. 命名空间“System.Web.UI.Design”中不存在类型或命名空间名称“ControlDesigner”

    命名空间"System.Web.UI.Design"中不存在类型或命名空间名称"ControlDesigner" 命名空间"System.Web.UI ...

  3. 基于斜率-截距式参数方程的直线Hough变换

  4. 关于经纬度坐标与utm坐标之间的相互转换api

    /* * Author: Sami Salkosuo, sami.salkosuo@fi.ibm.com * * (c) Copyright IBM Corp. 2007 */ package com ...

  5. 夜莺监控支持 ES 日志告警了

    夜莺项目( https://github.com/ccfos/nightingale )发布了 v8.0.0-beta.3 版本,这个版本主要是支持了 ES 日志告警,下面给大家介绍一下. 新版本下载 ...

  6. IoC究竟shift什么?——IoC的基础分析

    IoC全称Inversion of Control,直译为控制反转.这是一种设计理念,并非技术. 在明白控制反转之前,应该知道"反转"反的是什么. 被反转的正转 我们从生活中的做饭 ...

  7. IDEA配置Maven(详细版)

    https://blog.csdn.net/qq_42057154/article/details/106114515 IDEA配置MavenIDEA创建Maven工程第一节 IDEA集成Maven插 ...

  8. 如何快速的开发一个完整的iOS直播app(播放篇)

    作者:袁峥链接:https://www.jianshu.com/p/7b2f1df74420来源:简书著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 开发一款直播app,集成ij ...

  9. 日志数据采集-Flume

    1. 前言 在一个完整的离线大数据处理系统中,除了hdfs+mapreduce+hive组成分析系统的核心之外,还需要数据采集.结果数据导出.任务调度等不可或缺的辅助系统,而这些辅助工具在hadoop ...

  10. 密码学报如何正确Latex投稿?

    记录一下<密码学报>投稿遇到的坑,要不研究一下,投稿都不会投!(死在第一步) 模版地址 http://www.jcr.cacrnet.org.cn/CN/column/column13.s ...