Midnight CTF 2023 - Jet Logger
Jet Logger
was a linux reverse engineering challenge from MidNightCTF 2023.
We are given an archive with the following content :
jet_logger
encrypted_frames.txt.enc
Given the files, we can guess that we will be needing to find a way to decrypt the encrypted frames.
jet_logger
is a classic x64 ELF binary we can open in IDA.
Main function is rather simple:
int __cdecl main(int argc, const char **argv, const char **envp)
{
puts(
" ------------- JetLogger v1.19.3 -------------\n"
"| |\n"
"| The solution to keep your ADS-B frames safe |\n"
"| by HellCorp |\n"
"| |\n"
" ---------------------------------------------\n");
__printf_chk(1LL, "Please input your password : ");
if ( !fgets(s, 17, _bss_start) )
{
puts("An error occured when reading input...");
result = 1;
goto LABEL_7;
}
if ( !(unsigned __int8)check_password((FILE *)s) )
{
puts("Wrong password !");
goto LABEL_7;
}
puts("Success ! Decrypting your frames...");
v3 = fopen("./frames.txt.enc", "r");
while(1){
LOBYTE(v17) = fgetc(v3);
[some computation in a loop, using our input "s"]
algo_encrypt(&v17, (__m128i *)&v15[(16 * v12) & 0xFF0]);
[some more computation in a loop]
}
LABEL_17:
__printf_chk(1LL, "Here they are : \n%s\n", v15);
}
Parts of the decompilation has been redacted for brevity.
Two things to note :
- algo_encrypt is probably symmetrical, otherwise it could not be used to decrypt
frames.txt.enc
- we have to input a password that will probably be used as a key to decrypt frames
check_password
function seems rather simple too:
v3 = __readfsqword(0x28u);
result = 0;
if ( trigger_data == 34740228 )
{
puts("Password verification...");
trigger_data = 0x25120496uLL / (unsigned __int8)valid_ciphertext;
algo_encrypt(a1, &v2);
result = valid_ciphertext == v2;
}
if ( v3 != __readfsqword(0x28u) )
return on_breakpoint();
return result;
We have to find an input that result in valid_ciphertext
’s value once encrypted.
Let’s take a lok at algo_encrypt
:
.text:0000000000001984 mov edx, [rdi+8]
.text:0000000000001987 mov r8, rsi
.text:000000000000198A xor edx, cs:dword_4078
.text:0000000000001990 mov esi, [rdi]
.text:0000000000001992 xor esi, cs:key
.text:0000000000001998 ror edx, 0Dh
.text:000000000000199B rol esi, 7
.text:000000000000199E mov ecx, [rdi+4]
.text:00000000000019A1 mov eax, [rdi+0Ch]
.text:00000000000019A4 xor ecx, cs:dword_4074
.text:00000000000019AA xor eax, cs:dword_407C
.text:00000000000019B0 xor edx, esi
.text:00000000000019B2 mov r9d, esi
.text:00000000000019B5 xor eax, esi
.text:00000000000019B7 mov esi, ecx
.text:00000000000019B9 not r9d
.text:00000000000019BC ror ecx, 9
.text:00000000000019BF ror esi, 2
.text:00000000000019C2 xor ecx, r9d
.text:00000000000019C5 movd xmm1, r9d
.text:00000000000019CA xor eax, esi
.text:00000000000019CC movd xmm2, ecx
.text:00000000000019D0 xor eax, edx
.text:00000000000019D2 not edx
.text:00000000000019D4 punpckldq xmm1, xmm2
.text:00000000000019D8 movd xmm0, eax
.text:00000000000019DC movd xmm3, edx
.text:00000000000019E0 punpckldq xmm0, xmm3
.text:00000000000019E4 punpcklqdq xmm0, xmm1
.text:00000000000019E8 movups xmmword ptr [r8], xmm0
.text:00000000000019EC retn
The algorithm is simple enough. A constraint solver tool such as z3 should be able to provide a solution easily. Problem is, we cannot simply emulate this function to get our constraints, since it requieres knowing values of variables key
, qword_4074
, qword_4078
, qword_407C
, which are in .bss
section :
.bss:0000000000004070 key dd ? ; DATA XREF: algo_encrypt+12↑r
.bss:0000000000004070 ; on_breakpoint:loc_1C09↑w ...
.bss:0000000000004074 dword_4074 dd ? ; DATA XREF: algo_encrypt+24↑r
.bss:0000000000004074 ; on_breakpoint:loc_1B09↑w ...
.bss:0000000000004078 dword_4078 dd ? ; DATA XREF: algo_encrypt+A↑r
.bss:0000000000004078 ; on_breakpoint:loc_1C79↑w ...
.bss:000000000000407C dword_407C dd ? ; DATA XREF: algo_encrypt+2A↑r
I assume that those values will be filled at runtime. We can check where by looking at Xrefs. It seems to be used in a on_breakpoint
function, which is quite large, and I do not want to reverse engineer this function. A better approach would be to recover this value dynamically.
Back on check_password
, checking valid_ciphertext
’s reveals something fishy :
v3 = __readfsqword(0x28u);
result = 0;
if ( trigger_data == 34740228 )
{
puts("Password verification...");
trigger_data = 0x25120496uLL / (unsigned __int8)valid_ciphertext;
algo_encrypt(a1, &v2);
result = valid_ciphertext == v2;
}
if ( v3 != __readfsqword(0x28u) )
return on_breakpoint();
return result;
.data:0000000000004040 valid_ciphertext xmmword 3C2E4CAC1BC19A84684145B626570D00h
This value ends with a 0, which should trigger a zero divide exception.
Something must be editing this value. Taking a look at Xrefs, we can discover premain
function:
v0 = sys_clone(0x1200011uLL, 0LL, 0LL, (void *)(__readfsqword(0x10u) + 720));
child_pid = v0;
if ( (_DWORD)v0 )
{
waitpid(v0, 0LL, v1);
v2 = (unsigned int)child_pid;
v3 = v29;
for ( i = 54LL; i; --i )
{
*(_DWORD *)v3 = 0;
v3 += 4;
}
ptrace(PTRACE_GETREGS, v2, 0LL, v29);
child_load_addr = (__int64 (__fastcall *)(int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, __int64))(v31 - (_QWORD)child_load_addr);
install_hardware_breakpoint(12, v2, v5, v6, v7, v8, v21 & 0xFC, (__int64)main, v22 & 0xF0);
install_hardware_breakpoint(12, v2, v9, v10, v11, v12, v23 & 0xFC | 1, (__int64)check_password, v24 & 0xF0);
install_hardware_breakpoint(12, v2, v13, v14, v15, v16, v25 & 0xFC | 2, (__int64)&trigger_data, v26 & 0xF0 | 9);
install_hardware_breakpoint(12, v2, v17, v18, v19, v20, v27 | 3, (__int64)&trigger_data, v28 & 0xF0 | 0xB);
while ( 1 )
{
ptrace(PTRACE_CONT, (unsigned int)child_pid, 0LL, 0LL);
if ( (unsigned __int8)bp_index > 3u )
break;
waitpid(child_pid, 0LL, 0);
on_breakpoint();
}
bp_index = 0;
waitpid(child_pid, 0LL, 0);
ptrace(PTRACE_GETREGS, (unsigned int)child_pid, 0LL, v29);
v30 = 1LL;
LOBYTE(valid_ciphertext) = valid_ciphertext_start;
ptrace(PTRACE_POKEDATA, (unsigned int)child_pid, &valid_ciphertext, (_QWORD)valid_ciphertext);
ptrace(PTRACE_SETREGS, (unsigned int)child_pid, 0LL, v29);
ptrace(PTRACE_CONT, (unsigned int)child_pid, 0LL, 0LL);
exit(0);
}
ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);
A lot of mess here, this looks like a classic nanomite or father/child ptrace stuff I do not want to reverse engineer myself.
Sadly, PTRACE will probably prevent us from debugging this binary and recovering the values we need to recover to find a valid input (key
, qword_4074
, qword_4078
and qword_407C
).
Luckily, I created a tool at Flare-On CTF 2020 to help me reverse engineer ptraced binaries. It’s called tick, and all it does is preloading 99% of libc functions to log them to the terminal.
Running this tool outputs a rather long trace :
[ 5486] [waitpid]
[ 5486] [ptrace] ptrace(PTRACE_GETREGS, 5487)
rax : 0
rcx : 7FADF516D19E
rdx : 0
rsi : 0
rdi : 0
rbx : 0
r8 : FFFFFFFF
r9 : F
r10 : 0
r11 : 286
r12 : 7FFF2904A5D8
r14 : 55AB8F3611C0
r13 : 55AB8F363D68
r15 : 7FADF5334040
rbp : 1
rsp : 7FFF2904A3F0
orax : FFFFFFFFFFFFFFFF (origin RAX)
rip : 55AB8F36163F
[ 5486] [ptrace] ptrace | 3 is not implemented by the tool.
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x350) | data : 55AB8F3611C0
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x388) | data : 401
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x380) | data : 0
[ 5486] [ptrace] ptrace | 3 is not implemented by the tool.
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x358) | data : 55AB8F3619F0
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x388) | data : 405
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x380) | data : 0
[ 5486] [ptrace] ptrace | 3 is not implemented by the tool.
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x360) | data : 55AB8F364028
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x388) | data : 9000415
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x380) | data : 0
[ 5486] [ptrace] ptrace | 3 is not implemented by the tool.
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x368) | data : 55AB8F364028
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x388) | data : B9000455
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x380) | data : 0
[ 5486] [ptrace] ptrace(PTRACE_CONT, 5487) | [Delivered signal]: [Unknown signal 0]
[ 5486] [waitpid]
[long list of PTRACE PEEKTEXT here]
[ 5486] [ptrace] ptrace(PTRACE_POKEDATA, pid 5487, @ 0x55ab8f364070) | data : C4A34DA6
[ 5486] [ptrace] ptrace | 3 is not implemented by the tool.
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x350) | data : 0
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x388) | data : B9000454
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x380) | data : 0
[ 5486] [ptrace] ptrace(PTRACE_CONT, 5487) | [Delivered signal]: [Unknown signal 0]
[ 5486] [waitpid]
[long list of PTRACE PEEKTEXT here]
[ 5486] [ptrace] ptrace(PTRACE_POKEDATA, pid 5487, @ 0x55ab8f364074) | data : B7D80F2B
[ 5486] [ptrace] ptrace | 3 is not implemented by the tool.
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x358) | data : 0
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x388) | data : B9000450
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x380) | data : 0
[ 5486] [ptrace] ptrace(PTRACE_CONT, 5487) | [Delivered signal]: [Unknown signal 0]
[ 5486] [waitpid]
[long list of PTRACE PEEKTEXT here]
[ 5486] [ptrace] ptrace(PTRACE_POKEDATA, pid 5487, @ 0x55ab8f364078) | data : 77871143
[ 5486] [ptrace] ptrace | 3 is not implemented by the tool.
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x360) | data : 0
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x388) | data : B9000440
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x380) | data : 0
[ 5486] [ptrace] ptrace(PTRACE_CONT, 5487) | [Delivered signal]: [Unknown signal 0]
[ 5486] [waitpid]
[long list of PTRACE PEEKTEXT here]
[ 5486] [ptrace] ptrace(PTRACE_POKEDATA, pid 5487, @ 0x55ab8f36407c) | data : 9D63F669
[ 5486] [ptrace] ptrace | 3 is not implemented by the tool.
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x368) | data : 0
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x388) | data : B8000400
[ 5486] [ptrace] ptrace(PTRACE_POKEUSER, pid 5487, @ 0x380) | data : 0
[ 5486] [ptrace] ptrace(PTRACE_CONT, 5487) | [Delivered signal]: [Unknown signal 0]
[ 5486] [waitpid]
[ 5486] [ptrace] ptrace(PTRACE_GETREGS, 5487)
rax : 25120496
rcx : 0
rdx : 0
rsi : 7FFF2904A420
rdi : 7FFF2904A470
rbx : 0
r8 : 7FADF526CA70
r9 : 55AB8FF658A0
r10 : 7FADF526B2E0
r11 : 246
r12 : 7FFF2904A470
r14 : 55AB8F3611C0
r13 : 55AB8F363D68
r15 : 7FADF5334040
rbp : 7FFF2904A470
rsp : 7FFF2904A420
orax : FFFFFFFFFFFFFFFF (origin RAX)
rip : 55AB8F361A53
[ 5486] [ptrace] ptrace(PTRACE_POKEDATA, pid 5487, @ 0x55ab8f364040) | data : 684145B626570D2F
[ 5486] [ptrace] ptrace(PTRACE_SETREGS, 5487)
rax : 25120496
rcx : 1
rdx : 0
rsi : 7FFF2904A420
rdi : 7FFF2904A470
rbx : 0
r8 : 7FADF526CA70
r9 : 55AB8FF658A0
r10 : 7FADF526B2E0
r11 : 246
r12 : 7FFF2904A470
r14 : 55AB8F3611C0
r13 : 55AB8F363D68
r15 : 7FADF5334040
rbp : 7FFF2904A470
rsp : 7FFF2904A420
orax : FFFFFFFFFFFFFFFF (origin RAX)
rip : 55AB8F361A53
[ 5486] [ptrace] ptrace(PTRACE_CONT, 5487) | [Delivered signal]: [Unknown signal 0]
[ 5486] [exit]
[ 5487] [ptrace] ptrace(PTRACE_TRACEME, 0)
------------- JetLogger v1.19.3 -------------
| |
| The solution to keep your ADS-B frames safe |
| by HellCorp |
| |
---------------------------------------------
Please input your password : [ 5487] [fgets]
Password verification...
Wrong password !
Interresting values came out of PTRACE_POKEDATA
:
[ 5567] [ptrace] ptrace(PTRACE_POKEDATA, pid 5568, @ 0x55ccbecf4070 (base+0x4070)) | data : C4A34DA6
[ 5567] [ptrace] ptrace(PTRACE_POKEDATA, pid 5568, @ 0x55ccbecf4074 (base+0x4074)) | data : B7D80F2B
[ 5567] [ptrace] ptrace(PTRACE_POKEDATA, pid 5568, @ 0x55ccbecf4078 (base+0x4078)) | data : 77871143
[ 5567] [ptrace] ptrace(PTRACE_POKEDATA, pid 5568, @ 0x55ccbecf407c (base+0x407c)) | data : 9D63F669
[ 5567] [ptrace] ptrace(PTRACE_POKEDATA, pid 5568, @ 0x55ccbecf4040 (base+0x4040)) | data : 684145B626570D2F
Those are the values of key
(qword_4070
), qword_4074
, qword_4078
and qword_407C
. It also modifies valid_ciphertext
’s value, replacing the 0 with 0x2F.
All we have to do now is use them in a symbolic execution to gather our constraints and solve them using z3 :
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB
from miasm.analysis.binary import Container
from miasm.jitter.csts import *
from miasm.expression.expression import *
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.ir.translators.z3_ir import TranslatorZ3
from miasm.arch.x86.lifter_model_call import LifterModelCall_x86_64
import z3
import sys
if len(sys.argv) != 2:
print(f'Usage : {sys.argv[0]} <filename>')
exit(1)
filename = sys.argv[1] #'jet_logger.bak'
loc_db = LocationDB()
data = open(filename, 'rb').read()
container = Container.from_stream(open(filename, 'rb'), loc_db)
machine = Machine(container.arch)
ira = machine.lifter(loc_db)
dis_engine = machine.dis_engine(container.bin_stream, loc_db=loc_db)
# we want to start emulation right inside encrypt func, here:
# .text:0000000000001984 mov edx, [rdi+8]
# .text:0000000000001987 mov r8, rsi
# .text:000000000000198A xor edx, cs:dword_4078
# .text:0000000000001990 mov esi, [rdi]
# .text:0000000000001992 xor esi, cs:key
# .text:0000000000001998 ror edx, 0Dh
start_address = 0x1984
# Disassemble fn
asm_cfg = dis_engine.dis_multiblock(start_address)
ira_cfg = ira.new_ircfg_from_asmcfg(asm_cfg)
# Now we need to prepare the state for symexec
# This function takes two parameters :
# - a pointer to a 16 bytes argument in RDI,
# - a pointer to a 16 bytes result in RSI
init_state = {}
# RDI will point to our arguments @0xDEAD0000, RSI to the result @0xC0FEC0FE
init_state[ExprId('RDI', 64)] = ExprInt(0xDEAD0000, 64)
init_state[ExprId('RSI', 64)] = ExprInt(0xC0FEC0FE, 64)
# Symbolize each byte of input
for i in range(16):
init_state[ExprMem(ExprInt(0xDEAD0000+i, 64), 8)] = ExprId(f'ARG{i}', 8)
# We add relevant information produced by ptrace child/father mechanism
init_state[ExprMem(ExprInt(0x4070, 64), 32)] = ExprInt(0xC4A34DA6, 32)
init_state[ExprMem(ExprInt(0x4074, 64), 32)] = ExprInt(0xB7D80F2B, 32)
init_state[ExprMem(ExprInt(0x4078, 64), 32)] = ExprInt(0x77871143, 32)
init_state[ExprMem(ExprInt(0x407C, 64), 32)] = ExprInt(0x9D63F669, 32)
# Execution of the encrypt function
sb = SymbolicExecutionEngine(LifterModelCall_x86_64(loc_db) , state=init_state)
sb.run_block_at(ira_cfg, addr=start_address, step=False)
# At this point symex is over, we translate result to z3 expression
trans = TranslatorZ3(loc_db=loc_db)
# We want to find an input that result in this key we retrieved from ptrace child/father mechanism
values = [0x2F, 0x0D, 0x57, 0x26, 0xB6, 0x45, 0x41, 0x68,
0x84, 0x9A, 0xC1, 0x1B, 0xAC, 0x4C, 0x2E, 0x3C]
s = z3.Solver()
for i in range(16):
s.add(trans.from_expr(ExprId(f'ARG{i}', 8)) >= trans.from_expr(ExprInt(0x20, 8) ))
s.add(trans.from_expr(ExprId(f'ARG{i}', 8)) <= trans.from_expr(ExprInt(0x7F, 8) ))
expr = sb.eval_expr(ExprMem(ExprInt(0xC0FEC0FE+i, 64), 8))
cmp = ExprInt(values[i], 8)
cmp = trans.from_expr(cmp)
expr = trans.from_expr(expr)
s.add(expr == cmp)
assert (s.check() == z3.sat)
## Extract values from model
model = s.model()
r = {}
for model_index, _ in enumerate(model):
index = int(_.name().split("ARG")[1])
r[index] = _
password = ''.join([chr(int(str(model[r[i]]))) for i in range(16)])
print(f'Password is : {password}')
$ python3 ./src/solve.py ../../jet_logger
Password is : l1k3d_th3_alg0_?