POSTS
MRMCDCTF2019: ElizeVC
Solution to ElizeVC (very hard) from MRMCDCTF 2019
ElizeVC was the hardest challenge I had written for this year’s MRMCDCTF.
Protections
The binary of this challenge is protected with various methods:
- crypter/protector: The binary is encrypted on a per-function basis. Each protected function is encrypted with 128-bit XTEA in Counter Mode, with different keys and IVs for every function. Whenever a protected function is entered, the uncrypter is called, which decrypts the function in place. Before doing so, it checks if it had decrypted another function before. If so, that function is re-encrypted before decrypting the later function. That way there is never more than one protected function unencrypted in memory at any time.
- The uncrypter itself was subjected to the carbonara obfuscator to transform it into spaghetti code.
- The uncrypter uses
ptrace(PTRACE_TRACEME)
to check for an attached debugger. If a debugger is found, it corrupts the encryption key before decryption. This leads to a crash in the protected function (far away from the debugger check). - The uncrypter computes a checksum of itself, which is then used as part of the initialisation vector. Any modification of the uncrypter code (like patching out the debugger check, undoing the carbonara obfuscation or just setting a soft breakpoint) leads to a changed IV and corrupted code later on.
- The symbol values of all protected functions were randomised to point to (more or less) random places in the binary (similar to the misguided challenge).
Solution:
The challenge can be solved without completely understanding the crypter/uncrypter system.
There are two intended solutions: the first one solves the challenge with the protections in place, while the second approach is to undo the encryption and (more or less) reconstruct the original binary. I will focus on the the first way here, and save the ‘reconstruct the binary’ solution for another post.
Both ways rely on getting around the anti-debugger protection, so this is the first thing we will do.
Solving the Anti-Debugger check
The presence of this check can be easily deduced by the fact that the program runs flawlessly without a debugger, but crashes once a debugger gets attached. And you can detect the nature of the check by running is with strace:
user@host:~/MRMCD2019/reversing/elizeVC/downloads$ strace ./elize
execve("./elize", ["./elize"], 0x7ffc63821050 /* 70 vars */) = 0
[...]
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
ptrace(PTRACE_TRACEME) = -1 EPERM (Vorgang nicht zulässig)
mprotect(0x55c1dab83000, 543, PROT_READ|PROT_WRITE) = 0
mprotect(0x55c1dab83000, 543, PROT_READ|PROT_EXEC) = 0
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x27bb8527} ---
+++ killed by SIGSEGV +++
Speicherzugriffsfehler
Whenever you see a ptrace(PTRACE_TRACEME) syscall in a CTF challenge, you can safely assume it’s an anti-debugger trick.
The ptrace check here can not be patched out (neither on disk nor in memory) without breaking the checksum (which results in faulty decryption). When you try to do this, you will only end up with a binary that’s not running, no matter whether a debugger is present or not.
However the ptrace syscall can be intercepted by the debugger and its return value changed:
[...]
gef➤ catch syscall ptrace
Abfangpunkt 1 (syscall 'ptrace' [101])
gef➤ commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>set $rax = 0
>c
>end
gef➤ r
Starting program: /media/tmp/elize.encrypted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Catchpoint 1 (call to syscall ptrace), 0x000055555556323e in ?? ()
Catchpoint 1 (returned from syscall ptrace), 0x000055555556323e in ?? ()
The password is the flag. What is the password?
bla
Wrong. Try again.
[Inferior 1 (process 6347) exited normally]
[...]
The debugger breaks after each ptrace-syscall, sets the return value (in rax) to 0 and continues. This is equivalent to passing the anti-debugger check, and the program now runs under gdb.
Solution 1: Follow the data
The first intended solution solves the challenge without actually defeating the protector. The approach here is to follow the entered password around in memory and understand what is happening to it (just as we did with the Carbonara challenge).
The challenge process must somehow read in the password.
One way to do this is fgets
, which is imported, but (seemingly) not referenced in the code (the function using it is encrypted).
But we can set a breakpoint on it anyway:
gef➤ b fgets
Haltepunkt 2 at 0x3070
gef➤ r
Starting program: elizeVC/downloads/elize
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Catchpoint 1 (call to syscall ptrace), 0x000055555556323e in ?? ()
Catchpoint 1 (returned from syscall ptrace), 0x000055555556323e in ?? ()
The password is the flag. What is the password?
Breakpoint 2, _IO_fgets (buf=0x55555556e670 "", n=0x100, fp=0x7ffff7d83a00 <_IO_2_1_stdin_>) at iofgets.c:37
37 iofgets.c: Datei oder Verzeichnis nicht gefunden.
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────── registers ────
[...]
────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7c210f4 <fgetpos64+388> mov rsi, rax
0x7ffff7c210f7 <fgetpos64+391> jmp 0x7ffff7bc46f8 <_IO_new_fgetpos+4294588296>
0x7ffff7c210fc nop DWORD PTR [rax+0x0]
→ 0x7ffff7c21100 <fgets+0> test esi, esi
0x7ffff7c21102 <fgets+2> jle 0x7ffff7c21240 <_IO_fgets+320>
0x7ffff7c21108 <fgets+8> push r12
0x7ffff7c2110a <fgets+10> mov r8d, esi
0x7ffff7c2110d <fgets+13> mov r12, rdi
0x7ffff7c21110 <fgets+16> push rbp
Success! The program has now executed past the point where it displays the “What is the password?” message and is trying the get the password from the user.
We can now let fgets
finish, and then set an access watchpoint on the read buffer (0x55555556e670, see the ‘Breakpoint 2’-line).
gef➤ finish
Run till exit from #0 _IO_fgets (buf=0x55555556e670 "", n=0x100, fp=0x7ffff7d83a00 <_IO_2_1_stdin_>) at iofgets.c:37
fooooooooo
0x000055555555727e in ?? ()
Value returned is $1 = 0x55555556e670 "fooooooooo\n"
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────── registers ────
$rax : 0x000055555556e670 → "fooooooooo"
$rbx : 0x0000555555557231 → mov edi, 0x100
$rcx : 0xfbad2288
$rdx : 0x000055555556e670 → "fooooooooo"
$rsp : 0x00007fffffffd898 → 0x000055555556d160 → 0x000000000000adc1
$rbp : 0x00007fffffffd8b0 → 0x00007fffffffd950 → 0x00007fffffffd978 → 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15
$rsi : 0x00007ffff7d86590 → 0x0000000000000000
$rdi : 0x000055555556e671 → "ooooooooo"
$rip : 0x000055555555727e → mov rax, QWORD PTR [rbp-0x8]
$r8 : 0x000055555556e78b → 0x0000000000000000
$r9 : 0x00007ffff7b76b80 → 0x00007ffff7b76b80 → [loop detected]
$r10 : 0x00007ffff7d83ca0 → 0x000055555556eb80 → 0x0000000000000000
$r11 : 0x246
$r12 : 0x00005555555570c0 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdb10 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
───────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd898│+0x0000: 0x000055555556d160 → 0x000000000000adc1 ← $rsp
0x00007fffffffd8a0│+0x0008: 0x00007fffffffd920 → 0x0000008f24e9b100
0x00007fffffffd8a8│+0x0010: 0x000055555556e670 → "fooooooooo"
0x00007fffffffd8b0│+0x0018: 0x00007fffffffd950 → 0x00007fffffffd978 → 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15 ← $rbp
0x00007fffffffd8b8│+0x0020: 0x000055555556358a → jne 0x5555555632ad
0x00007fffffffd8c0│+0x0028: 0x00007ffff7f91f10 → 0x00007ffff7b78000 → 0x03010102464c457f
0x00007fffffffd8c8│+0x0030: 0x00000000ffffffff
0x00007fffffffd8d0│+0x0038: 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15
─────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555557271 mov esi, 0x100
0x555555557276 mov rdi, rax
0x555555557279 call 0x555555557070 <fgets@plt>
→ 0x55555555727e mov rax, QWORD PTR [rbp-0x8]
0x555555557282 lea rsi, [rip+0x1d7b] # 0x555555559004
0x555555557289 mov rdi, rax
0x55555555728c call 0x555555557090 <strtok@plt>
0x555555557291 mov rax, QWORD PTR [rbp-0x8]
0x555555557295 leave
───────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x55555555727e → mov rax, QWORD PTR [rbp-0x8]
[#1] 0x55555556358a → jne 0x5555555632ad
[#2] 0x7ffff7f91f10 → add BYTE PTR [rax+0x7ffff7b7], al
────────────────────────────────────────────────────────────────────────────────────
gef➤ awatch *0x55555556e670
Hardware access (read/write) watchpoint 3: *0x55555556e670
gef➤
Now we have an access watchpoint on the buffer the password was read into. Whenever the program tries to do anything with it, the debugger will step in.
But one other thing is noteworthy here:
The disassembly above is the code that makes the call to fgets
.
It is located in the memory range where the challenge binary was loaded,
but it was definitely not part of the binary on disk or at startup time.
This is the whole function now:
gef➤ x/30i 0x555555557220
0x555555557220: mov rbp,rsp
0x555555557223: sub rsp,0x10
0x555555557227: mov eax,0x0
0x55555555722c: call 0x55555555823f <tohtaapixohliboifuje+190>
0x555555557231: mov edi,0x100
0x555555557236: call 0x555555557080 <malloc@plt>
0x55555555723b: mov QWORD PTR [rbp-0x8],rax
0x55555555723f: mov rax,QWORD PTR [rbp-0x8]
0x555555557243: mov edx,0x100
0x555555557248: mov esi,0x0
0x55555555724d: mov rdi,rax
0x555555557250: call 0x555555557060 <memset@plt>
0x555555557255: cmp QWORD PTR [rbp-0x8],0x0
0x55555555725a: jne 0x555555557266
0x55555555725c: mov edi,0xffffffff
0x555555557261: call 0x5555555570a0 <exit@plt>
0x555555557266: mov rdx,QWORD PTR [rip+0x3da3] # 0x55555555b010 <stdin@@GLIBC_2.2.5>
0x55555555726d: mov rax,QWORD PTR [rbp-0x8]
0x555555557271: mov esi,0x100
0x555555557276: mov rdi,rax
0x555555557279: call 0x555555557070 <fgets@plt>
=> 0x55555555727e: mov rax,QWORD PTR [rbp-0x8]
0x555555557282: lea rsi,[rip+0x1d7b] # 0x555555559004
0x555555557289: mov rdi,rax
0x55555555728c: call 0x555555557090 <strtok@plt>
0x555555557291: mov rax,QWORD PTR [rbp-0x8]
0x555555557295: leave
0x555555557296: ret
0x555555557297: lea rax,[rip+0x15daa] # 0x55555556d048
0x55555555729e: pop rbx
gef➤
And this is how the same code looked at the start of the program:
gef➤ x/30i 0x555555557220
0x555555557220: lea eax,[rip+0x15dea] # 0x55555556d010
0x555555557226: pop rbx
0x555555557227: push rax
0x555555557228: push rbx
0x555555557229: jmp 0x555555563000
0x55555555722e: add BYTE PTR [rax],bh
0x555555557230: imul esp,edx,0x37ee45b6
0x555555557236: push 0x36
0x555555557238: jge 0x555555557219
0x55555555723a: iret
0x55555555723b: jae 0x5555555572b9
0x55555555723d: loop 0x55555555724b
0x55555555723f: (bad)
0x555555557240: mov edx,0x9354d72e
0x555555557245: xor al,bl
0x555555557247: or ah,BYTE PTR [rdi+0x27bb8526]
0x55555555724d: in eax,dx
0x55555555724e: cmps BYTE PTR ds:[rsi],BYTE PTR es:[rdi]
0x55555555724f: (bad)
0x555555557250: (bad)
0x555555557251: loopne 0x55555555729a
0x555555557253: xchg esp,eax
0x555555557254: out 0xb1,al
0x555555557256: xor ebx,ebx
0x555555557258: cmp ebp,DWORD PTR [rdi+0x79]
0x55555555725b: sbb ch,ah
0x55555555725d: add al,0xd6
0x55555555725f: (bad)
0x555555557260: or al,0xf0
0x555555557262: retf
This is definitely not the same code; in fact, it’s not code at all (except the first five instructions): this is what it looks like if you try to disassemble random bytes.
If you didn’t already know you are dealing with some form of self-modifying code, this would have been your hint.
But back to the password in memory (guarded by the watchpoint):
If we let the process continue, the watchpoint will be hit in strtok
:
gef➤ c
Continuing.
Hardware access (read/write) watchpoint 3: *0x55555556e670
Value = 0x6f6f6f66
0x00007ffff7c3d125 in __GI___strtok_r (s=0x55555556e670 "fooooooooo\n", delim=0x555555559004 "\n", save_ptr=0x7ffff7d86728 <olds>) at strtok_r.c:49
49 strtok_r.c: Datei oder Verzeichnis nicht gefunden.
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────── registers ────
$rax : 0x000055555556e670 → "fooooooooo"
$rbx : 0x000055555556e670 → "fooooooooo"
$rcx : 0xfbad2288
$rdx : 0x00007ffff7d86728 → 0x0000000000000000
$rsp : 0x00007fffffffd878 → 0x0000555555557231 → mov edi, 0x100
$rbp : 0x00007ffff7d86728 → 0x0000000000000000
$rsi : 0x0000555555559004 → 0x25000a73250a000a
$rdi : 0x000055555556e670 → "fooooooooo"
$rip : 0x00007ffff7c3d125 → <strtok_r+21> je 0x7ffff7c3d160 <__GI___strtok_r+80>
$r8 : 0x000055555556e78b → 0x0000000000000000
$r9 : 0x00007ffff7b76b80 → 0x00007ffff7b76b80 → [loop detected]
$r10 : 0x00007ffff7d83ca0 → 0x000055555556eb80 → 0x0000000000000000
$r11 : 0x246
$r12 : 0x0000555555559004 → 0x25000a73250a000a
$r13 : 0x00007fffffffdb10 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
───────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd878│+0x0000: 0x0000555555557231 → mov edi, 0x100 ← $rsp
0x00007fffffffd880│+0x0008: 0x00007fffffffd8b0 → 0x00007fffffffd950 → 0x00007fffffffd978 → 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15
0x00007fffffffd888│+0x0010: 0x00005555555570c0 → <_start+0> xor ebp, ebp
0x00007fffffffd890│+0x0018: 0x0000555555557291 → mov rax, QWORD PTR [rbp-0x8]
0x00007fffffffd898│+0x0020: 0x000055555556d160 → 0x000000000000adc1
0x00007fffffffd8a0│+0x0028: 0x00007fffffffd920 → 0x0000008f24e9b100
0x00007fffffffd8a8│+0x0030: 0x000055555556e670 → "fooooooooo"
0x00007fffffffd8b0│+0x0038: 0x00007fffffffd950 → 0x00007fffffffd978 → 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15
─────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7c3d11d <strtok_r+13> test rdi, rdi
0x7ffff7c3d120 <strtok_r+16> je 0x7ffff7c3d170 <__GI___strtok_r+96>
0x7ffff7c3d122 <strtok_r+18> cmp BYTE PTR [rbx], 0x0
→ 0x7ffff7c3d125 <strtok_r+21> je 0x7ffff7c3d160 <__GI___strtok_r+80> NOT taken [Reason: !(Z)]
0x7ffff7c3d127 <strtok_r+23> mov rdi, rbx
0x7ffff7c3d12a <strtok_r+26> mov rsi, r12
0x7ffff7c3d12d <strtok_r+29> call 0x7ffff7bc40b0 <*ABS*+0x9d720@plt>
0x7ffff7c3d132 <strtok_r+34> add rbx, rax
0x7ffff7c3d135 <strtok_r+37> cmp BYTE PTR [rbx], 0x0
───────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7c3d125 → __GI___strtok_r(s=0x55555556e670 "fooooooooo\n", delim=0x555555559004 "\n", save_ptr=0x7ffff7d86728 <olds>)
[#1] 0x555555557291 → mov rax, QWORD PTR [rbp-0x8]
[#2] 0x55555556358a → jne 0x5555555632ad
[#3] 0x7ffff7f91f10 → add BYTE PTR [rax+0x7ffff7b7], al
────────────────────────────────────────────────────────────────────────────────────
gef➤
The most interesting line is this one:
0x00007ffff7c3d125 in __GI___strtok_r (s=0x55555556e670 "fooooooooo\n", delim=0x555555559004 "\n", save_ptr=0x7ffff7d86728 <olds>) at strtok_r.c:49
strtok
is call with “\n” as delimiter.
So it will search the string for the first newline character, replace it with 0x00, and return a pointer to the byte immediately after that.
Since that is already past the input given, the strtok
call serves only to strip away the trailing newline character from the password.
It will continue to create a lot of watchpoint hits, so it’s best to disable the watchpoint, let strtok
finish its work, and enable it again.
gef➤ disable 3
gef➤ finish
Run till exit from #0 0x00007ffff7c3d125 in __GI___strtok_r (s=0x55555556e670 "fooooooooo\n", delim=0x555555559004 "\n", save_ptr=0x7ffff7d86728 <olds>) at strtok_r.c:49
0x0000555555557291 in ?? ()
Value returned is $2 = 0x55555556e670 "fooooooooo"
[...]
─────────────────────────────────────────────────────────────────── code:x86:64 ────
0x555555557282 lea rsi, [rip+0x1d7b] # 0x555555559004
0x555555557289 mov rdi, rax
0x55555555728c call 0x555555557090 <strtok@plt>
→ 0x555555557291 mov rax, QWORD PTR [rbp-0x8]
0x555555557295 leave
0x555555557296 ret
0x555555557297 lea rax, [rip+0x15daa] # 0x55555556d048
0x55555555729e pop rbx
0x55555555729f push rax
───────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555557291 → mov rax, QWORD PTR [rbp-0x8]
[#1] 0x55555556358a → jne 0x5555555632ad
[#2] 0x7ffff7f91f10 → add BYTE PTR [rax+0x7ffff7b7], al
────────────────────────────────────────────────────────────────────────────────────
gef➤ enable 3
gef➤
The next time the watchpoint triggers seems more interesting:
gef➤ c
Continuing.
Hardware access (read/write) watchpoint 3: *0x55555556e670
Value = 0x6f6f6f66
0x00005555555581ba in tohtaapixohliboifuje ()
__main__:2274: DeprecationWarning: invalid escape sequence '\'
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────── registers ────
$rax : 0x66
$rbx : 0x000055555555819d → <tohtaapixohliboifuje+28> mov QWORD PTR [rbp-0x8], rax
$rcx : 0x0000555555564318 → je 0x55555556406a
$rdx : 0x0
$rsp : 0x00007fffffffd880 → 0x000055555556d048 → 0x000000000000bd69
$rbp : 0x00007fffffffd8a8 → 0x00007fffffffd948 → 0x00007fffffffd978 → 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15
$rsi : 0x1e7
$rdi : 0x0000555555558000 → <ahngashifaisuethooqu+1810> push rbx
$rip : 0x00005555555581ba → <tohtaapixohliboifuje+57> movsx rdx, al
$r8 : 0x000055555556d0d8 → 0xa2c4a414b4a63d61
$r9 : 0x00007ffff7b76b80 → 0x00007ffff7b76b80 → [loop detected]
$r10 : 0x00007ffff7d83ca0 → 0x000055555556ec90 → 0x0000000000000000
$r11 : 0x206
$r12 : 0x00005555555570c0 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdb10 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
───────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd880│+0x0000: 0x000055555556d048 → 0x000000000000bd69 ← $rsp
0x00007fffffffd888│+0x0008: 0x0000555555563b01 → jne 0x5555555648d8
0x00007fffffffd890│+0x0010: 0x000055555556e670 → "fooooooooo"
0x00007fffffffd898│+0x0018: 0x000000005556d0f0
0x00007fffffffd8a0│+0x0020: 0x000055555556eb90 → 0x48264660e7e20a93
0x00007fffffffd8a8│+0x0028: 0x00007fffffffd948 → 0x00007fffffffd978 → 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15 ← $rbp
0x00007fffffffd8b0│+0x0030: 0x000055555556358a → jne 0x5555555632ad
0x00007fffffffd8b8│+0x0038: 0x000055555556d010 → 0x000000000000bde1
─────────────────────────────────────────────────────────────────── code:x86:64 ────
0x5555555581af <tohtaapixohliboifuje+46> ror BYTE PTR [rax-0x75], 1
0x5555555581b2 <tohtaapixohliboifuje+49> rex.RB call 0x555565258300
0x5555555581b8 <tohtaapixohliboifuje+55> mov dh, 0x0
→ 0x5555555581ba <tohtaapixohliboifuje+57> movsx rdx, al
0x5555555581be <tohtaapixohliboifuje+61> mov rax, QWORD PTR [rbp-0x8]
0x5555555581c2 <tohtaapixohliboifuje+65> add rax, rdx
0x5555555581c5 <tohtaapixohliboifuje+68> mov edx, DWORD PTR [rbp-0xc]
0x5555555581c8 <tohtaapixohliboifuje+71> movsxd rcx, edx
0x5555555581cb <tohtaapixohliboifuje+74> mov rdx, QWORD PTR [rbp-0x18]
───────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555555581ba → tohtaapixohliboifuje()
[#1] 0x55555556358a → jne 0x5555555632ad
[#2] 0x55555556d010 → loope 0x55555556cfcf
[#3] 0x55555556e670 → outs dx, WORD PTR ds:[rsi]
────────────────────────────────────────────────────────────────────────────────────
gef➤
The disassembly here is misleading:
gef tried to disassemble the instruction flow backward from the current position (0x5555555581ba
). Disassembling backward sometimes goes wrong on architectures with variable instruction length (like x86-64).
This becomes obvious once we start disassembling the whole function:
gef➤ x/30i 0x555555558188
0x555555558188 <tohtaapixohliboifuje+7>: mov rbp,rsp
0x55555555818b <tohtaapixohliboifuje+10>: sub rsp,0x20
0x55555555818f <tohtaapixohliboifuje+14>: mov QWORD PTR [rbp-0x18],rdi
0x555555558193 <tohtaapixohliboifuje+18>: mov eax,0x0
0x555555558198 <tohtaapixohliboifuje+23>: call 0x555555557297
0x55555555819d <tohtaapixohliboifuje+28>: mov QWORD PTR [rbp-0x8],rax
0x5555555581a1 <tohtaapixohliboifuje+32>: mov DWORD PTR [rbp-0xc],0x0
0x5555555581a8 <tohtaapixohliboifuje+39>: jmp 0x5555555581db <tohtaapixohliboifuje+90>
0x5555555581aa <tohtaapixohliboifuje+41>: mov eax,DWORD PTR [rbp-0xc]
0x5555555581ad <tohtaapixohliboifuje+44>: movsxd rdx,eax
0x5555555581b0 <tohtaapixohliboifuje+47>: mov rax,QWORD PTR [rbp-0x18]
0x5555555581b4 <tohtaapixohliboifuje+51>: add rax,rdx
0x5555555581b7 <tohtaapixohliboifuje+54>: movzx eax,BYTE PTR [rax]
=> 0x5555555581ba <tohtaapixohliboifuje+57>: movsx rdx,al
0x5555555581be <tohtaapixohliboifuje+61>: mov rax,QWORD PTR [rbp-0x8]
0x5555555581c2 <tohtaapixohliboifuje+65>: add rax,rdx
0x5555555581c5 <tohtaapixohliboifuje+68>: mov edx,DWORD PTR [rbp-0xc]
0x5555555581c8 <tohtaapixohliboifuje+71>: movsxd rcx,edx
0x5555555581cb <tohtaapixohliboifuje+74>: mov rdx,QWORD PTR [rbp-0x18]
0x5555555581cf <tohtaapixohliboifuje+78>: add rdx,rcx
0x5555555581d2 <tohtaapixohliboifuje+81>: movzx eax,BYTE PTR [rax]
0x5555555581d5 <tohtaapixohliboifuje+84>: mov BYTE PTR [rdx],al
0x5555555581d7 <tohtaapixohliboifuje+86>: add DWORD PTR [rbp-0xc],0x1
0x5555555581db <tohtaapixohliboifuje+90>: cmp DWORD PTR [rbp-0xc],0xff
0x5555555581e2 <tohtaapixohliboifuje+97>: jle 0x5555555581aa <tohtaapixohliboifuje+41>
0x5555555581e4 <tohtaapixohliboifuje+99>: nop
0x5555555581e5 <tohtaapixohliboifuje+100>: leave
0x5555555581e6 <tohtaapixohliboifuje+101>: ret
0x5555555581e7 <tohtaapixohliboifuje+102>: lea rax,[rip+0x14f3a] # 0x55555556d128
0x5555555581ee <tohtaapixohliboifuje+109>: pop rbx
So the instruction that triggered the watchpoint was:
0x5555555581b7 <tohtaapixohliboifuje+54>: movzx eax,BYTE PTR [rax]
When looking at the function, the loop between 0x5555555581aa
and 0x5555555581e2
sticks out.
What is happening here?
When you iterate though it a few times and check the values, you will find out this:
- It’s a for loop with a loop counter at
[rbp-0xc]
, which was initialised to 0 at0x5555555581a1
. The counter is incremented (at0x5555555581d7
) until it reaches0xff
(0x5555555581db
) - A pointer to the password buffer can be found at
[rbp-0x18]
- A pointer to an array of values is stored at
[rbp-0x8]
- In each iteration, a byte from the password buffer is fetched (
0x5555555581b7
, the operation that triggered the watchpoint) - This byte is used as an index into the value table (
0x5555555581d2
) - That value is written back to the password buffer, replacing the original password byte (
0x5555555581d5
) - This happens for all 256 bytes of the password buffer
So what does the array at [rbp-0x8]
look like? It’s a table with 256 values, from 0x00
to 0xff
, randomly arranged.
gef➤ x/a $rbp-8
0x7fffffffd8a0: 0x55555556eb90
gef➤ x/256bx 0x55555556eb90
0x55555556eb90: 0x93 0x0a 0xe2 0xe7 0x60 0x46 0x26 0x48
0x55555556eb98: 0xd8 0x68 0x91 0x8a 0x71 0xac 0x47 0x0e
0x55555556eba0: 0xcd 0x78 0x53 0x8e 0xda 0x9c 0x43 0xc3
0x55555556eba8: 0xc1 0xb6 0xdc 0x17 0x54 0xa6 0xc5 0x1a
0x55555556ebb0: 0x1b 0xe0 0x87 0xa2 0x06 0x30 0xcc 0xdb
0x55555556ebb8: 0x9f 0xd4 0x2e 0xbf 0x7c 0xce 0x3a 0xe3
0x55555556ebc0: 0x8b 0xa7 0x92 0xed 0x36 0x28 0x51 0x23
0x55555556ebc8: 0x29 0x04 0x63 0x45 0xf1 0x81 0xb0 0xbe
0x55555556ebd0: 0xbd 0x90 0xd2 0xe8 0x88 0x70 0x16 0xa5
0x55555556ebd8: 0x67 0x31 0x08 0xb2 0x7b 0x99 0x39 0xfa
0x55555556ebe0: 0xf2 0x65 0x6b 0x5b 0xf6 0xe4 0xe6 0x8c
0x55555556ebe8: 0xb1 0x25 0x33 0x75 0xc6 0x44 0xc2 0xc7
0x55555556ebf0: 0xc0 0x8f 0x4e 0xc4 0xca 0xfd 0xbc 0xb3
0x55555556ebf8: 0xff 0x0b 0xa8 0x57 0xf4 0xe9 0xde 0x6f
0x55555556ec00: 0x49 0x5d 0x0f 0x83 0x62 0x21 0x59 0xe5
0x55555556ec08: 0x34 0xea 0x86 0x11 0x3c 0x41 0xf9 0x00
0x55555556ec10: 0x6a 0x5c 0x52 0x5a 0x1c 0x95 0x4a 0x24
0x55555556ec18: 0xd3 0xba 0x9b 0x94 0x1e 0x2d 0xa4 0x1d
0x55555556ec20: 0x96 0xa0 0x15 0xf7 0x20 0x09 0x77 0xdf
0x55555556ec28: 0xf0 0x7e 0xb9 0x12 0x4d 0x2f 0xef 0x58
0x55555556ec30: 0xaf 0x2b 0xa3 0x6c 0x3b 0x3e 0x2c 0x64
0x55555556ec38: 0x38 0x4f 0xdd 0xd0 0x5f 0xec 0x4b 0x89
0x55555556ec40: 0x7d 0xee 0xfc 0xcb 0xb7 0x01 0x50 0xb4
0x55555556ec48: 0x3d 0x76 0xf8 0x97 0x03 0x4c 0x6e 0x79
0x55555556ec50: 0x27 0xbb 0x14 0xfe 0x0d 0x72 0xe1 0x80
0x55555556ec58: 0x05 0x0c 0x55 0xfb 0x18 0x9d 0xab 0x2a
0x55555556ec60: 0xa9 0xd9 0x19 0xd7 0xb8 0x6d 0x9a 0xd1
0x55555556ec68: 0x85 0x40 0x69 0x73 0xd5 0xeb 0x7f 0xad
0x55555556ec70: 0x61 0xd6 0x9e 0x3f 0x02 0x07 0x22 0xa1
0x55555556ec78: 0x84 0x1f 0x8d 0xf3 0xc8 0x82 0xb5 0x42
0x55555556ec80: 0x5e 0x66 0x35 0x10 0xaa 0xae 0xcf 0x74
0x55555556ec88: 0x13 0x7a 0x56 0x98 0xf5 0x32 0x37 0xc9
gef➤
So the function substitutes each value in the original password with another value from the table, like a simple monoalphabetic cipher. Better save the substitution table for later use.
gef➤ dump memory tabledump.bin 0x55555556eb90 0x55555556ec90
gef➤
As the password is encrypted in place (and not written to another buffer), we can continue to use the memory watchpoint.
But just as with strtok
, we should disable it until the function is done, because it will be triggered more than 500 times during the encryption process.
gef➤ disable 3
gef➤ finish
Run till exit from #0 0x00005555555581d7 in tohtaapixohliboifuje ()
0x000055555556358a in ?? ()
[...]
gef➤ enable 3
gef➤ c
Continuing.
Hardware access (read/write) watchpoint 3: *0x55555556e670
Value = 0x6f6f6fbc
0x0000555555558216 in tohtaapixohliboifuje ()
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x000055555556e670 → 0x6f6f6f6f6f6f6fbc
$rbx : 0x00005555555581e7 → <tohtaapixohliboifuje+102> push rbp
$rcx : 0xbc
$rdx : 0x0
$rsp : 0x00007fffffffd898 → 0x00007fffffffd938 → 0x00007fffffffd978 → 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15
$rbp : 0x00007fffffffd898 → 0x00007fffffffd938 → 0x00007fffffffd978 → 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15
$rsi : 0x000055555556eca0 → 0x16f6e888e8996b99
$rdi : 0x000055555556e670 → 0x6f6f6f6f6f6f6fbc
$rip : 0x0000555555558216 → <tohtaapixohliboifuje+149> mov eax, DWORD PTR [rbp-0x4]
$r8 : 0x000055555556d110 → 0x73cbe86189951598
$r9 : 0x00007ffff7b76b80 → 0x00007ffff7b76b80 → [loop detected]
$r10 : 0x00007ffff7d83ca0 → 0x000055555556eda0 → 0x0000000000000000
$r11 : 0x216
$r12 : 0x00005555555570c0 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdb10 → 0x0000000000000001
$r14 : 0x0
$r15 : 0x0
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
───────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd898│+0x0000: 0x00007fffffffd938 → 0x00007fffffffd978 → 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15 ← $rsp, $rbp
0x00007fffffffd8a0│+0x0008: 0x000055555556358a → jne 0x5555555632ad
0x00007fffffffd8a8│+0x0010: 0x000055555556d080 → 0x000000000000b5f1
0x00007fffffffd8b0│+0x0018: 0x000055555556d0b8 → 0x000000000000ae79
0x00007fffffffd8b8│+0x0020: 0x000055555556d010 → 0x000000000000bde1
0x00007fffffffd8c0│+0x0028: 0x000055555556e670 → 0x6f6f6f6f6f6f6fbc
0x00007fffffffd8c8│+0x0030: 0x000055555556eca0 → 0x16f6e888e8996b99
0x00007fffffffd8d0│+0x0038: 0x00007fffffffda18 → 0x00007fffffffda30 → 0x00005555555585b0 → <__libc_csu_init+0> push r15
─────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x55555555820b <tohtaapixohliboifuje+138> ror BYTE PTR [rax-0x75], 1
0x55555555820e <tohtaapixohliboifuje+141> rex.RB call 0x55556525835c
0x555555558214 <tohtaapixohliboifuje+147> mov dh, 0x8
→ 0x555555558216 <tohtaapixohliboifuje+149> mov eax, DWORD PTR [rbp-0x4]
0x555555558219 <tohtaapixohliboifuje+152> movsxd rdx, eax
0x55555555821c <tohtaapixohliboifuje+155> mov rax, QWORD PTR [rbp-0x20]
0x555555558220 <tohtaapixohliboifuje+159> add rax, rdx
0x555555558223 <tohtaapixohliboifuje+162> movzx eax, BYTE PTR [rax]
0x555555558226 <tohtaapixohliboifuje+165> xor eax, ecx
───────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555558216 → tohtaapixohliboifuje()
[#1] 0x55555556358a → jne 0x5555555632ad
[#2] 0x55555556d080 → icebp
[#3] 0x55555556d0b8 → jns 0x55555556d068
[#4] 0x55555556d010 → loope 0x55555556cfcf
[#5] 0x55555556e670 → mov esp, 0x6f6f6f6f
[#6] 0x55555556eca0 → cdq
[#7] 0x7fffffffda18 → xor dl, bl
[#8] 0x7ffff7b76b80 → sub BYTE PTR [rbx-0x49], 0xf7
[#9] 0x55555556d110 → cwde
────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
Here’s the next time the (now encrypted) password is used. And again, the backward disassembling fails. This is the whole function:
gef➤ x/30i 0x5555555581e7
0x5555555581e7 <tohtaapixohliboifuje+102>: push rbp
0x5555555581e8 <tohtaapixohliboifuje+103>: mov rbp,rsp
0x5555555581eb <tohtaapixohliboifuje+106>: mov QWORD PTR [rbp-0x18],rdi
0x5555555581ef <tohtaapixohliboifuje+110>: mov QWORD PTR [rbp-0x20],rsi
0x5555555581f3 <tohtaapixohliboifuje+114>: mov DWORD PTR [rbp-0x24],edx
0x5555555581f6 <tohtaapixohliboifuje+117>: mov DWORD PTR [rbp-0x8],0x0
0x5555555581fd <tohtaapixohliboifuje+124>: mov DWORD PTR [rbp-0x4],0x0
0x555555558204 <tohtaapixohliboifuje+131>: jmp 0x555555558232 <tohtaapixohliboifuje+177>
0x555555558206 <tohtaapixohliboifuje+133>: mov eax,DWORD PTR [rbp-0x4]
0x555555558209 <tohtaapixohliboifuje+136>: movsxd rdx,eax
0x55555555820c <tohtaapixohliboifuje+139>: mov rax,QWORD PTR [rbp-0x18]
0x555555558210 <tohtaapixohliboifuje+143>: add rax,rdx
0x555555558213 <tohtaapixohliboifuje+146>: movzx ecx,BYTE PTR [rax]
=> 0x555555558216 <tohtaapixohliboifuje+149>: mov eax,DWORD PTR [rbp-0x4]
0x555555558219 <tohtaapixohliboifuje+152>: movsxd rdx,eax
0x55555555821c <tohtaapixohliboifuje+155>: mov rax,QWORD PTR [rbp-0x20]
0x555555558220 <tohtaapixohliboifuje+159>: add rax,rdx
0x555555558223 <tohtaapixohliboifuje+162>: movzx eax,BYTE PTR [rax]
0x555555558226 <tohtaapixohliboifuje+165>: xor eax,ecx
0x555555558228 <tohtaapixohliboifuje+167>: movsx eax,al
0x55555555822b <tohtaapixohliboifuje+170>: or DWORD PTR [rbp-0x8],eax
0x55555555822e <tohtaapixohliboifuje+173>: add DWORD PTR [rbp-0x4],0x1
0x555555558232 <tohtaapixohliboifuje+177>: mov eax,DWORD PTR [rbp-0x4]
0x555555558235 <tohtaapixohliboifuje+180>: cmp DWORD PTR [rbp-0x24],eax
0x555555558238 <tohtaapixohliboifuje+183>: ja 0x555555558206 <tohtaapixohliboifuje+133>
0x55555555823a <tohtaapixohliboifuje+185>: mov eax,DWORD PTR [rbp-0x8]
0x55555555823d <tohtaapixohliboifuje+188>: pop rbp
0x55555555823e <tohtaapixohliboifuje+189>: ret
0x55555555823f <tohtaapixohliboifuje+190>: lea rax,[rip+0x14f1a] # 0x55555556d160
0x555555558246 <tohtaapixohliboifuje+197>: pop rbx
gef➤
And again the access (at 0x555555558213
) happens inside a loop (0x555555558206
to 0x555555558238
).
By looking at the values and single stepping, you can analyse it again:
- The loop counter is at
[rbp-0x4]
this time - A pointer to the start of the password buffer is at
[rbp-0x18]
- There is some other byte string at
[rbp-0x20]
On each iteration, a byte from the password buffer and the other byte string are read (at 0x555555558213
and 0x555555558223
, respectively).
The values are then xor’rd (at 0x555555558226
) and the result is or’rd to [rbp-0x8]
(at 0x55555555822b
).
[rbp-0x8]
itself was initialised to 0 (at 0x5555555581f6
).
That means that if both arrays of values are identical, at the end [rbp-0x8]
will be 0x00
.
In any other case, it will be something else.
Well, if that isn’t most interesting!
Lets take a close look at this array (and save it for later):
gef➤ x/a $rbp-0x20
0x7fffffffd878: 0x55555556eca0
gef➤ x/256bx 0x55555556eca0
0x55555556eca0: 0x99 0x6b 0x99 0xe8 0x88 0xe8 0xf6 0x16
0x55555556eca8: 0x11 0xea 0x6f 0x21 0xc7 0xa8 0x21 0x83
0x55555556ecb0: 0x62 0xc7 0xff 0x8f 0x59 0xfd 0xc7 0x4e
0x55555556ecb8: 0xfd 0x8f 0x62 0xfd 0xde 0xc7 0x62 0xff
0x55555556ecc0: 0xfd 0xc7 0xbc 0x0b 0xde 0x8f 0xf4 0xc7
0x55555556ecc8: 0x4e 0x6f 0x83 0x83 0x41 0x93 0x93 0x93
0x55555556ecd0: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ecd8: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ece0: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ece8: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ecf0: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ecf8: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed00: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed08: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed10: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed18: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed20: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed28: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed30: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed38: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed40: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed48: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed50: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed58: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed60: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed68: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed70: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed78: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed80: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed88: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed90: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
0x55555556ed98: 0x93 0x93 0x93 0x93 0x93 0x93 0x93 0x93
gef➤ dump memory flagdump.bin 0x55555556eca0 0x55555556ed98
gef➤
This is definitely not a plain flag. But remember: the password was encrypted with the substitution cipher. So the encrypted password will be compared to an encrypted flag.
And as we already have the substitution table saved, we just need some python script to undo the substitution of the flag. Here we go:
#!/usr/bin/env python
table = [
0x93, 0x0a, 0xe2, 0xe7, 0x60, 0x46, 0x26, 0x48, 0xd8, 0x68, 0x91, 0x8a,
0x71, 0xac, 0x47, 0x0e, 0xcd, 0x78, 0x53, 0x8e, 0xda, 0x9c, 0x43, 0xc3,
0xc1, 0xb6, 0xdc, 0x17, 0x54, 0xa6, 0xc5, 0x1a, 0x1b, 0xe0, 0x87, 0xa2,
0x06, 0x30, 0xcc, 0xdb, 0x9f, 0xd4, 0x2e, 0xbf, 0x7c, 0xce, 0x3a, 0xe3,
0x8b, 0xa7, 0x92, 0xed, 0x36, 0x28, 0x51, 0x23, 0x29, 0x04, 0x63, 0x45,
0xf1, 0x81, 0xb0, 0xbe, 0xbd, 0x90, 0xd2, 0xe8, 0x88, 0x70, 0x16, 0xa5,
0x67, 0x31, 0x08, 0xb2, 0x7b, 0x99, 0x39, 0xfa, 0xf2, 0x65, 0x6b, 0x5b,
0xf6, 0xe4, 0xe6, 0x8c, 0xb1, 0x25, 0x33, 0x75, 0xc6, 0x44, 0xc2, 0xc7,
0xc0, 0x8f, 0x4e, 0xc4, 0xca, 0xfd, 0xbc, 0xb3, 0xff, 0x0b, 0xa8, 0x57,
0xf4, 0xe9, 0xde, 0x6f, 0x49, 0x5d, 0x0f, 0x83, 0x62, 0x21, 0x59, 0xe5,
0x34, 0xea, 0x86, 0x11, 0x3c, 0x41, 0xf9, 0x00, 0x6a, 0x5c, 0x52, 0x5a,
0x1c, 0x95, 0x4a, 0x24, 0xd3, 0xba, 0x9b, 0x94, 0x1e, 0x2d, 0xa4, 0x1d,
0x96, 0xa0, 0x15, 0xf7, 0x20, 0x09, 0x77, 0xdf, 0xf0, 0x7e, 0xb9, 0x12,
0x4d, 0x2f, 0xef, 0x58, 0xaf, 0x2b, 0xa3, 0x6c, 0x3b, 0x3e, 0x2c, 0x64,
0x38, 0x4f, 0xdd, 0xd0, 0x5f, 0xec, 0x4b, 0x89, 0x7d, 0xee, 0xfc, 0xcb,
0xb7, 0x01, 0x50, 0xb4, 0x3d, 0x76, 0xf8, 0x97, 0x03, 0x4c, 0x6e, 0x79,
0x27, 0xbb, 0x14, 0xfe, 0x0d, 0x72, 0xe1, 0x80, 0x05, 0x0c, 0x55, 0xfb,
0x18, 0x9d, 0xab, 0x2a, 0xa9, 0xd9, 0x19, 0xd7, 0xb8, 0x6d, 0x9a, 0xd1,
0x85, 0x40, 0x69, 0x73, 0xd5, 0xeb, 0x7f, 0xad, 0x61, 0xd6, 0x9e, 0x3f,
0x02, 0x07, 0x22, 0xa1, 0x84, 0x1f, 0x8d, 0xf3, 0xc8, 0x82, 0xb5, 0x42,
0x5e, 0x66, 0x35, 0x10, 0xaa, 0xae, 0xcf, 0x74, 0x13, 0x7a, 0x56, 0x98,
0xf5, 0x32, 0x37, 0xc9 ]
flag = [
0x99, 0x6b, 0x99, 0xe8, 0x88, 0xe8, 0xf6, 0x16, 0x11, 0xea, 0x6f, 0x21,
0xc7, 0xa8, 0x21, 0x83, 0x62, 0xc7, 0xff, 0x8f, 0x59, 0xfd, 0xc7, 0x4e,
0xfd, 0x8f, 0x62, 0xfd, 0xde, 0xc7, 0x62, 0xff, 0xfd, 0xc7, 0xbc, 0x0b,
0xde, 0x8f, 0xf4, 0xc7, 0x4e, 0x6f, 0x83, 0x83, 0x41, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93,
0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93, 0x93 ]
plain = ""
for c in flag:
p = chr(table.index(c))
plain += p
print(plain)
I’ll leave you the honour of running it yourself (SPOILER: it prints the flag!).
In the next post, I will explain how to undo the obfuscation and recover the original code.