Skip to content

Commit 10ea704

Browse files
committed
[apacheGH-563] Implement [email protected] extension and keystroke obfuscation (wip)
1 parent 4b30ab0 commit 10ea704

File tree

8 files changed

+206
-1
lines changed

8 files changed

+206
-1
lines changed

Diff for: sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java

+3
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ public final class SshConstants {
9595
public static final byte SSH_MSG_CHANNEL_SUCCESS = 99;
9696
public static final byte SSH_MSG_CHANNEL_FAILURE = 100;
9797

98+
public static final byte SSH_MSG_PING = (byte) 192;
99+
public static final byte SSH_MSG_PONG = (byte) 193;
100+
98101
//
99102
// Disconnect error codes
100103
//

Diff for: sshd-common/src/main/java/org/apache/sshd/common/kex/extension/KexExtensions.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.sshd.common.kex.extension.parser.DelayCompression;
4141
import org.apache.sshd.common.kex.extension.parser.Elevation;
4242
import org.apache.sshd.common.kex.extension.parser.NoFlowControl;
43+
import org.apache.sshd.common.kex.extension.parser.PingPong;
4344
import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
4445
import org.apache.sshd.common.util.GenericUtils;
4546
import org.apache.sshd.common.util.MapEntryUtils;
@@ -84,7 +85,8 @@ public final class KexExtensions {
8485
ServerSignatureAlgorithms.INSTANCE,
8586
NoFlowControl.INSTANCE,
8687
Elevation.INSTANCE,
87-
DelayCompression.INSTANCE)
88+
DelayCompression.INSTANCE,
89+
PingPong.INSTANCE)
8890
.collect(Collectors.toMap(
8991
NamedResource::getName, Function.identity(),
9092
MapEntryUtils.throwingMerger(), () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.sshd.common.kex.extension.parser;
21+
22+
import java.io.IOException;
23+
import java.nio.charset.StandardCharsets;
24+
25+
import org.apache.sshd.common.util.buffer.Buffer;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
29+
/**
30+
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
31+
* @see <A HREF="https://raw.githubusercontent.com/openssh/openssh-portable/master/PROTOCOL">OpenSSH PROTOCOL</A>
32+
*/
33+
public class PingPong extends AbstractKexExtensionParser<Integer> {
34+
public static final String NAME = "[email protected]";
35+
36+
public static final PingPong INSTANCE = new PingPong();
37+
38+
private static final Logger LOG = LoggerFactory.getLogger(PingPong.class);
39+
40+
public PingPong() {
41+
super(NAME);
42+
}
43+
44+
@Override
45+
public Integer parseExtension(Buffer buffer) throws IOException {
46+
return parseExtension(buffer.array(), buffer.rpos(), buffer.available());
47+
}
48+
49+
@Override
50+
public Integer parseExtension(byte[] data, int off, int len) throws IOException {
51+
if (len <= 0) {
52+
if (LOG.isDebugEnabled()) {
53+
LOG.debug("Inconsistent KEX extension {} received; no data (len={})", NAME, len);
54+
}
55+
return null;
56+
}
57+
String value = new String(data, off, len, StandardCharsets.UTF_8);
58+
try {
59+
Integer result = Integer.valueOf(Integer.parseUnsignedInt(value));
60+
LOG.info("Server announced support for {} version {}", NAME, result);
61+
return result;
62+
} catch (NumberFormatException e) {
63+
if (LOG.isDebugEnabled()) {
64+
LOG.debug("Cannot parse KEX extension {} version {}", NAME, value);
65+
}
66+
}
67+
return null;
68+
}
69+
70+
@Override
71+
protected void encode(Integer version, Buffer buffer) throws IOException {
72+
buffer.putString(version.toString());
73+
}
74+
}

Diff for: sshd-core/src/main/java/org/apache/sshd/client/channel/PtyCapableChannelSession.java

+69
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,34 @@
1818
*/
1919
package org.apache.sshd.client.channel;
2020

21+
import java.io.EOFException;
2122
import java.io.IOException;
2223
import java.util.Collections;
24+
import java.util.Locale;
2325
import java.util.Map;
26+
import java.util.concurrent.ScheduledExecutorService;
27+
import java.util.concurrent.ScheduledFuture;
28+
import java.util.concurrent.TimeUnit;
29+
import java.util.concurrent.atomic.AtomicReference;
2430

31+
import org.apache.sshd.common.FactoryManager;
2532
import org.apache.sshd.common.SshConstants;
2633
import org.apache.sshd.common.channel.PtyChannelConfiguration;
2734
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder;
2835
import org.apache.sshd.common.channel.PtyChannelConfigurationMutator;
2936
import org.apache.sshd.common.channel.PtyMode;
37+
import org.apache.sshd.common.io.AbstractIoWriteFuture;
38+
import org.apache.sshd.common.io.IoWriteFuture;
3039
import org.apache.sshd.common.session.Session;
3140
import org.apache.sshd.common.util.GenericUtils;
3241
import org.apache.sshd.common.util.MapEntryUtils;
3342
import org.apache.sshd.common.util.buffer.Buffer;
3443
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
3544
import org.apache.sshd.core.CoreModuleProperties;
3645

46+
import static org.apache.sshd.common.SshConstants.SSH_MSG_PING;
47+
import static org.apache.sshd.core.CoreModuleProperties.OBFUSCATE_KEYSTROKE_TIMING;
48+
3749
/**
3850
* <P>
3951
* Serves as the base channel session for executing remote commands - including a full shell. <B>Note:</B> all the
@@ -80,9 +92,12 @@
8092
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
8193
*/
8294
public class PtyCapableChannelSession extends ChannelSession implements PtyChannelConfigurationMutator {
95+
private static final String PING_MESSAGE = "PING!";
8396
private boolean agentForwarding;
8497
private boolean usePty;
98+
private int obfuscate;
8599
private final PtyChannelConfiguration config;
100+
private final AtomicReference<ScheduledFuture<?>> chaffFuture = new AtomicReference<>();
86101

87102
public PtyCapableChannelSession(boolean usePty, PtyChannelConfigurationHolder configHolder, Map<String, ?> env) {
88103
this.usePty = usePty;
@@ -267,8 +282,62 @@ protected void doOpenPty() throws IOException {
267282
modes.putByte(PtyMode.TTY_OP_END);
268283
buffer.putBytes(modes.getCompactData());
269284
writePacket(buffer);
285+
286+
String obf
287+
= OBFUSCATE_KEYSTROKE_TIMING.get(getSession()).orElse(Boolean.FALSE.toString()).toLowerCase(Locale.ENGLISH);
288+
if (obf.equals("yes") || obf.equals("true")) {
289+
obfuscate = 20;
290+
} else if (obf.equals("no") || obf.equals("false")) {
291+
obfuscate = 0;
292+
} else if (obf.matches("interval:[0-9]{1,5}")) {
293+
obfuscate = Integer.parseInt(obf.substring("interval:".length()));
294+
} else {
295+
log.warn("doOpenPty({}) unrecognized value {} for property {}", this, obf,
296+
OBFUSCATE_KEYSTROKE_TIMING.getName());
297+
}
270298
}
271299

272300
sendEnvVariables(session);
273301
}
302+
303+
@Override
304+
public IoWriteFuture writePacket(Buffer buffer) throws IOException {
305+
if (obfuscate > 0 && buffer.available() < 256) {
306+
log.info("Sending: ");
307+
if (mayWrite()) {
308+
Session s = getSession();
309+
return s.writePacket(buffer);
310+
}
311+
if (log.isDebugEnabled()) {
312+
log.debug("writePacket({}) Discarding output packet because channel state={}", this, state);
313+
}
314+
return AbstractIoWriteFuture.fulfilled(toString(), new EOFException("Channel is being closed"));
315+
} else {
316+
return super.writePacket(buffer);
317+
}
318+
}
319+
320+
protected void scheduleChaff() {
321+
FactoryManager manager = getSession().getFactoryManager();
322+
ScheduledExecutorService service = manager.getScheduledExecutorService();
323+
long delay = 1024 + manager.getRandomFactory().get().random(2048);
324+
ScheduledFuture<?> future = service.schedule(this::sendChaff, delay, TimeUnit.MILLISECONDS);
325+
future = this.chaffFuture.getAndSet(future);
326+
if (future != null) {
327+
future.cancel(false);
328+
}
329+
}
330+
331+
protected void sendChaff() {
332+
try {
333+
Buffer buf = getSession().createBuffer(SSH_MSG_PING, PING_MESSAGE.length() + Integer.SIZE);
334+
buf.putString(PING_MESSAGE);
335+
getSession().writePacket(buf);
336+
} catch (IOException e) {
337+
if (log.isDebugEnabled()) {
338+
log.debug("Error sending chaff message", e);
339+
}
340+
}
341+
}
342+
274343
}

Diff for: sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultClientKexExtensionHandler.java

+21
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.sshd.common.AttributeRepository.AttributeKey;
3131
import org.apache.sshd.common.NamedFactory;
3232
import org.apache.sshd.common.kex.extension.parser.HostBoundPubkeyAuthentication;
33+
import org.apache.sshd.common.kex.extension.parser.PingPong;
3334
import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
3435
import org.apache.sshd.common.session.Session;
3536
import org.apache.sshd.common.signature.Signature;
@@ -58,6 +59,11 @@ public class DefaultClientKexExtensionHandler extends AbstractLoggingBean implem
5859
*/
5960
public static final AttributeKey<Integer> HOSTBOUND_AUTHENTICATION = new AttributeKey<>();
6061

62+
/**
63+
* Session {@link AttributeKey} storing the version if the server supports ping-pong requests.
64+
*/
65+
public static final AttributeKey<Integer> PING_PONG = new AttributeKey<>();
66+
6167
public DefaultClientKexExtensionHandler() {
6268
super();
6369
}
@@ -88,6 +94,21 @@ public boolean handleKexExtensionRequest(
8894
} else {
8995
session.setAttribute(HOSTBOUND_AUTHENTICATION, version);
9096
}
97+
} else if (PingPong.NAME.equals(name)) {
98+
Integer version = PingPong.INSTANCE.parseExtension(data);
99+
if (version == null) {
100+
if (log.isDebugEnabled()) {
101+
log.debug("handleKexExtensionRequest({}) : ignoring unknown {} extension", session,
102+
PingPong.NAME);
103+
}
104+
} else if (version != 0) {
105+
if (log.isDebugEnabled()) {
106+
log.debug("handleKexExtensionRequest({}) : ignoring unknown {} version {}", session,
107+
PingPong.NAME, version);
108+
}
109+
} else {
110+
session.setAttribute(PING_PONG, version);
111+
}
91112
}
92113
return true;
93114
}

Diff for: sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultServerKexExtensionHandler.java

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import org.apache.sshd.common.AttributeRepository.AttributeKey;
2929
import org.apache.sshd.common.kex.KexProposalOption;
30+
import org.apache.sshd.common.kex.extension.parser.PingPong;
3031
import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
3132
import org.apache.sshd.common.session.Session;
3233
import org.apache.sshd.common.util.GenericUtils;
@@ -157,5 +158,7 @@ public void collectExtensions(Session session, KexPhase phase, BiConsumer<String
157158
ServerSignatureAlgorithms.NAME);
158159
}
159160
}
161+
162+
marshaller.accept(PingPong.NAME, "0");
160163
}
161164
}

Diff for: sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractConnectionService.java

+26
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@
7171
import org.apache.sshd.server.x11.DefaultX11ForwardSupport;
7272
import org.apache.sshd.server.x11.X11ForwardSupport;
7373

74+
import static org.apache.sshd.common.SshConstants.SSH_MSG_PONG;
75+
7476
/**
7577
* Base implementation of ConnectionService.
7678
*
@@ -492,6 +494,12 @@ public void process(int cmd, Buffer buffer) throws Exception {
492494
case SshConstants.SSH_MSG_REQUEST_FAILURE:
493495
requestFailure(buffer);
494496
break;
497+
case SshConstants.SSH_MSG_PING:
498+
ping(buffer);
499+
break;
500+
case SshConstants.SSH_MSG_PONG:
501+
pong(buffer);
502+
break;
495503
default: {
496504
/*
497505
* According to https://tools.ietf.org/html/rfc4253#section-11.4
@@ -922,6 +930,24 @@ protected void requestFailure(Buffer buffer) throws Exception {
922930
s.requestFailure(buffer);
923931
}
924932

933+
public void ping(Buffer buffer) throws Exception {
934+
String req = buffer.getString();
935+
if (log.isDebugEnabled()) {
936+
log.debug("ping({}) Received SSH_MSG_PING len {}", this, req.length());
937+
}
938+
AbstractSession session = getSession();
939+
Buffer rsp = session.createBuffer(SSH_MSG_PONG, req.length() + Integer.BYTES);
940+
rsp.putString(req);
941+
session.writePacket(rsp);
942+
}
943+
944+
public void pong(Buffer buffer) throws Exception {
945+
String req = buffer.getString();
946+
if (log.isDebugEnabled()) {
947+
log.debug("ping({}) Received SSH_MSG_PONG len {}", this, req.length());
948+
}
949+
}
950+
925951
@Override
926952
public String toString() {
927953
return getClass().getSimpleName() + "[" + getSession() + "]";

Diff for: sshd-core/src/main/java/org/apache/sshd/core/CoreModuleProperties.java

+7
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,13 @@ public final class CoreModuleProperties {
800800
}
801801
});
802802

803+
/**
804+
* Obfuscate keystroke timing. Values can be {@code yes}, {@code true}, {@code no}, {@code false},
805+
* {@code interval:[int-value]} to specify the keystroke default interval in milliseconds.
806+
*/
807+
public static final Property<String> OBFUSCATE_KEYSTROKE_TIMING
808+
= Property.string("obfuscate-keystroke-timing", Boolean.FALSE.toString());
809+
803810
private CoreModuleProperties() {
804811
throw new UnsupportedOperationException("No instance");
805812
}

0 commit comments

Comments
 (0)