Skip to content

Commit 6afdcfa

Browse files
committed
Added pre-commit, license, fixed pep8 violations
1 parent f2221b0 commit 6afdcfa

14 files changed

+124
-38
lines changed

.pre-commit-config.yaml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
repos:
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v3.4.0
4+
hooks:
5+
- id: check-yaml
6+
- id: end-of-file-fixer
7+
exclude: '.*.bin|requirements.txt'
8+
- id: trailing-whitespace
9+
exclude: '.*.bin'
10+
11+
- repo: https://github.com/pycqa/isort
12+
rev: 5.7.0
13+
hooks:
14+
- id: isort
15+
16+
- repo: https://github.com/psf/black
17+
rev: 20.8b1
18+
hooks:
19+
- id: black
20+
language_version: python3
21+
22+
- repo: https://gitlab.com/pycqa/flake8
23+
rev: 3.8.4
24+
hooks:
25+
- id: flake8

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Christopher Sacca
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+25-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,25 @@
1-
# Reverse engineering of the Horizon 7.4 AT (-02) Treadmill
1+
# Reverse Engineering of the Horizon 7.4 AT (-02) Treadmill
2+
3+
## Analyzed software
4+
5+
- Horizon 7.4 AT Treadmill Firmware: [7.4AT-02 (TM746B or TM499B) Treadmill V2.001](https://cdn.horizonfitness.rocks/content/5321/Horizon-7.4.zip)
6+
- `7.4AT-02+V2.001.EFM.bin`
7+
- SHA-256: `f7e6f57a15f69bdf68c484cef338a42265723f788b140950ee169e0a3761e8c3`
8+
- Android App: "AFG Pro Fitness" (com.xtremeprog.shell.treadmillv2)
9+
- `AFG Pro Fitness_v1.5.8_apkpure.com.apk`
10+
- SHA-256: `e296302f6766eb169b105b2406d0870124f015b2f2b55fb115409ffa480687ca`
11+
12+
## Reverse engineering artifacts
13+
14+
- IDA Pro 7.5 database: `ida/7.4AT-02+V2.001.EFM.bin.idb`
15+
- JEB 3.28 project: `jeb/AFG Pro Fitness_v1.5.8_apkpure.com.apk.jdb2`
16+
17+
## Instantiating a virtual environment
18+
19+
python3 -m venv ./env
20+
source ./env/bin/activate
21+
pip3 install -r requirements.txt
22+
23+
## Analyzing a Bluetooth HCI snoop log
24+
25+
./parse_btsnoop.py btsnoop_hci.log

horizon/__init__.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# flake8: noqa
2-
from .state import State
32
from .consts import HEADER_LENGTH
4-
from .workout_data import WorkoutData
5-
from .user_info import UserInfo
6-
from .message import Message
73
from .device_info import DeviceInfo
8-
from .set_speed import SetSpeed
4+
from .message import Message
95
from .set_incline import SetIncline
6+
from .set_speed import SetSpeed
7+
from .state import State
8+
from .user_info import UserInfo
9+
from .workout_data import WorkoutData
1010
from .workout_summary import WorkoutSummary

horizon/device_info.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def __init__(self, msg: bytes) -> None:
1919
def format_body(self) -> str:
2020
return (
2121
"Device Info:\n"
22-
f" Machine Type: {self.machineType:>3d} Model Type: {self.modelType:>3d}\n"
23-
f" Security Switch: {self.securitySwitch:>3d} Run Status: {self.runStatus:>3d}"
22+
f" Machine Type: {self.machineType:>3d}"
23+
f" Model Type: {self.modelType:>3d}\n"
24+
f" Security Switch: {self.securitySwitch:>3d}"
25+
f" Run Status: {self.runStatus:>3d}"
2426
)

horizon/message.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ def __init__(self, msg: bytes) -> None:
2424
def format_header(self) -> str:
2525
return (
2626
"Header\n"
27-
f" signature: 0x{self.signature.hex()} sequence number: 0x{self.seq_num:04x}\n"
27+
f" signature: 0x{self.signature.hex()}"
28+
f" sequence number: 0x{self.seq_num:04x}\n"
2829
f" msg[4]: 0x{self.msg4:02x}\n"
2930
f" msg[5]: 0x{self.msg5:02x}\n"
3031
f" length: 0x{self.length:04x}\n"
@@ -49,7 +50,9 @@ def format_raw_msg(self) -> str:
4950
return string
5051

5152
def __str__(self) -> str:
52-
return self.format_type() + "\n" + self.format_header() + "\n" + self.format_body()
53+
return (
54+
self.format_type() + "\n" + self.format_header() + "\n" + self.format_body()
55+
)
5356

5457
def parse_msg_type(self) -> None:
5558
msg_type = ""
@@ -165,8 +168,10 @@ def parse_msg_type(self) -> None:
165168

166169
if msg_type == "":
167170
msg_type = (
168-
f"Unknown: msg[4]: {self.msg[4:5].hex()} msg[5]: {self.msg[5:6].hex()} "
169-
f"msg[10]: {self.msg[10:11].hex()} msg[11]: {self.msg[11:12].hex()}"
171+
f"Unknown: msg[4]: {self.msg[4:5].hex()}"
172+
f" msg[5]: {self.msg[5:6].hex()}"
173+
f" msg[10]: {self.msg[10:11].hex()}"
174+
f" msg[11]: {self.msg[11:12].hex()}"
170175
)
171176

172177
self.type = msg_type

horizon/parsers.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from .workout_data import WorkoutData
2-
from .user_info import UserInfo
3-
from .header import Header
41
from .device_info import DeviceInfo
2+
from .header import Header
53
from .set_speed import SetSpeed
4+
from .user_info import UserInfo
5+
from .workout_data import WorkoutData
66

77

88
def parse_header(msg: bytes) -> Header:

horizon/set_incline.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,7 @@ def __init__(self, msg: bytes) -> None:
1111
self.msg12 = msg[12]
1212

1313
def format_body(self) -> str:
14-
return f"Set Incline:\n Incline: {self.incline/10.0:.1f}\n msg[12]: {self.msg12:02x}"
14+
return (
15+
f"Set Incline:\n Incline: {self.incline/10.0:.1f}\n"
16+
f" msg[12]: {self.msg12:02x}"
17+
)

horizon/user_info.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,16 @@ def format_body(self) -> str:
5555
f" User Slot: {self.userSlot:d}\n"
5656
f" User Name: {self.userName!r}\n"
5757
f" User Weight: {self.userWeight:d}\n"
58-
f" User Birth Year: {self.userBirthYear:d} User Birth Month: {self.userBirthMonth:d} "
58+
f" User Birth Year: {self.userBirthYear:d}"
59+
f" User Birth Month: {self.userBirthMonth:d} "
5960
f"User Birth Day: {self.userBirthDay:d}\n"
6061
f" Units: {self.units:d}\n"
6162
f" Custom Program CRC: {self.customProgramCRC:04x} Custom Heartrate CRC: "
6263
f"{self.customHeartrateCRC:04x}\n"
6364
f" My First 5k Week: {self.myFirst5kWeek:d} My First 5k Workout: "
6465
f"{self.myFirst5kWorkout:d}\n"
65-
f" My First 5k Walk Speed: {self.myFirst5kWalkSpeed:d} My First 5k Jog Speed: "
66-
f"{self.myFirst5kJogSpeed:d}\n"
66+
f" My First 5k Walk Speed: {self.myFirst5kWalkSpeed:d}"
67+
f" My First 5k Jog Speed: {self.myFirst5kJogSpeed:d}\n"
6768
f" My First 5k Reset Counter: {self.myFirst5kResetCounter:d}\n"
6869
f" Map My Fitness Token ID: {self.MMFTokenId!r}\n"
6970
f" My Fitness Pal Token ID: {self.MFPTokenId!r}\n"

horizon/workout_summary.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from .message import Message
21
from typing import List
32

3+
from .message import Message
4+
45

56
class WorkoutSummary(Message):
67
userName: str = ""
@@ -45,7 +46,7 @@ def parse_workout_list(self, msg: bytes, workoutCounter: int) -> List:
4546
def format_body(self) -> str:
4647
s = (
4748
"Workout Summary:\n"
48-
f" User Name: \'{self.userName:s}\' User Id: {self.userId:d}\n"
49+
f" User Name: '{self.userName:s}' User Id: {self.userId:d}\n"
4950
f" MMFTokenId: {self.MMFTokenId:s}\n"
5051
f" MFPTokenId: {self.MFPTokenId:s}\n"
5152
f" MFPUserId: {self.MFPUserId:s}\n"
@@ -95,7 +96,8 @@ def __init__(self, msg: bytes, start: int):
9596

9697
def __str__(self) -> str:
9798
return (
98-
f" Time: {self.time:d} Calories: {self.calories:d} Distance: {self.distance:d}\n"
99+
f" Time: {self.time:d} Calories: {self.calories:d}"
100+
f" Distance: {self.distance:d}\n"
99101
f" Max Speed: {self.maxSpeed:d} Average Speed: {self.averageSpeed:d}\n"
100102
f" Max HR: {self.maxHR:d} Average HR: {self.averageHR:d}\n"
101103
f" mem10: {self.mem10:d} Units: {self.units:d}\n"

message_type_notes.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@ msg[12] (0,1)
5858

5959
0307 setResistance
6060
msg[10] resistance
61-
msg[11] (0,1)
61+
msg[11] (0,1)

parse_btsnoop.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ def main():
8989
and int(pkt.bthci_evt.le_meta_subevent, 16) == 0x02
9090
and int(pkt.bthci_evt.le_advts_event_type, 16) == 0x04
9191
and "btcommon_eir_ad_entry_device_name" in pkt.bthci_evt.field_names
92-
and pkt.bthci_evt.btcommon_eir_ad_entry_device_name.lower().startswith("horizon")
92+
and pkt.bthci_evt.btcommon_eir_ad_entry_device_name.lower().startswith(
93+
"horizon"
94+
)
9395
):
9496
# pkt.pretty_print()
9597
mac = pkt.bthci_evt.bd_addr

requirements.txt

364 Bytes
Binary file not shown.

uc/uc_scratch.py

+15-14
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1+
from capstone import CS_ARCH_ARM, CS_MODE_THUMB, Cs
12
from unicorn import (
23
UC_ARCH_ARM,
3-
UC_ERR_MAP,
4-
UC_HOOK_BLOCK,
54
UC_HOOK_CODE,
65
UC_HOOK_MEM_FETCH_UNMAPPED,
7-
UC_HOOK_MEM_READ,
8-
UC_HOOK_MEM_READ_AFTER,
96
UC_HOOK_MEM_READ_UNMAPPED,
107
UC_HOOK_MEM_WRITE_UNMAPPED,
118
UC_MODE_THUMB,
129
Uc,
13-
UcError,
1410
)
1511
from unicorn.arm_const import (
1612
UC_ARM_REG_PC,
@@ -22,9 +18,6 @@
2218
UC_ARM_REG_SP,
2319
)
2420

25-
from capstone import Cs, CS_ARCH_ARM, CS_MODE_THUMB
26-
from unicorn.unicorn_const import UC_HOOK_MEM_WRITE
27-
2821
cs = Cs(CS_ARCH_ARM, CS_MODE_THUMB)
2922

3023
PAGE_SIZE = 0x1000
@@ -46,15 +39,17 @@ def hook_mem_read(uc, access, address, size, value, user_data):
4639
def hook_mem_read_after(uc, access, address, size, value, user_data):
4740
if address < 0x38000:
4841
print(
49-
">>> Memory READ at 0x%x, data size = %u, data value = 0x%x" % (address, size, value)
42+
">>> Memory READ at 0x%x, data size = %u, data value = 0x%x"
43+
% (address, size, value)
5044
)
5145
return True
5246

5347

5448
def hook_mem_write(uc, access, address, size, value, user_data):
5549
if address < 0xF0000000:
5650
print(
57-
">>> Memory WRITE at 0x%x, data size = %u, data value = 0x%x" % (address, size, value)
51+
">>> Memory WRITE at 0x%x, data size = %u, data value = 0x%x"
52+
% (address, size, value)
5853
)
5954
return True
6055

@@ -101,15 +96,18 @@ def hook_instr(mu: Uc, address, size, user_data):
10196
# input()
10297
pass
10398
if address >= 0x0369E0 and address <= 0x36A00:
104-
print(">>> Tracing instruction at 0x%X, instruction size = 0x%X" % (address, size))
99+
print(
100+
">>> Tracing instruction at 0x%X, instruction size = 0x%X" % (address, size)
101+
)
105102
R0 = mu.reg_read(UC_ARM_REG_R0)
106103
R1 = mu.reg_read(UC_ARM_REG_R1)
107104
R2 = mu.reg_read(UC_ARM_REG_R2)
108105
R3 = mu.reg_read(UC_ARM_REG_R3)
109106
R4 = mu.reg_read(UC_ARM_REG_R4)
110107
PC = mu.reg_read(UC_ARM_REG_PC)
111108
print(
112-
f"R0: {R0:08X} R1: {R1:08X} R2: {R2:08X} R3: {R3:08X} R4: {R4:08X} PC: {PC:08X}"
109+
f"R0: {R0:08X} R1: {R1:08X} R2: {R2:08X} "
110+
f"R3: {R3:08X} R4: {R4:08X} PC: {PC:08X}"
113111
)
114112
mem = mu.mem_read(address, size)
115113
for i in cs.disasm(mem, address):
@@ -120,15 +118,18 @@ def hook_instr(mu: Uc, address, size, user_data):
120118
print()
121119
# dump_hex_buf(mu, R0, R1)
122120

123-
mu.last_instr = f">>> Tracing instruction at 0x{address:08X}, instruction size = 0x{size:X}\n"
121+
mu.last_instr = (
122+
f">>> Tracing instruction at 0x{address:08X}, instruction size = 0x{size:X}\n"
123+
)
124124
R0 = mu.reg_read(UC_ARM_REG_R0)
125125
R1 = mu.reg_read(UC_ARM_REG_R1)
126126
R2 = mu.reg_read(UC_ARM_REG_R2)
127127
R3 = mu.reg_read(UC_ARM_REG_R3)
128128
R4 = mu.reg_read(UC_ARM_REG_R4)
129129
PC = mu.reg_read(UC_ARM_REG_PC)
130130
mu.last_instr += (
131-
f"R0: {R0:08X} R1: {R1:08X} R2: {R2:08X} R3: {R3:08X} R4: {R4:08X} PC: {PC:08X}\n"
131+
f"R0: {R0:08X} R1: {R1:08X} R2: {R2:08X} "
132+
f"R3: {R3:08X} R4: {R4:08X} PC: {PC:08X}\n"
132133
)
133134

134135
# branch = False

0 commit comments

Comments
 (0)