Binary modding a water dispenser to save me from pressing a button (2021)
practicapp.com108 points by trinix912 4 days ago
108 points by trinix912 4 days ago
> In the end, it took about 8 hours to get this project finished, which was spread out over multiple weeks. Considering that I usually fill my bottle once a day, and it takes about 50 seconds to do so, this will have a positive return-on-investment after only about 1.5 years!
Spoken like a true programmer. :)
if you factor in the joy he gets each time he uses it, ROI is through the roof.
> Finally, we need to be aware that we can’t rely on the global variables to be initialized with a known value. Initializing it globally in this code would result in the compiler generating some instructions to be run before main() is called, which we can’t very easily patch into the original binary.
Assuming your increase the size of the binary, change the first instruction to jump to the end of the program, do your magic, perform the original first instruction, and finally jump to the second instruction. If the program itself does not self modify or otherwise examine its own size, this would get you to the original start with your modifications applied.
> In the end, it took about 8 hours to get this project finished, which was spread out over multiple weeks.
That’s incredible.
I'm surprised the manufacturer didn't enable code protection on the PIC.
If I were in need of a mod like this, I'd just wire another switch in parallel to control the appropriate solenoid.
This is legitimately great stuff!
His expensive electronic device now functions, from a UI perspective, like a normal tap.
Thank goodness traditional taps just work, and were invented before all this "smart" technology made our stuff so annoying.
When hacking a firmware like this, why can't you just rebuild the whole binary after modifying the decompiled source instead of patching a specific section of it?
Decompilation is primarily intended to be consumed by humans, I.e. to make the assembly more understandable because it’s written in functions, ifs, and for loops; it’ll likely not compile at all and if it does not to the original firmware. I don’t believe it’s typically an explicit goal of decompilation to be that high fidelity (ie allow recompilation). It’s probably possible to hack the source to compile to byte-for-byte the same firmware if desired with a little patience.
Other than that, you’d be right :-)
Correct, though you can essentially edit the 'decompiled' code directly at low level (then put it back into a binary), and for some languages this is even tolerable e.g. languages which compile to CIL.
OP didn't do this, because as they said: "I don’t like writing assembly". Although what they did is arguably harder...which maybe was the point! After all they could've just hooked up an MCU to proxy the button press for a given duration, which would've taken a lot less time.
You can edit disassembled code in your decompiler, though I'm not aware of a decompiler that lets you edit decompiled code. And editing a disassembly is tricky because you need to make sure your patch is exactly the same size as the original code, otherwise the addresses won't line up.
The usual approach for a non-trivial patch is exactly what the OP did here: put your patch somewhere in free space, and replace the original sequence of instructions with a jump to your patch.
>though I'm not aware of a decompiler that lets you edit decompiled code
DnSpy lets you do this (that's why I was careful to prefix decompilers with "native"in my answer). You can edit snippets of decompiled C# code directly inside. But vm based runtimes are much easier to decompile/recompile, and it still may not work in the end.
Though programs compiled to intermediate code are in general very different to analyze, so that's kind of nitpicking.
For C# and Java sure. Those are relatively easy to decompile and the decompilers are quite reliable. With exception of features like generator functions and lambdas most of the languages map 1:1 from source to bytecode, type information is mostly maintained.
With C and C++ things are a lot harder.
That assumes you have a source tree to build. Decompilation output generally is meant for human consumption and is not suitable for a compiler without significant rework and carrying out a full decompilation project (even a non-matching one) is a lot of work.
This is why traditional binary patching is the usual go-to option, because it doesn't require much upfront effort to perform. The downside is that you're constrained by the memory map of the original program and contorting your modifications to fit can be a huge pain.
There are alternative, less widely known techniques out there with different tradeoffs. For example, if you can make the original program bytes relocatable then you can run those through a linker and let it mend both original and new parts together. I've built tooling to help do that specifically (delinking programs back to object files [1]), it's faster than a decompilation project because you merely need to figure out and undo all the relocation spots, but you don't get source code at the end.
It won't work. Source code from native code decompilers is:
* not designed to be compiled again. It most likely won't compile without serious manual fixing. For example, decompilers often insert "pseudo-functions" to denote that something not easily representable in C is happening. Like CONCAT(var1, var2) may mean that both var1 and var2 are used as a single variable obtained by concatenating their bits (in practice: AX is used when AH and AL were already determined to be variables). Similar intrinsics exist for carry bits, jumps to arbitrary pointers, etc. This sometimes means that type inference went wrong somewhere, which brings us to...
* not perfect, and not designed to be perfect. Decompiler has no idea if a variable on stack is a qword or 8 byte array, it can only guess from local usage. In many cases array will be incorrectly decompiled as a single variable, or pointer parameter as an integer. This is not a huge problem when reversing, but catastrophic for decompilation. Automated struct identification is even harder, often almost impossible when you take unions into account. As a reverse engineer you are supposed to fix that interactively during analysis.
* decompilers in general don't decompile global data definitions - you interpret memory using a separate view. And for a good reason - what may look to the decompiler like three consecutive independent variables may actually be an (unrecognized) structure that must be kept in the same exact layout. Defining them as three independent C variables would almost surely not work.
* for firmware in particular the binary may be required to follow a specific layout or have other unusual characteristics (like volatile memory regions). No decompiler will give you a correct linker script to use for recompilation.
These problems are just out of the top of my mind. Worth noting that a new and upcoming reversing tool called rev.ng actually has working recompilation from C as one of their planned features, so we'll see what they come up with (and if they succeed).
Neat, but you don't need to drink water if you aren't thirsty.
The only practical purpose of this appears to be to author a blog post about it.
The true hack would be to purchase a mechanical water filter, probably much higher quality for the money spent, and have it attached to the plumbing. Bonus is it works when the power's out.
Aren't these water dispensers built into most fridges nowadays?
> The true hack would be to purchase a mechanical water filter, probably much higher quality for the money spent, and have it attached to the plumbing. Bonus is it works when the power's out.
Well, no. That's a solution, and it's probably practical, but in no way is buying a replacement a "hack".