Sitemap
Prosper Kalu

Prosper Kalu

REA + DeepSeek: Reverse-Engineering an EXE with a Weak License

A prompt-driven walkthrough — entry point, core logic, and the license workflow (plus the defect that makes the local check moot)

8 min readOct 24, 2026

--

Press enter or click to view image in full size

I spent an afternoon reverse-engineering a small Windows desktop tool using REA (a reverse-engineering agent toolkit) driven by DeepSeek (V4.1 Flash), with Ghidra 12.1.4 as the analysis provider. The goal was modest: understand what the binary does, how its licensing works, and how far I could get with a mostly conversational workflow instead of clicking around a disassembler.

This post documents the prompts, the dead ends, the workarounds, and the findings. If you do similar work, I hope the methodology transfers — the specific target is just the vehicle.

Scope note. This was performed on a local copy of a shipped binary, for interoperability and research. The post describes what the code does. It does not contain patch instructions, key forgery, or server emulation, and I’m not publishing anything intended to bypass someone’s licensing.

1. The target

Format
PE, x86–64

Toolchain
MSVC, Unicode, MFC GUI (Debug-style build)

SHA-256
1010925a5a94c385c7f582da13ec9b61ac75db0558239fe0028fd1d4d738441e

Provider
Ghidra 12.1.4 via REA

No PDB, so every application function arrived as FUN_<address>. That's the normal starting condition for this kind of work, and it's where the agent's routing matters more than its decompiler.

2. Prompt 1 — let the agent route the target

My opening prompt was deliberately outcome-shaped, not tool-shaped:

“Use REA with the ghidra provider to open the binary at MODLocker_fix_V160/SCSBlock.exe. Find the entry point, decompile the core logic functions, and explain what this binary is doing.”

Two things I like about framing it this way:

  • I describe the outcome (“explain what it’s doing”), not a sequence of menu clicks.
  • The provider is named explicitly, which removes ambiguity when multiple engines are available.

The agent opened the binary, bound a Ghidra session, and immediately gave me the entry point plus the CRT/MFC boot chain.

3. What “entry point” actually means here

The raw entry is uninteresting — it’s boilerplate:

0x1401e17d8: SUB RSP, 0x28
0x1401e17dc: CALL 0x1401e2320 ; __security_init_cookie
0x1401e17e1: ADD RSP, 0x28
0x1401e17e5: JMP 0x1401e1664 ; __scrt_common_main

But following it tells you a lot about the program:

flowchart TD
A["entry 0x1401e17d8"] --> B["__security_init_cookie 0x1401e2320"]
A --> C["__scrt_common_main 0x1401e1664"]
C --> D["_initterm: C + C++ initializers"]
C --> E["thunk -> AfxWinMain 0x140217d94"]
E --> F["AfxWinInit + CWinApp virtuals<br/>InitApplication / InitInstance / Run"]

The interesting bit is at the end of __scrt_common_main:

uVar4 = __scrt_get_show_window_mode();
uVar8 = _get_wide_winmain_command_line(); /* <- Unicode build */
uVar6 = thunk_FUN_140217d94(&IMAGE_DOS_HEADER_140000000, 0, uVar8, uVar4);

_get_wide_winmain_command_line plus AfxWinInit tells you it's an MFC Unicode GUI application, not a console tool. And AfxWinMain dispatches the classic CWinApp virtuals (InitApplication, InitInstance, Run).

Takeaway for your own workflow: decompile the first three functions before anything else. They cost nothing and they classify the binary (CRT, GUI vs console, framework).

4. Mapping the app when every symbol is FUN_*

Here’s where the session got interesting. A chunk of the native inventory tools were disabled in my session. Doing this by hand in Ghidra would have meant hours of cross-reference clicking. The workaround turned out to be the most useful trick of the day:

Ghidra names defined strings, and REA can resolve those names.

For a string at 0x140288788 with the value /api/scsblock?id=, Ghidra's auto-label is:

u_/api/scsblock?id=_140288788

Ask for xrefs to that name and you get the code that references the string:

“Find xrefs to the name u_/api/scsblock?id=_140288788.”

{ "status": "resolved",
"address": "0x140288788",
"xrefs": ["0x14004c361", "0x14004c3aa", "0x14004def1"] }

That single technique mapped the app’s functional surface in minutes:

/api/scsblock?id=
licence client

timer.acme.com.br
timer client

license.txt
id parser + “invalid” writer

manifest.sii
block/repack worker

Get Prosper Kalu’s stories in your inbox

Join Medium for free to get updates from this writer.

EncryptSII / SUI / DDS / MAT
options builder

Practical tip: when symbol-based xref tools are unavailable, pivot through string labels. s_ = ASCII, u_ = UTF‑16. For a Unicode MFC build, most app strings are u_.

5. Prompt 2 — diagram the licence workflow

Once I knew the players, I asked for the workflow rather than more code:

“Use REA to trace the licence workflow in SCSBlock.exe. Produce a sequence diagram of the validation flow that includes the exact request URL and the exact server response that constitutes a valid state. Show what the client does on a valid response, and on anything else. Keep observed facts separate from inferences.”

The diagram it produced (with the same request/response detail spelled out):

Press enter or click to view image in full size

The request, exactly

GET /api/scsblock?id=76561198000000001 HTTP/1.1
Host: login.acme.com
User-Agent: Acme/1.0
Accept: application/json

…which is reproducible as:

curl -sS --tlsv1.2 -o /dev/null -w '%{http_code}\n' \
'https://login.acme.com/api/scsblock?id=76561198000000001' \
-H 'User-Agent: Acme/1.0' \
-H 'Accept: application/json'

The “valid” response

HTTP/1.1 200 OK

That’s the whole contract on the client side. To paraphrase the decompiled code:

WinHttpQueryHeaders(req, 0x20000013 /* STATUS_CODE | FLAG_NUMBER */, 0, &status);
if (query_ok && status == 200) {
ok = 1;
FUN_14004a4b0(id); /* success handler */
} else {
ok = 0; /* any other result */
}

No body parsing, no content-type check, no signature. The body is discarded unread, and the request asks for application/json purely as a hint. That's worth sitting with: the client treats a bare status code as the entire authorisation decision.

6. Line by line: the licence client

Walking FUN_14004c1d0 turned up a few nice details that a summary would have missed:

  • The id arrives as a std::string and is copied to a global (DAT_1402d5060).
  • It’s converted to UTF‑16 and the route is prepended with a std::wstring::insert. Ghidra renders that as three memmove/memcpy calls and a 0x11 (17) length bump.
  • WinHttpOpen(L"Acme/1.0", …) — the UA is baked in.
  • WinHttpSetOption(h, 0x54, {0x800}) → TLS 1.2 only.
  • WinHttpSetTimeouts(h, 10000, 10000, 10000).
  • WinHttpConnect(h, L"login.acme.com", 443).

Prompt that produced it:

“Walk the licence API client line by line. Annotate the Ghidra pseudocode and explain what each meaningful line does, including the string manipulation.”

Small detail with a lesson attached: WinHttpOpen's access type here is WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, which reads WinHTTP's proxy configuration — not the browser/IE proxy settings. That distinction matters if you're debugging through a proxy and wondering why the client seems to ignore it.

7. The timer that isn’t about licensing

The second endpoint is the one people misread:

GET / HTTP/1.1
Host: timer.acme.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: application/json
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Upgrade-Insecure-Requests: 1

Two observations:

  1. It carries no identity. The object is the constant L"/"; param_1 is declared and never used. I confirmed the verb and path by reading the UTF‑16 literals straight out of .rdata (GET at 0x140285e68, / at 0x140285e64).
  2. It’s a time service. The response is a worldtimeapi-style document:
{"abbreviation":"-03","client_ip":"…","datetime":"2026-10-14T11:36:57.381381-03:00",
"day_of_week":3,"day_of_year":287,"raw_offset":-10800,"timezone":"America/Sao_Paulo",
"unixtime":1791988617,"utc_datetime":"2026-10-14T14:36:57.381381+00:00", …}

Prompt:

“What value from the timer response is used to calculate ‘Days left’?”

The decisive check wasn’t more decompiling — it was a string search. The JSON key the client looks up must exist as a literal in the image. Result:

unixtime      : (absent)
utc_datetime : (absent)
timezone : (absent)
datetime : ascii @0x140285be8 + '%Y-%m-%dT%H:%M:%S' right beside it
client_ip : (absent)
plugin : (absent)

So: the client reads datetime, not unixtime, and parses it with %Y-%m-%dT%H:%M:%S. Which explains a quirk in the code we'd seen earlier — the parsed value is required to be a string (cmp BYTE PTR [rax],0x3), and a string is exactly what datetime is.

The format has no %f and no %z, so the .381381 fraction and the -03:00 offset are both ignored: the client effectively uses the server's local wall-clock time as a naive timestamp. Nice detail, easy to miss.

8. The bug hiding in the licence state

This was the most interesting finding, and it came from a completeness discipline rather than a clever trick.

The app writes an “invalid licence” notice into license.txt when a flag is zero:

if (DAT_1402d5058 == 0) {
_wfopen_s(&fp, L"license.txt", L"w+");
fprintf(fp, "Invalid License: %s\nContact: …", DAT_1402d5060);
}

So I asked a blunt question:

“Find every reference to 0x1402d5058 and tell me whether it is ever written.”

My first scan was wrong — I looked for RIP-relative operands where the disp32 is the last field, which silently misses mov [rip+d], imm forms. Redoing it with a real disassembly gave the complete list:

READ  0x14000c432   cmp  DWORD PTR [rip+…], ebx      # 0x1402d5058
READ 0x14000c636 cmp DWORD PTR [rip+…], 0x0 # 0x1402d505c (×10)
WRITE 0x1400101c6 mov QWORD PTR [rip+…], rbx # 0x1402d5058 (zeroing block)

That’s it. The only write is in a bulk zero-initialiser. The success handler (FUN_14004a4b0, called only on HTTP 200) never touches the flag. So the condition flag == 0 is effectively always true, and the notice is written regardless of what the server said.

Two lessons:

  • “Only one writer” is a finding, not a non-finding. Enumerating all accesses and finding nothing is how you discover dead/missing state.
  • Validate your scanning method. A plausible-looking scan that misses an instruction form will quietly give you a wrong story. I caught mine by cross-checking against the disassembler’s own RIP-relative comments.

9. Methodology notes I’d carry forward

  1. Start with boot, not with main. The three CRT functions classify the binary cheaply.
  2. Pivot through string labels when symbol xrefs are unavailable. s_… / u_… are stable and let you jump from a visible string to the code that uses it.
  3. Read the literals. Constants next to each other in .rdata tell you the protocol: GET next to / next to the host; datetime next to its strptime format.
  4. Ask for “observed vs inferred” explicitly. It keeps the write-up honest and stops you from over-claiming on a decompiler artifact.
  5. Use more than one tool for the same question. The decompiler gives you semantics; objdump + a PE parser give you ground truth about encodings and xrefs.
  6. Frame prompts as deliverables. “Produce a sequence diagram including the exact URL and the valid response” works far better than “look at the licensing code”.

10. Ethics and scope

Reverse-engineering a program you have, to understand it, is normal engineering. Publishing an analysis is normal too. What I deliberately did not do here:

  • no patch offsets or “set flag X to 1” instructions,
  • no forged licence files,
  • no emulation of the vendor’s licence server.

If you’re writing up similar work: redact anything that is effectively a bypass recipe, keep the target’s identity and hashes for reproducibility, and separate mechanism from exploitation.

Appendix — prompts used in this session

1) Use REA with the `ghidra` provider to open the binary at
`SCSBlock.exe`. Find the entry point,
decompile the core logic functions, and explain what this binary is doing.

2) Use REA to trace the licence workflow in SCSBlock.exe. Produce a sequence diagram of
the validation flow that includes the exact request URL and the exact server response
that constitutes a valid state. Show what the client does on a valid response, and on
anything else. Keep observed facts separate from inferences.

3) Walk the licence API client line by line. Annotate the Ghidra pseudocode and explain
what each meaningful line does, including the string manipulation.

4) Find xrefs to the name u_/api/scsblock?id=_140288788.

5) What value from the timer response is used to calculate "Days left"?

6) Confirm the full request made to timer with the endpoint, and show it as curl.

7) Find every reference to 0x1402d5058 and tell me whether it is ever written.

--

--