Skip to content

Instantly share code, notes, and snippets.

@supechicken
Last active March 6, 2024 12:08
Show Gist options
  • Save supechicken/58d530210620d24eea327043b7cb9b3b to your computer and use it in GitHub Desktop.
Save supechicken/58d530210620d24eea327043b7cb9b3b to your computer and use it in GitHub Desktop.
A simple `su` client/daemon script for ChromeOS crosh shell
#!/usr/bin/env ruby
# CroshSU: "Fix" sudo in crosh by redirecting all sudo calls to VT-2 shell, inspired by root solutions on Android
#
# Usage: put this script into /usr/local/bin, run `crosh-su --daemon` in VT-2 and run
# some command with `crosh-su --client <command you want to run with root>` in crosh
#
require 'io/console'
require 'socket'
require 'pty'
require 'fileutils'
require 'json'
SOCKET_PATH = '/tmp/sudo-server'
def forward_io(srcIO, dstIO)
Thread.new do
until srcIO.closed?
begin
data = srcIO.read_nonblock(102400)
warn "[daemon] Got #{data.bytesize} bytes from #{srcIO}"
dstIO.write(data)
rescue IO::WaitReadable
begin
IO.select([srcIO])
rescue IOError
end
end
end
end
end
def send_event(sock, event, args = {}) = sock.puts({ event: event }.merge(args).to_json)
def daemon_mode(argv)
# create unix socket
@server = UNIXServer.new(SOCKET_PATH)
FileUtils.chmod(0o600, SOCKET_PATH)
Socket.accept_loop(@server) do |sock, _|
Thread.new do
# receive client's stdin/stdout/stderr io from client
client_stdin, client_stdout, client_stderr = [sock.recv_io, sock.recv_io, sock.recv_io]
client_request = JSON.parse(sock.gets, symbolize_names: true)
if client_stdout.isatty && client_stderr.isatty
# if client's stdout is a tty (not a pipe/file), create a pty for process
@pty_master, @pty_slave = PTY.open
# forward client input to pty + pty output to client
forward_io(client_stdin, @pty_master)
forward_io(@pty_master, client_stdout)
end
warn "[daemon] Spawn process: #{client_request[:cmd_argv]}"
pid = fork do
if client_stdout.isatty && client_stderr.isatty
# if client's stdout is a tty (not a pipe/file), attach to the pty opened by PTY.open
[$stdin, $stdout, $stderr].each {|io| io.reopen(@pty_slave) }
# set new process group
Process.setsid
# 0x540E: TIOCSCTTY
# set controlling terminal to the pty
@pty_master.ioctl(0x540E, 0)
else
# attach to stdin/stdout/stderr of client directly
$stdin.reopen(client_stdin)
$stdout.reopen(client_stdout)
$stderr.reopen(client_stderr)
end
Dir.chdir(client_request[:cwd])
ENV.merge!(client_request[:env].transform_keys(&:to_s))
exec('/usr/bin/sudo', *client_request[:cmd_argv])
end
# listen to client events
Thread.new do
until sock.closed?
event = JSON.parse(sock.gets, symbolize_names: true)
case event[:event]
when 'set_termsize' # when client tty resized
rows, cols = event[:newsize]
# 0x5414: TIOCSWINSZ
warn "[daemon] Resize terminal to #{rows} rows, #{cols} cols"
warn "[daemon] Sending TIOCSWINSZ loctl to PTY..."
@pty_master.ioctl(0x5414, [rows, cols, 0, 0].pack('S!*'))
end
end
end
# wait for process end and send the exit status back to client
Process.waitpid(pid)
send_event(sock, 'cmd_terminated', { cmd_exit_status: $?.exitstatus })
ensure
@pty_master.close if @pty_master
@pty_slave.close if @pty_slave
sock.close
end
end
ensure
@server.close
FileUtils.rm_f(SOCKET_PATH)
end
def client_mode(argv)
# connect to daemon
sock = UNIXSocket.open(SOCKET_PATH)
@tty_attr = `stty -g`.chomp
# disable terminal echo
system('stty', 'raw', '-echo')
# send stdin/stdout/stderr to daemon
sock.send_io($stdin)
sock.send_io($stdout)
sock.send_io($stderr)
# let daemon to take over stdin
$stdin.close
request = { cmd_argv: argv, env: ENV.to_h, cwd: Dir.pwd }
sock.puts(request.to_json)
# listen to terminal resize event
trap('WINCH') { send_event(sock, 'set_termsize', { newsize: IO.console.winsize }) }
Process.kill('WINCH', Process.pid)
# listen to client events
until sock.closed?
event = JSON.parse(sock.gets, symbolize_names: true)
case event[:event]
when 'cmd_terminated' # process exited
system('stty', @tty_attr) # restore tty attributes on program exit
warn "[client] Process exited with status #{event[:cmd_exit_status]}"
exit(event[:cmd_exit_status])
end
end
ensure
# restore tty attributes
system('stty', @tty_attr)
end
# resolve command arguments
case File.basename($0)
when 'crosh-su'
case ARGV[0]
when '-d', '--daemon'
daemon_mode(ARGV[1..-1])
when '-c', '--client'
client_mode(ARGV[1..-1])
when '-h', '--help'
warn <<~EOT
CroshSU multi-purpose script
Usage: crosh-su [mode]
crosh-su -h|--help
crosh-su -V|--version
Available modes:
--daemon: Run as daemon mode, listen incoming requests at #{SOCKET_PATH}
--client: Run as client mode, pass all given command arguments to daemon
EOT
when '-V', '--version'
warn 'CroshSU version 1.0'
else
warn <<~EOT
crosh-su: #{ARGV[0]}: unknown option
Run 'crosh-su --help' for usage.
EOT
end
when 'sudod'
daemon_mode(ARGV)
when 'sudo'
client_mode(ARGV)
end
@s1gnate-sync
Copy link

I've been there... I also made a prototype using dtach (kinda screen without features) and another prototype using dropbear ssh. But there is an even simpler method which works out of the box:

server: /usr/bin/vshd
client: exec /usr/bin/vsh --cid=2 to replace shell

Also, it's easy to automate server startup so you don't need to switch to VT-2 and start it manually:

start on started attestationd
stop on stop ui
respawn
exec /usr/bin/vshd

to /etc/init/local-vshd.conf

You can also automate process replacement by throwing client cmd to ~/.bash_profile , just make sure that you handle errors correctly since no matter what exec is gonna to replace bash and if vsh responded with error you would have hard time logging back!

@supechicken
Copy link
Author

supechicken commented Jan 13, 2024

I love your idea, especially on making use of vsh. However, this will not work:

If you run strace vshd, you can see that vshd uses VMADDR_CID_ANY as the vsock address while starting a vsock server. However, it only works in VM and have no effect in host:

# strace vshd
...snip...
socket(AF_VSOCK, SOCK_STREAM|SOCK_CLOEXEC, 0) = 4
bind(4, {sa_family=AF_VSOCK, svm_cid=VMADDR_CID_ANY, svm_port=0x2329, svm_flags=0}, 16) = 0
listen(4, 32)                           = 0

(to "fix" it, one way is to use the LD_PRELOAD hack to wrap the bind() syscall and replace VMADDR_CID_ANY with VMADDR_CID_HOST (UPDATE: just tried it but with no luck, it looks like vsock only works on vm->host or host->vm but not host->host))

For me, this error appeared when trying to connect it:

$ vsh --cid=2
ERROR vsh: [vsh.cc(445)] Failed to connect to vshd: No such device (19)

@s1gnate-sync
Copy link

It cannot be! VMADDR_CID_HOST (which is 2) is a well known CID representing HOST on GUEST side. In other words if GUEST decided to connect to HOST it would be using CID=2. In our case GUEST==HOST and I don't see any problem here. It's the same GUEST->HOST connection using 2.

And about "working only in VM" I really doubt it because how the hell HOST would understand the origin of the connection? It doesn't care if it's from VM or anything else.

@s1gnate-sync
Copy link

Can you try cid=1? It works for me as well, but I thought it's better to use 2, but who knows... It looks like cid=1 is used exactly for local connections.

Also I think running crostini can interfere since it's running vshd too... I stopped using it long time ago and completely forgot about it.

@supechicken
Copy link
Author

supechicken commented Jan 13, 2024

It cannot be! VMADDR_CID_HOST (which is 2) is a well known CID representing HOST on GUEST side.

The issue is there is no GUEST involved. (As you said, two sides are HOST) And VMADDR_CID_ANY only works on GUEST.

And about "working only in VM"

vsock is initially designed for communication between VM and host (VM <-> VM or VM <-> HOST), so I am not sure if it works on HOST<->HOST.

I have also tried VMADDR_CID_ANY (-1), VMADDR_CID_HOST (2) and VMADDR_CID_LOCAL (1) but with no luck.

I really doubt it because how the hell HOST would understand the origin of the connection? It doesn't care if it's from VM or anything else.

Of course it knows, each participant in vsock has its unique CID. Maybe the kernel just checks if CID == 2.

@supechicken
Copy link
Author

@s1gnate-sync Just seen this gist, which described another possible solution to this issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment