Skip to content

Commit

Permalink
[GH-563] Implement [email protected] extension and keystroke obfuscati…
Browse files Browse the repository at this point in the history
…on (wip)
  • Loading branch information
gnodet committed Jul 26, 2024
1 parent 4b30ab0 commit 10ea704
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public final class SshConstants {
public static final byte SSH_MSG_CHANNEL_SUCCESS = 99;
public static final byte SSH_MSG_CHANNEL_FAILURE = 100;

public static final byte SSH_MSG_PING = (byte) 192;
public static final byte SSH_MSG_PONG = (byte) 193;

//
// Disconnect error codes
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.apache.sshd.common.kex.extension.parser.DelayCompression;
import org.apache.sshd.common.kex.extension.parser.Elevation;
import org.apache.sshd.common.kex.extension.parser.NoFlowControl;
import org.apache.sshd.common.kex.extension.parser.PingPong;
import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
Expand Down Expand Up @@ -84,7 +85,8 @@ public final class KexExtensions {
ServerSignatureAlgorithms.INSTANCE,
NoFlowControl.INSTANCE,
Elevation.INSTANCE,
DelayCompression.INSTANCE)
DelayCompression.INSTANCE,
PingPong.INSTANCE)
.collect(Collectors.toMap(
NamedResource::getName, Function.identity(),
MapEntryUtils.throwingMerger(), () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.sshd.common.kex.extension.parser;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

import org.apache.sshd.common.util.buffer.Buffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
* @see <A HREF="https://raw.githubusercontent.com/openssh/openssh-portable/master/PROTOCOL">OpenSSH PROTOCOL</A>
*/
public class PingPong extends AbstractKexExtensionParser<Integer> {
public static final String NAME = "[email protected]";

public static final PingPong INSTANCE = new PingPong();

private static final Logger LOG = LoggerFactory.getLogger(PingPong.class);

public PingPong() {
super(NAME);
}

@Override
public Integer parseExtension(Buffer buffer) throws IOException {
return parseExtension(buffer.array(), buffer.rpos(), buffer.available());
}

@Override
public Integer parseExtension(byte[] data, int off, int len) throws IOException {
if (len <= 0) {
if (LOG.isDebugEnabled()) {
LOG.debug("Inconsistent KEX extension {} received; no data (len={})", NAME, len);
}
return null;
}
String value = new String(data, off, len, StandardCharsets.UTF_8);
try {
Integer result = Integer.valueOf(Integer.parseUnsignedInt(value));
LOG.info("Server announced support for {} version {}", NAME, result);
return result;
} catch (NumberFormatException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Cannot parse KEX extension {} version {}", NAME, value);
}
}
return null;
}

@Override
protected void encode(Integer version, Buffer buffer) throws IOException {
buffer.putString(version.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,34 @@
*/
package org.apache.sshd.client.channel;

import java.io.EOFException;
import java.io.IOException;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.channel.PtyChannelConfiguration;
import org.apache.sshd.common.channel.PtyChannelConfigurationHolder;
import org.apache.sshd.common.channel.PtyChannelConfigurationMutator;
import org.apache.sshd.common.channel.PtyMode;
import org.apache.sshd.common.io.AbstractIoWriteFuture;
import org.apache.sshd.common.io.IoWriteFuture;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.apache.sshd.core.CoreModuleProperties;

import static org.apache.sshd.common.SshConstants.SSH_MSG_PING;
import static org.apache.sshd.core.CoreModuleProperties.OBFUSCATE_KEYSTROKE_TIMING;

/**
* <P>
* Serves as the base channel session for executing remote commands - including a full shell. <B>Note:</B> all the
Expand Down Expand Up @@ -80,9 +92,12 @@
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
*/
public class PtyCapableChannelSession extends ChannelSession implements PtyChannelConfigurationMutator {
private static final String PING_MESSAGE = "PING!";
private boolean agentForwarding;
private boolean usePty;
private int obfuscate;
private final PtyChannelConfiguration config;
private final AtomicReference<ScheduledFuture<?>> chaffFuture = new AtomicReference<>();

public PtyCapableChannelSession(boolean usePty, PtyChannelConfigurationHolder configHolder, Map<String, ?> env) {
this.usePty = usePty;
Expand Down Expand Up @@ -267,8 +282,62 @@ protected void doOpenPty() throws IOException {
modes.putByte(PtyMode.TTY_OP_END);
buffer.putBytes(modes.getCompactData());
writePacket(buffer);

String obf
= OBFUSCATE_KEYSTROKE_TIMING.get(getSession()).orElse(Boolean.FALSE.toString()).toLowerCase(Locale.ENGLISH);
if (obf.equals("yes") || obf.equals("true")) {
obfuscate = 20;
} else if (obf.equals("no") || obf.equals("false")) {
obfuscate = 0;
} else if (obf.matches("interval:[0-9]{1,5}")) {
obfuscate = Integer.parseInt(obf.substring("interval:".length()));
} else {
log.warn("doOpenPty({}) unrecognized value {} for property {}", this, obf,
OBFUSCATE_KEYSTROKE_TIMING.getName());
}
}

sendEnvVariables(session);
}

@Override
public IoWriteFuture writePacket(Buffer buffer) throws IOException {
if (obfuscate > 0 && buffer.available() < 256) {
log.info("Sending: ");
if (mayWrite()) {
Session s = getSession();
return s.writePacket(buffer);
}
if (log.isDebugEnabled()) {
log.debug("writePacket({}) Discarding output packet because channel state={}", this, state);
}
return AbstractIoWriteFuture.fulfilled(toString(), new EOFException("Channel is being closed"));
} else {
return super.writePacket(buffer);
}
}

protected void scheduleChaff() {
FactoryManager manager = getSession().getFactoryManager();
ScheduledExecutorService service = manager.getScheduledExecutorService();
long delay = 1024 + manager.getRandomFactory().get().random(2048);
ScheduledFuture<?> future = service.schedule(this::sendChaff, delay, TimeUnit.MILLISECONDS);
future = this.chaffFuture.getAndSet(future);
if (future != null) {
future.cancel(false);
}
}

protected void sendChaff() {
try {
Buffer buf = getSession().createBuffer(SSH_MSG_PING, PING_MESSAGE.length() + Integer.SIZE);
buf.putString(PING_MESSAGE);
getSession().writePacket(buf);
} catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug("Error sending chaff message", e);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.apache.sshd.common.AttributeRepository.AttributeKey;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.kex.extension.parser.HostBoundPubkeyAuthentication;
import org.apache.sshd.common.kex.extension.parser.PingPong;
import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.signature.Signature;
Expand Down Expand Up @@ -58,6 +59,11 @@ public class DefaultClientKexExtensionHandler extends AbstractLoggingBean implem
*/
public static final AttributeKey<Integer> HOSTBOUND_AUTHENTICATION = new AttributeKey<>();

/**
* Session {@link AttributeKey} storing the version if the server supports ping-pong requests.
*/
public static final AttributeKey<Integer> PING_PONG = new AttributeKey<>();

public DefaultClientKexExtensionHandler() {
super();
}
Expand Down Expand Up @@ -88,6 +94,21 @@ public boolean handleKexExtensionRequest(
} else {
session.setAttribute(HOSTBOUND_AUTHENTICATION, version);
}
} else if (PingPong.NAME.equals(name)) {
Integer version = PingPong.INSTANCE.parseExtension(data);
if (version == null) {
if (log.isDebugEnabled()) {
log.debug("handleKexExtensionRequest({}) : ignoring unknown {} extension", session,
PingPong.NAME);
}
} else if (version != 0) {
if (log.isDebugEnabled()) {
log.debug("handleKexExtensionRequest({}) : ignoring unknown {} version {}", session,
PingPong.NAME, version);
}
} else {
session.setAttribute(PING_PONG, version);
}
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import org.apache.sshd.common.AttributeRepository.AttributeKey;
import org.apache.sshd.common.kex.KexProposalOption;
import org.apache.sshd.common.kex.extension.parser.PingPong;
import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.util.GenericUtils;
Expand Down Expand Up @@ -157,5 +158,7 @@ public void collectExtensions(Session session, KexPhase phase, BiConsumer<String
ServerSignatureAlgorithms.NAME);
}
}
// [email protected]
marshaller.accept(PingPong.NAME, "0");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
import org.apache.sshd.server.x11.DefaultX11ForwardSupport;
import org.apache.sshd.server.x11.X11ForwardSupport;

import static org.apache.sshd.common.SshConstants.SSH_MSG_PONG;

/**
* Base implementation of ConnectionService.
*
Expand Down Expand Up @@ -492,6 +494,12 @@ public void process(int cmd, Buffer buffer) throws Exception {
case SshConstants.SSH_MSG_REQUEST_FAILURE:
requestFailure(buffer);
break;
case SshConstants.SSH_MSG_PING:
ping(buffer);
break;
case SshConstants.SSH_MSG_PONG:
pong(buffer);
break;
default: {
/*
* According to https://tools.ietf.org/html/rfc4253#section-11.4
Expand Down Expand Up @@ -922,6 +930,24 @@ protected void requestFailure(Buffer buffer) throws Exception {
s.requestFailure(buffer);
}

public void ping(Buffer buffer) throws Exception {
String req = buffer.getString();
if (log.isDebugEnabled()) {
log.debug("ping({}) Received SSH_MSG_PING len {}", this, req.length());
}
AbstractSession session = getSession();
Buffer rsp = session.createBuffer(SSH_MSG_PONG, req.length() + Integer.BYTES);
rsp.putString(req);
session.writePacket(rsp);
}

public void pong(Buffer buffer) throws Exception {
String req = buffer.getString();
if (log.isDebugEnabled()) {
log.debug("ping({}) Received SSH_MSG_PONG len {}", this, req.length());
}
}

@Override
public String toString() {
return getClass().getSimpleName() + "[" + getSession() + "]";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,13 @@ public final class CoreModuleProperties {
}
});

/**
* Obfuscate keystroke timing. Values can be {@code yes}, {@code true}, {@code no}, {@code false},
* {@code interval:[int-value]} to specify the keystroke default interval in milliseconds.
*/
public static final Property<String> OBFUSCATE_KEYSTROKE_TIMING
= Property.string("obfuscate-keystroke-timing", Boolean.FALSE.toString());

private CoreModuleProperties() {
throw new UnsupportedOperationException("No instance");
}
Expand Down

0 comments on commit 10ea704

Please sign in to comment.