Lime SJ2.5 hacking

hahaha
Posts: 1
Joined: Fri Feb 07, 2020 10:21 am

Lime SJ2.5 hacking

Postby hahaha Fri Feb 07, 2020 10:29 am

With this, I was able to spoof the Lime receiver and turn abandoned scooters on until they updated this month. https://scootertalk.org/forum/viewtopic.php?f=56&t=5767

I've gotten pretty far trying to tease out the latest serial commands needed to turn them on. I wrote a Java program that interacts with them and is brute forcing codes now... If anyone has any interest or info, I'd love some help!
The new FW version reads out as SANTR_FW_2.4.26_20191119100000
An old working one I have here reads LIS01_FW_1.8.3_201907250912XX
import com.github.snksoft.crc.CRC;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Vector;

import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;

import com.fazecast.jSerialComm.SerialPort;;

class Code {
public String name;
public byte[] code;

public Code(String inName, byte[] inCode) {
name = inName;
code = inCode;
}

public String toString() {
return name;
}
}

public class LimeProbe extends JPanel implements Runnable, SerialResponder {
final static byte read = 0x11, write = 0x16;
final static byte light = 0x12, fw = 0x14, hw = 0x18, id = 0x20, reboot = 0x21, motor = 0x61, lock = 0x63,
timer = 0x71;
private static final Font font = new Font("Monospaced", Font.PLAIN, 12);

Thread tthis;
byte[] readBuffer = { 0 };
byte[] data = { 0 };
long lastDraw;
SerialPort comPort = null;
boolean initialized = false;
ProbeReadThread reader = null;
Vector<byte[]> responses;
Vector<byte[]> validResponses;
JComboBox<Code> comboBox;

public LimeProbe() throws InterruptedException {
Vector<Code> codes = new Vector<Code>();
codes.add(new Code("lock on", buildCode(write, lock, new byte[] { (byte) 0xF1 }))); // lock on
codes.add(new Code("lock off", buildCode(write, lock, new byte[] { (byte) 0xF0 }))); // lock off
codes.add(new Code("motor on", buildCode(write, motor, new byte[] { (byte) 0xF1 }))); // motor on
codes.add(new Code("motor off", buildCode(write, motor, new byte[] { (byte) 0xF0 }))); // motor off
codes.add(new Code("light on", buildCode(write, light, new byte[] { (byte) 0xF1 }))); // light on
codes.add(new Code("light off", buildCode(write, light, new byte[] { (byte) 0xF0 }))); // light off
codes.add(new Code("light flash", buildCode(write, (byte) 0x13, new byte[] { (byte) 0xFF }))); // Light flash
codes.add(new Code("light stop flash", buildCode(write, (byte) 0x13, new byte[] { (byte) 0x00 }))); // noflash
codes.add(new Code("FW", buildCode(read, fw, new byte[] {}))); // firmware ver
codes.add(new Code("reboot?", buildCode(write, reboot, new byte[] { (byte) 0xF1 })));
codes.add(new Code("write custom",
buildCode(write, (byte) 0xF2, new byte[] { (byte) 0xAB, (byte) 0xCA, (byte) 0x98, (byte) 0xB2 })));// AB
// CA
// 98
// B2
codes.add(new Code("write custom", buildCode(write, (byte) 0xF3, new byte[] { (byte) 0x09, (byte) 0x60 })));// 54
// 21
// 56
codes.add(new Code("read custom", buildCode(read, (byte) 0xF3, new byte[] {})));
codes.add(new Code("scan", new byte[] {}));
comboBox = new JComboBox<Code>(codes);
comboBox.setSelectedIndex(codes.size() - 1);
add(comboBox);
responses = new Vector<byte[]>();
validResponses = new Vector<byte[]>();
tthis = new Thread(this);
tthis.start();
while (!initialized) {
Thread.sleep(5);
}
reader = new ProbeReadThread(comPort, this);
}

public static void main(String[] args) throws InterruptedException {
JFrame probeFrame = new JFrame();
probeFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
LimeProbe probe = new LimeProbe();
probeFrame.setSize(1600, 600);
probeFrame.add(probe);
probeFrame.setVisible(true);
}

public void paint(Graphics g) {
g.setFont(font);
if (System.currentTimeMillis() - lastDraw > 42) {
lastDraw = System.currentTimeMillis();
super.paint(g);
// g.setColor(Color.lightGray);
// g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(Color.black);
g.drawString(bToS(data), 40, 80);
g.drawString(bToS(reader.readArray), 40, 140);
g.drawString(bToC(reader.readArray), 40, 160);
g.setColor(new Color(100, 0, 0));
for (int i = 0; i < Math.min(20, responses.size()); i++) {
// for(int i=Math.max(0, responses.size()-10); i<Math.min(10,responses.size());i++) {
byte[] rs = responses.get(i);
if (rs[0] == (byte) 0xFF && rs[1] == (byte) 0xFF && rs[2] == (byte) 0xFF) {
g.setColor(new Color(0, 150, 0));
g.drawString(bToS(Arrays.copyOfRange(rs, 3, rs.length)), 800, i * 14 + 210);
} else {
g.setColor(new Color(100, 0, 0));
g.drawString(bToS(rs), 40, i * 14 + 200);
}
}
for (int i = 0; i < Math.min(20, validResponses.size()); i++) {
// for(int i=Math.max(0, responses.size()-10); i<Math.min(10,responses.size());i++) {
byte[] rs = validResponses.get(i);
g.setColor(new Color(0, 100, 0));
g.drawString(bToS(rs), 40, i * 14 + 200);
}
}
}

public static boolean checkCRC(byte[] data) {
if (data.length >= 3) {
long crc = CRC.calculateCRC(CRC.Parameters.XMODEM, Arrays.copyOf(data, data.length - 2));
byte[] crca = { (byte) ((crc & 0xFF00) >> 8), (byte) (crc & 0xFF) };
byte[] rcrc = Arrays.copyOfRange(data, data.length - 2, data.length);
if (crca[0] == rcrc[0] && crca[1] == rcrc[1]) {
return true;
}
}
return false;
}

public void recieveResponse(byte[] response) {
if (checkCRC(response) && response[6] == (byte) 0xF1) {
// System.out.println("Got good code: " + bToS(response));
responses.add(cat(new byte[] { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, }, response));
validResponses.add(response);
} else {
responses.add(response);
}
if (responses.size() > 20) {
responses.remove(0);
}
}

@Override
public void run() {

try {

//
for (int i = 0; i < SerialPort.getCommPorts().length; i++) {
System.out.println("port found: " + SerialPort.getCommPorts());
}

comPort = SerialPort.getCommPorts()[0];
initialized = true;
System.out.println("Connecting to " + comPort);
comPort.setBaudRate(9600);
comPort.setFlowControl(SerialPort.FLOW_CONTROL_DISABLED);
comPort.openPort();

readBuffer = new byte[comPort.bytesAvailable()];
int numRead = comPort.readBytes(readBuffer, readBuffer.length);
if (numRead != 0) {
System.out.println("Cleared buffer of " + numRead);
}

// // Scanning ///////////////////
for (int i = (int)((byte)0x0A); i < 256; i++) {
Code code = (Code) comboBox.getSelectedItem();
for (int j = 0; j < 256; j++) {
if (code.name == "scan") {
data = buildCode(write, (byte) 0x61, new byte[] { (byte) 0xF1, (byte) i, (byte) j });
// (byte)i,(byte)j}); // 2D BRUTE FORCE
// data = buildCode(read, (byte) i, new byte[] {}); // Scan for codes
} else if (code.name == "custom read") {
data = code.code;
} else {
data = code.code;
}
// data = buildCode(read, (byte) 0x61, new byte[] {});
// data = buildCode(write, motor, new byte[] { (byte) 0xF1 }); // motor on
// data = buildCode(write, motor, new byte[] { (byte) 0xF1 }); // motor on
while (reader.busy) {
Thread.sleep(1);
}
comPort.writeBytes(data, data.length);
System.out.println("sent: " + bToS(data));
Thread.sleep(30);// 40
repaint();
}
}
Thread.sleep(500);
System.out.println("Valid responses found:");
for (int i = 0; i < validResponses.size(); i++) {
System.out.println(bToS(validResponses.get(i)));
}
//////////////////////////////
comPort.closePort();
Thread.sleep(100);
} catch (InterruptedException e) {
comPort.closePort();
e.printStackTrace();
}

}

private static byte[] buildCode(byte rw, byte code, byte[] msg) {
if (msg == null) {
msg = new byte[] {};
}
byte[] data = { 0x46, 0x43, rw, code, 0x00, (byte) msg.length };
data = cat(data, msg);
long crc = CRC.calculateCRC(CRC.Parameters.XMODEM, data);
byte[] crca = { (byte) ((crc & 0xFF00) >> 8), (byte) (crc & 0xFF) };
data = cat(data, crca);
// System.out.printf("%02X ", (crc & 0xFF00)>>8);
// System.out.printf("Printing: " + stringOut + "\n");
return data;
}

public static String bToS(byte[] bytes) {
String strOut = "";
for (int i = 0; i < bytes.length; i++) {
strOut += String.format("%02X ", bytes);
}
return strOut;
}

public static String bToC(byte[] bytes) {
String strOut = "";
for (int i = 0; i < bytes.length; i++) {
strOut += (char) bytes;
}
return strOut;
}

public static byte[] cat(byte[] a, byte[] b) {
byte[] c = new byte[a.length + b.length];
System.arraycopy(a, 0, c, 0, a.length);
System.arraycopy(b, 0, c, a.length, b.length);
return c;
}
}

class ProbeReadThread implements Runnable {
public boolean busy = false;
public byte[] readArray = { 0 };
SerialPort comPort = null;
SerialResponder serialResponder;

public ProbeReadThread(SerialPort inCom, SerialResponder toBeNotified) {
serialResponder = toBeNotified;
comPort = inCom;
(new Thread(this)).start();
}

@Override
public void run() {
try {
while (true) {
int waited = 0;
boolean verbose = false;
ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(513);
byte[] readBuffer = {};
while (waited < 40) {
if (comPort.bytesAvailable() <= 0) {
Thread.sleep(1);
if (busy) {
System.out.print("-");
waited++;
}
} else {
busy = true;
readBuffer = new byte[comPort.bytesAvailable()];
int numRead = comPort.readBytes(readBuffer, readBuffer.length);
// System.out.println(LimeProbe.bToS(data) + " sent");
System.out.print("\n(size:" + numRead + ")");
if (verbose) {
System.out.println(LimeProbe.bToS(readBuffer) + " response");
// System.out.println(bToC(readBuffer) + " chars");
System.out.println();
}
outBuffer.write(readBuffer);
}
} // end of message?
busy = false;
readArray = outBuffer.toByteArray();
serialResponder.recieveResponse(readArray);
System.out.println('\n' + LimeProbe.bToS(readArray) + '\n');
}
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
}

}
PetertheMD
Posts: 1
Joined: Sun Mar 08, 2020 1:06 am

Re: Lime SJ2.5 hacking

Postby PetertheMD Sun Mar 08, 2020 1:24 am

Hello!

I also have the updated version, unable to turn on the newer versions with the coding from before. I was not even able to read out the firmware version with the previous coding using SPP, so I am not sure how I can actually proceed. I got to the auction one month late it seems. I was able to simply sniff communications between the scooter and my SPP and I am getting repeating hex codes (as expected) but no response to anything. It seems none of the previous commands worked. But I recall the firmware data length being upped to 3 bytes (could be wrong). Anyway, if you need any help, I have a computer and a scooter that both need jobs, I'd be happy to help. I have most of the knowhow but am not too versed in java.

Maybe you can shed some light, as this command worked(used to) to read firmware: 46 43 16 14 00 00 00 C0 B0
CDogg
Posts: 6
Joined: Tue Sep 03, 2019 7:08 am

Re: Lime SJ2.5 hacking

Postby CDogg Tue Apr 28, 2020 9:40 am

Thanks for your efforts and info on this particular model. I bought a case lot of misc. junked scooters from my local city auction and I was able to identify and work on just about everything except for this one Lime scoot. This one was completely devoid of any identifying stickers, or model info. Having ridden the latest top 3 hive models in my city, it does resemble the current Lime on the street (minus the box on the front, which I assume to be the tracker box). Being that this unit is missing the IoT box, I figured there should be a way to turn it on at the ESC.
After poppin off the handle bars, it looked like the ESC is just sitting right there at the top. I could identify all the different wire harness except 1, which was taped up to the chassis of the ESC (sorry for the lack of pics- hindsight is 20/20)
My question is, being that I have exposed this ESC now, could I power up this behemoth scoot with some sort of power button?
CDogg
Posts: 6
Joined: Tue Sep 03, 2019 7:08 am

Re: Lime SJ2.5 hacking

Postby CDogg Tue Apr 28, 2020 9:50 am

Of course my pics didn't get uploaded with my reply 🤦‍♂️

Return to “Other scooters”

Who is online

Users browsing this forum: No registered users and 3 guests