POSTS
MRMCDCTF2019: Carbonara
Solution to Carbonara (medium - hard) from MRMCDCTF 2019
Carbonara is another one of my challenges for MRMCDCTF 2019.
Just like KonradVC, it employs an anti-reversing trick,
but of a completely different kind:
the code is chopped into small pieces, each one instruction long.
These pieces are saved in random order, and connected by jmp
s.
The resulting code executes the instructions in the right order, but looks like a total mess.
The challenge
When you try to decompile main
with Ghidra, you get something like this:
void main(char *pcParm1,undefined *puParm2,ulong uParm3)
{
byte bVar1;
code *pcVar2;
uint uVar3;
undefined8 *in_RAX;
ulong uVar4;
char *pcVar5;
uint uVar6;
ulong extraout_RDX;
ulong extraout_RDX_00;
ulong extraout_RDX_01;
[ some more variables ]
puVar9 = (undefined8 *)&stack0xfffffffffffffff8;
if ((!in_CF) &&
(puVar9 = (undefined8 *)&stack0xfffffffffffffff8, puVar8 = &stack0xfffffffffffffff8, in_CF))
goto LAB_005031e5;
LAB_00503204:
unaff_RBP = puVar9;
if (!in_PF) goto LAB_00503259;
unaff_RBP = puVar9;
if (in_PF) goto LAB_00503259;
do {
*(undefined *)((long)unaff_RBP + -1) = 0;
if (in_CF) goto LAB_005032c9;
if (!in_CF) goto LAB_005032c9;
LAB_0050321b:
in_RAX = (undefined8 *)(uParm3 & 0xffffffff);
if ((!in_PF) || (in_PF)) {
LAB_0050301e:
*(byte *)((long)unaff_RBP + -1) = *(byte *)((long)unaff_RBP + -1) | (byte)in_RAX;
__s = unaff_RBP + -1;
in_CF = 0xfffffffe < *(uint *)__s;
in_OF = SCARRY4(*(uint *)__s,1);
*(uint *)__s = *(uint *)__s + 1;
in_ZF = *(uint *)__s == 0;
if (in_CF) goto LAB_00503132;
if (!in_CF) goto LAB_00503132;
goto LAB_0050302e;
}
[ a lot more of this ]
It’s obvious something is not right here.
The disassembler shows how the decompiler got confused:
**************************************************************
* FUNCTION *
**************************************************************
undefined FUN_005031e0()
undefined AL:1 <RETURN>
undefined1 Stack[-0x9]:1 local_9 XREF[1]: 0050320b(W)
FUN_005031e0 XREF[1]: thunk_FUN_005031e0:00503000(T),
thunk_FUN_005031e0:00503000(j)
005031e0 PUSH RBP
005031e1 JC LAB_00503204
005031e3 JNC LAB_00503204
LAB_005031e5 XREF[1]: 005031d1(j)
005031e5 LEA RSI,[DAT_00101948] = 0Ah
005031ec JNO LAB_00503233
005031ee JO LAB_00503233
LAB_005031f0 XREF[2]: 00503273(j), 00503279(j)
005031f0 MOV EAX,dword ptr [RBP + -0x8]
005031f3 JMP LAB_0050309a
LAB_005031f8 XREF[2]: 00503006(j), 0050300c(j)
005031f8 RET
005031f9 ?? E9h
005031fa ?? 5Ch \
005031fb ?? FEh
005031fc ?? FFh
005031fd ?? FFh
LAB_005031fe XREF[2]: 00503062(j), 00503068(j)
005031fe MOV EDX,EAX
00503200 JNP LAB_0050327f
00503202 JP LAB_0050327f
LAB_00503204 XREF[2]: 005031e1(j), 005031e3(j)
00503204 MOV RBP,RSP
00503207 JNP LAB_00503259
00503209 JP LAB_00503259
LAB_0050320b XREF[1]: 00503019(j)
0050320b MOV byte ptr [RBP + local_9],0x0
0050320f JC LAB_005032c9
00503215 JNC LAB_005032c9
LAB_0050321b XREF[2]: 00503155(j), 0050315b(j)
0050321b MOV EAX,EDX
0050321d JNP LAB_0050306e
00503223 JP LAB_0050306e
[ a lot more of this ]
You can see the pattern here: there is always one instruction that does something, followed by two jumps: one conditional jump, and one other conditional jump for the exactly opposite condition. So one of these jumps always gets followed, no matter what. And they both always lead to the same destination anyway.
So these jumps do not actually do anything: they just make the control flow jump around without ever doing anything meaningful. And, of course, they make the disassembly painful to read and confuse the decompiler.
The solution
Actually, this challenge can be solved in two ways: one intended, and one painful:
Solution 1: The intended one
It’s difficult to follow the code around. But maybe it’s easier to follow the data (in our case: the password/flag)?
The password gets read with fgets
:
user@host:$ ltrace ./carbonara
puts("The password is the flag. What i"...The password is the flag. What is the password?
) = 48
fgets(foo
"foo\n", 128, 0x7f6eb80bba00) = 0x7ffd6b3b2050
strtok("foo\n", "\n") = "foo"
strlen("foo") = 3
puts("Wrong. Try again."Wrong. Try again.
) = 18
+++ exited (status 255) +++
So we can set a breakpoint at fgets
, let the function finish, but set a watchpoint
(gdb’s term for a memory breakpoint) on the buffer where the password is set.
Then the program will be interrupted at any further access to that buffer.
user@host:$ gdb ./carbonara
GNU gdb (Ubuntu 8.2.91.20190405-0ubuntu3) 8.2.91.20190405-git
[...]
(No debugging symbols found in ./carbonara)
gef➤ b fgets
Haltepunkt 1 at 0x1620
gef➤ r
Starting program: carbonara
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
The password is the flag. What is the password?
Breakpoint 1, _IO_fgets (buf=0x7fffffffd990 "z\357\377\377\377\177", n=0x80, fp=0x7ffff7d83a00 <_IO_2_1_stdin_>) at iofgets.c:37
37 iofgets.c: Datei oder Verzeichnis nicht gefunden.
__main__:2274: DeprecationWarning: invalid escape sequence '\ç'
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffd990 → 0x00007fffffffef7a → "carbonara"
$rbx : 0x0
$rcx : 0x00007ffff7cac024 → 0x5477fffff0003d48 ("H="?)
$rdx : 0x00007ffff7d83a00 → 0x00000000fbad2088
$rsp : 0x00007fffffffd958 → 0x00005555559571c1 → je 0x5555559571ca
$rbp : 0x00007fffffffda20 → 0x0000555555555890 → <__libc_csu_init+0> push r15
$rsi : 0x80
$rdi : 0x00007fffffffd990 → 0x00007fffffffef7a → "carbonara"
$rip : 0x00007ffff7c21100 → <fgets+0> test esi, esi
$r8 : 0x0
$r9 : 0x5d
$r10 : 0x00007ffff7d83ca0 → 0x0000555555958660 → 0x0000000000000000
$r11 : 0x246
$r12 : 0x0000555555555650 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdb00 → 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 ────
0x00007fffffffd958│+0x0000: 0x00005555559571c1 → je 0x5555559571ca ← $rsp
0x00007fffffffd960│+0x0008: 0x88968586858f948f
0x00007fffffffd968│+0x0010: 0xb6a7aaa9a3b2b5bd
0x00007fffffffd970│+0x0018: 0xa1a7a6b1a5a1abb6
0x00007fffffffd978│+0x0020: 0xa3a7b4a4a1b4b1a8
0x00007fffffffd980│+0x0028: 0x0042bfb6b5a3a8ad
0x00007fffffffd988│+0x0030: 0x00007ffff7b7fbfb → <__pthread_initialize_minimal+875> mov rax, QWORD PTR [rsp+0xc8]
0x00007fffffffd990│+0x0038: 0x00007fffffffef7a → "carbonara" ← $rax, $rdi
────────────────────────────────────────────────────────── 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
──────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7c21100 → _IO_fgets(buf=0x7fffffffd990 "z\357\377\377\377\177", n=0x80, fp=0x7ffff7d83a00 <_IO_2_1_stdin_>)
[#1] 0x5555559571c1 → je 0x5555559571ca
───────────────────────────────────────────────────────────────────────────
gef➤ fin
Run till exit from #0 _IO_fgets (buf=0x7fffffffd990 "z\357\377\377\377\177", n=0x80, fp=0x7ffff7d83a00 <_IO_2_1_stdin_>) at iofgets.c:37
foo
0x00005555559571c1 in ?? ()
Value returned is $1 = 0x7fffffffd990 "foo\n"
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffd990 → 0x00007f000a6f6f66 ("foo"?)
$rbx : 0x0
$rcx : 0xfbad2288
$rdx : 0x00007fffffffd990 → 0x00007f000a6f6f66 ("foo"?)
$rsp : 0x00007fffffffd960 → 0x88968586858f948f
$rbp : 0x00007fffffffda20 → 0x0000555555555890 → <__libc_csu_init+0> push r15
$rsi : 0x00007ffff7d86590 → 0x0000000000000000
$rdi : 0x00007fffffffd991 → 0x7000007f000a6f6f ("oo"?)
$rip : 0x00005555559571c1 → je 0x5555559571ca
$r8 : 0x0000555555958674 → 0x0000000000000000
$r9 : 0x00007ffff7b76b80 → 0x00007ffff7b76b80 → [loop detected]
$r10 : 0x00007ffff7d83ca0 → 0x0000555555958a70 → 0x0000000000000000
$r11 : 0x246
$r12 : 0x0000555555555650 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdb00 → 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 ────
0x00007fffffffd960│+0x0000: 0x88968586858f948f ← $rsp
0x00007fffffffd968│+0x0008: 0xb6a7aaa9a3b2b5bd
0x00007fffffffd970│+0x0010: 0xa1a7a6b1a5a1abb6
0x00007fffffffd978│+0x0018: 0xa3a7b4a4a1b4b1a8
0x00007fffffffd980│+0x0020: 0x0042bfb6b5a3a8ad
0x00007fffffffd988│+0x0028: 0x00007ffff7b7fbfb → <__pthread_initialize_minimal+875> mov rax, QWORD PTR [rsp+0xc8]
0x00007fffffffd990│+0x0030: 0x00007f000a6f6f66 ("foo"?) ← $rax, $rdx
0x00007fffffffd998│+0x0038: 0x00007ffff7b80c70 → <__wait_lookup_done+0> push r15
────────────────────────────────────────────────────────── code:x86:64 ────
0x5555559571b0 je 0x55555595709e
0x5555559571b6 jne 0x55555595709e
0x5555559571bc call 0x555555555620 <fgets@plt>
→ 0x5555559571c1 je 0x5555559571ca TAKEN [Reason: Z]
↳ 0x5555559571ca lea rax, [rbp-0x90]
0x5555559571d1 jmp 0x5555559571e5
0x5555559571d3 movzx eax, BYTE PTR [rbp+rax*1-0xc0]
0x5555559571db jmp 0x555555957153
0x5555559571e0 push rbp
0x5555559571e1 jb 0x555555957204
──────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555559571c1 → je 0x5555559571ca
───────────────────────────────────────────────────────────────────────────
gef➤ awatch *0x7fffffffd990
Hardware access (read/write) watchpoint 2: *0x7fffffffd990
What did I do here?
First, I set the breakpoint on fgets
before the program is started (b fgets
, short for breakpoint).
Then I run the program (r
, short for run).
When fgets
is called and the breakpoint hits, I let the function finish (fin
) and enter some password (here ‘foo’).
But the gef script I use gdb with is also so nice to tell me where the destination buffer of fgets
is located:
at 0x7fffffffd990
Run till exit from #0 _IO_fgets (buf=0x7fffffffd990 "z\357\377\377\377\177", n=0x80, fp=0x7ffff7d83a00 <_IO_2_1_stdin_>) at iofgets.c:37
Then I set an access breakpoint on it (awatch *0x7fffffffd990
).
So now the program will be stopped at every read or write access to the memory at 0x7fffffffd990
.
This happens first inside strtok
:
gef➤ c
Continuing.
Hardware access (read/write) watchpoint 2: *0x7fffffffd990
Value = 0xa6f6f66
0x00007ffff7c3d125 in __GI___strtok_r (s=0x7fffffffd990 "foo\n", delim=0x555555555948 "\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 : 0x00007fffffffd990 → 0x00007f000a6f6f66 ("foo"?)
$rbx : 0x00007fffffffd990 → 0x00007f000a6f6f66 ("foo"?)
$rcx : 0xfbad2288
$rdx : 0x00007ffff7d86728 → 0x0000000000000000
$rsp : 0x00007fffffffd940 → 0x0000000000000000
$rbp : 0x00007ffff7d86728 → 0x0000000000000000
$rsi : 0x0000555555555948 → or al, BYTE PTR [rax]
$rdi : 0x00007fffffffd990 → 0x00007f000a6f6f66 ("foo"?)
$rip : 0x00007ffff7c3d125 → <strtok_r+21> je 0x7ffff7c3d160 <__GI___strtok_r+80>
$r8 : 0x0000555555958674 → 0x0000000000000000
$r9 : 0x00007ffff7b76b80 → 0x00007ffff7b76b80 → [loop detected]
$r10 : 0x00007ffff7d83ca0 → 0x0000555555958a70 → 0x0000000000000000
$r11 : 0x246
$r12 : 0x0000555555555948 → or al, BYTE PTR [rax]
$r13 : 0x00007fffffffdb00 → 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 ────
0x00007fffffffd940│+0x0000: 0x0000000000000000 ← $rsp
0x00007fffffffd948│+0x0008: 0x00007fffffffda20 → 0x0000555555555890 → <__libc_csu_init+0> push r15
0x00007fffffffd950│+0x0010: 0x0000555555555650 → <_start+0> xor ebp, ebp
0x00007fffffffd958│+0x0018: 0x00005555559570fa → je 0x5555559570bf
0x00007fffffffd960│+0x0020: 0x88968586858f948f
0x00007fffffffd968│+0x0028: 0xb6a7aaa9a3b2b5bd
0x00007fffffffd970│+0x0030: 0xa1a7a6b1a5a1abb6
0x00007fffffffd978│+0x0038: 0xa3a7b4a4a1b4b1a8
────────────────────────────────────────────────────────── 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=0x7fffffffd990 "foo\n", delim=0x555555555948 "\n", save_ptr=0x7ffff7d86728 <olds>)
[#1] 0x5555559570fa → je 0x5555559570bf
───────────────────────────────────────────────────────────────────────────
We already know strtok
is called with the password from the ltrace
output above, so no surprise here:
strtok
here just replaces the trailing newline (’\n’) with 0.
But the breakpoint will trigger multiple times in strtok
(and strspn
, which is called by strtok
), so it’s best to temporally disable the breakpoint until the function has finished, than re-enable it again (the ‘2’ is the breakpoint number):
gef➤ disable 2
gef➤ finish
Run till exit from #0 0x00007ffff7c3d132 in __GI___strtok_r (s=0x7fffffffd990 "foo\n", delim=0x555555555948 "\n", save_ptr=0x7ffff7d86728 <olds>) at strtok_r.c:56
0x00005555559570fa in ?? ()
Value returned is $3 = 0x7fffffffd990 "foo"
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────── registers ────
$rax : 0x00007fffffffd990 → 0x00007f00006f6f66 ("foo"?)
$rbx : 0x0
$rcx : 0x3
$rdx : 0x00007fffffffd990 → 0x00007f00006f6f66 ("foo"?)
$rsp : 0x00007fffffffd960 → 0x88968586858f948f
$rbp : 0x00007fffffffda20 → 0x0000555555555890 → <__libc_csu_init+0> push r15
$rsi : 0x1
$rdi : 0x00007fffffffd990 → 0x00007f00006f6f66 ("foo"?)
$rip : 0x00005555559570fa → je 0x5555559570bf
$r8 : 0x0000555555555940 → 0x003f64726f777373 ("ssword?"?)
$r9 : 0x00007ffff7b76b80 → 0x00007ffff7b76b80 → [loop detected]
$r10 : 0x00007ffff7d83ca0 → 0x0000555555958a70 → 0x0000000000000000
$r11 : 0x246
$r12 : 0x0000555555555650 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdb00 → 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 ────
0x00007fffffffd960│+0x0000: 0x88968586858f948f ← $rsp
0x00007fffffffd968│+0x0008: 0xb6a7aaa9a3b2b5bd
0x00007fffffffd970│+0x0010: 0xa1a7a6b1a5a1abb6
0x00007fffffffd978│+0x0018: 0xa3a7b4a4a1b4b1a8
0x00007fffffffd980│+0x0020: 0x0042bfb6b5a3a8ad
0x00007fffffffd988│+0x0028: 0x00007ffff7b7fbfb → <__pthread_initialize_minimal+875> mov rax, QWORD PTR [rsp+0xc8]
0x00007fffffffd990│+0x0030: 0x00007f00006f6f66 ("foo"?) ← $rax, $rdx, $rdi
0x00007fffffffd998│+0x0038: 0x00007ffff7b80c70 → <__wait_lookup_done+0> push r15
────────────────────────────────────────────────────────── code:x86:64 ────
0x5555559570ed xchg BYTE PTR [rbp+0x7d7b8896], al
0x5555559570f3 jp 0x555555957170
0x5555559570f5 call 0x555555555630 <strtok@plt>
→ 0x5555559570fa je 0x5555559570bf NOT taken [Reason: !(Z)]
0x5555559570fc jne 0x5555559570bf
0x5555559570fe cdqe
0x555555957100 jno 0x55555595726c
0x555555957106 jo 0x55555595726c
0x55555595710c mov QWORD PTR [rbp-0xc0], rax
──────────────────────────────────────────────────────────────── trace ────
[#0] 0x5555559570fa → je 0x5555559570bf
───────────────────────────────────────────────────────────────────────────
gef➤ enable 2
gef➤
The next hit is in strlen
.
Again, no surprise; again, not interesting.
I will omit coping the entire gdb output here, but the procedure is the same as with strtok
:
gef➤ disable 2
gef➤ finish
gef➤ enable 2
Then things get more interesting:
gef➤ c
Continuing.
Hardware access (read/write) watchpoint 2: *0x7fffffffd990
Value = 0x6f6f66
0x000055555595708e in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────── registers ────
$rax : 0x66
$rbx : 0x0
$rcx : 0x990
$rdx : 0xc0d8
$rsp : 0x00007fffffffd960 → 0x88968586858f948f
$rbp : 0x00007fffffffda20 → 0x0000555555555890 → <__libc_csu_init+0> push r15
$rsi : 0x1
$rdi : 0x00007fffffffd990 → 0x00007f00006f6f66 ("foo"?)
$rip : 0x000055555595708e → jb 0x555555957145
$r8 : 0x0000555555555940 → 0x003f64726f777373 ("ssword?"?)
$r9 : 0x00007ffff7b76b80 → 0x00007ffff7b76b80 → [loop detected]
$r10 : 0x00007ffff7d83ca0 → 0x0000555555958a70 → 0x0000000000000000
$r11 : 0x246
$r12 : 0x0000555555555650 → <_start+0> xor ebp, ebp
$r13 : 0x00007fffffffdb00 → 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 ────
0x00007fffffffd960│+0x0000: 0x88968586858f948f ← $rsp
0x00007fffffffd968│+0x0008: 0xb6a7aaa9a3b2b5bd
0x00007fffffffd970│+0x0010: 0xa1a7a6b1a5a1abb6
0x00007fffffffd978│+0x0018: 0xa3a7b4a4a1b4b1a8
0x00007fffffffd980│+0x0020: 0x0042bfb6b5a3a8ad
0x00007fffffffd988│+0x0028: 0x00007ffff7b7fbfb → <__pthread_initialize_minimal+875> mov rax, QWORD PTR [rsp+0xc8]
0x00007fffffffd990│+0x0030: 0x00007f00006f6f66 ("foo"?) ← $rdi
0x00007fffffffd998│+0x0038: 0x00007ffff7b80c70 → <__wait_lookup_done+0> push r15
────────────────────────────────────────────────────────── code:x86:64 ────
0x55555595707a jb 0x55555595711f
0x555555957080 jae 0x55555595711f
0x555555957086 movzx eax, BYTE PTR [rbp+rax*1-0x90]
→ 0x55555595708e jb 0x555555957145 TAKEN [Reason: C]
↳ 0x555555957145 add eax, 0x42
0x555555957148 je 0x5555559570e0
0x55555595714a jne 0x5555559570e0
0x55555595714c mov rdi, rax
0x55555595714f jb 0x5555559571bc
0x555555957151 jae 0x5555559571bc
──────────────────────────────────────────────────────────────── trace ────
[#0] 0x55555595708e → jb 0x555555957145
───────────────────────────────────────────────────────────────────────────
gef➤
Memory breakpoints trigger after the access has happened; so the instruction that triggered it was
0x555555957086 movzx eax, BYTE PTR [rbp+rax*1-0x90]
The first byte of our password (‘foo’, remember) got loaded into eax
.
But the really interesting stuff comes after this:
0x555555957086 movzx eax, BYTE PTR [rbp+rax*1-0x90]
→ 0x55555595708e jb 0x555555957145 TAKEN [Reason: C]
↳ 0x555555957145 add eax, 0x42
0x42
gets added to out password byte.
This could be a some sort of (extremely simple) encryption and is definitely something to look into a little deeper.
If we singestep further and remove the obfuscation jumps, the next real instructions are:
0x555555957086 movzx eax, BYTE PTR [rbp+rax*1-0x90]
→ 0x555555957145 add eax, 0x42
0x5555559570e0 mov edx, eax
0x55555595703f mov eax, DWORD PTR [rbp-0x8]
0x5555559570fe cdqe
0x55555595726c mov BYTE PTR [rbp+rax*1-0x90], dl
0x5555559571f0 mov eax, DWORD PTR [rbp-0x8]
0x55555595709a cdqe
0x55555595705a movzx eax, BYTE PTR [rbp+rax*1-0x90]
0x5555559571fe mov edx, eax
0x55555595727f mov eax, DWORD PTR [rbp-0x8]
0x5555559570d2 cdqe
0x5555559571d3 movzx eax, BYTE PTR [rbp+rax*1-0xc0]
0x555555957153 sub edx, eax
0x55555595721b mov eax, edx
0x55555595706e or BYTE PTR [rbp-0x1], al
The password character + 0x42 gets written back to the password buffer (0x55555595726c
),
just to be read back into rax
(0x55555595705a
).
It is then placed into rdx
(0x5555559571fe
), and an other value gets read into rax
, this time from [rbp+rax*1-0xc0]
(0x5555559571d3
).
This value gets subtracted from our ‘encrypted’ password character (0x555555957153
),
and the result is or’red to the local variable at [rbp-0x1]
(0x55555595706e
) (which is 0 at the beginning).
So the difference between the password char + 0x42 and the value at [rbp+rax*1-0xc0]
is calculated.
And if that difference is not 0, [rbp-0x1]
will be set to a value != 0; if (and only if) there is no difference [rbp-0x1]
will remain 0.
So what is there at [rbp+rax*1-0xc0]
(0x7fffffffd940
, if rax
is 0)?
gef➤ x/40bx 0x7fffffffd940
0x7fffffffd940: 0x8f 0x94 0x8f 0x85 0x86 0x85 0x96 0x88
0x7fffffffd948: 0xbd 0xb5 0xb2 0xa3 0xa9 0xaa 0xa7 0xb6
0x7fffffffd950: 0xb6 0xab 0xa1 0xa5 0xb1 0xa6 0xa7 0xa1
0x7fffffffd958: 0xa8 0xb1 0xb4 0xa1 0xa4 0xb4 0xa7 0xa3
0x7fffffffd960: 0xad 0xa8 0xa3 0xb5 0xb6 0xbf 0x42 0x00
The first value is 0x8f
; 0x8f - 0x42
is 0x4e
- ascii for ’M’.
So if the first password character is M
, the local variable at [rbp-0x1]
(which you might have guess already serves as a ‘wrong` marker) remains at 0.
The M
is not that much of a surprise as all flags in MRMCDCTF begin with, well, the letters MRMCDCTF{
.
And all that’s now left to do is subtract 0x42
from the other values to get the final flag.
Solution 2: The painful one
You can simply single-step until you reach the checks. This is how it was solved during testing.
Not nice, but it seems to work.