Skip to content

CTF 3 ways

Published: at 00:00

In the 2025 PwnMe.fr CTF, there’s a challenge called “backToThePast” that involves decrypting a flag encrypted with a simple XOR cipher. Its a fairly simple task and in this post, we’ll walk through three different approaches for solving the challenge: binary patching with Ghidra, runtime patching using GDB, and emulation with Qiling.

Table of contents

Open Table of contents

Binary Patching

Opening the binary in Ghidra and we see some loop over the file contents

while( true ) {
    uVar1 = getc(__fp);
    if (uVar1 == 0xffffffff) break;
    fseek(__fp,-1,1);
    key = rand();
    fputc(key % 0x7f ^ uVar1,__fp);
}
 

Upon loading the binary into Ghidra, we notice a loop that reads the file byte by byte. For each byte, it generates a random number using the rand() function and applies an XOR operation to it. The result is written back into the file, effectively encrypting it. This behavior is important because XOR encryption is symmetric, meaning if we can reverse the rand() sequence, we can decrypt the file.

Earlier in the code we see that rand() is seeded using the current time.

tVar4 = time((time_t *)0x0);
printf("time : %ld\n",tVar4);
srand((uint)tVar4);

If we can force the time to be the same as it was at the time of encryption, we can decrypt the file.

We can run stat to find the exact time the file was modified

Modify: 2024-05-08 20:01:17.000000000

As a Unix timestamp this is 0x663bda0d If we pass this as the argument to srand() the file should decrypt.

Lets look at where srand() is called

  MOV        RAX,qword ptr [RBP + local_128]
  MOV        EDI,EAX
  CALL       srand                                               

Standard x86 calling conventions state that the first argument should be stored in RDI. Here we can see RDI being set from a value on the stack. This is likely the result from the time() function earlier, lets check..

  MOV        EDI,0x0
  CALL       time                                 
  MOV        qword ptr [RBP + local_128],RAX

Let’s patch one of the MOV instructions to use our desired timestamp instead of the real timestamp on the stack

  MOV        RAX,0x663bda0d
  MOV        EDI,EAX
  CALL       srand                                

After applying this patch, running the binary should successfully decrypt the file and reveal the flag.

Runtime patching

As an alternative to patching the binary statically, we can also patch it at runtime using GDB. By setting a breakpoint at the srand() function call, we can manually modify the value in the RDI register, which holds the seed for srand(). Here’s the step-by-step process:

  1. Set a breakpoint at 0x4011d0 (the CALL srand instruction).
  2. Run the binary and wait for it to hit the breakpoint.
  3. Check the value of the RDI register, which holds the timestamp used to seed srand().
  4. Override the value of RDI with 0x663bda0d, the timestamp we extracted earlier.
  5. Continue execution to decrypt the file.

This approach allows us to modify the program’s behavior without having to patch the binary permanently.

gef➤ b *0x4011d0
gef➤ r flag.enc
# BREAKPOINT HIT
gef➤  p $rdi
$1 = 0x67c488b5
gef➤  set $rdi=0x663bda0d
gef➤  c
Continuing.
[Inferior 1 (process 143743) exited normally]

The program completes and the file is decrypted

Qiling emulation

For a more advanced approach, we can use Qiling, an emulation framework, to run the binary in a controlled environment. By hooking into system calls like clock_gettime(), we can intercept the time value and replace it with our own timestamp. This allows us to emulate the exact behavior of the program as if it were seeded with the correct time.

First we setup some boilerplate to run the binary, this is similar to running a binary under pwntools

if __name__=="__main__":
    path = ["backToThePast", "/home/dev/backtothepast/file"]
    rootfs = "/"
    q1 = Qiling(path, rootfs)
[=] 	set_tid_address(tidptr = 0x40b7d0) = 0x30bb5
[=] 	clock_gettime(clock_id = 0x0, tp = 0x80000000dc90) = 0x0
[=] 	ioctl(fd = 0x1, cmd = 0x5413, arg = 0x80000000d8e0) = 0x0

There we can see the call to the clock_gettime syscall and its arguments. Lets setup the hook and check the arguments

The tp = 80000000dc90 argument is a pointer to the timespec struct that we want to target, it is defined as such:

struct timespec {
	time_t tv_sec;
	long   tv_nsec;
};

That is the number of whole seconds since epoch and the number of nanoseconds elapsed since that second. To verify, intercept the syscalland inspect the struct pointed here at the end of the call, lets print the current timestamp at the same time.

def clock_gettime_hook(q1, clock_id, tp, res):
    curr_time = int(time.time())
    result = int.from_bytes(q1.mem.read(tp, 8), byteorder='little')
    print(f"found: {result}")
    print(f"curr : {curr_time}")

def hook_time(q1):
    q1.os.set_syscall('clock_gettime', clock_gettime_hook, QL_INTERCEPT.EXIT)

found: 1741031027
curr : 1741031027
[=] 	clock_gettime(clock_id = 0x0, tp = 0x80000000dc90) = 0x0

This confirms our hypothesis that the pointer is pointing at the target. All we need to do now is pack the payload struct and write it into memory in the hook. We only need to pack the first 32bit tv_sec value and can ignore the tv_ns value as time() and the follow on functions only read seconds.

Here is the completed script.

from qiling import *
from qiling.const import QL_INTERCEPT
import sys
import time
import os

def clock_gettime_hook(q1, clock_id, tp, res):
    q1.mem.write(tp, q1.pack32(0x663bda0d))
    return

def hook_time(q1):
    q1.os.set_syscall('clock_gettime', clock_gettime_hook, QL_INTERCEPT.EXIT)

if __name__=="__main__":
    file_path = os.path.abspath(sys.argv[1])
    path = ["backToThePast", file_path]
    rootfs = "/"
    q1 = Qiling(path, rootfs)
    hook_time(q1)
    q1.run()

Conclusion

Qiling is a tool that’s relatively new to me, but I have to say, I really like it. It’s incredibly powerful and offers a lot of potential, especially when it comes to emulation and reverse engineering. Being able to work with different architectures and perform deep analysis in a controlled environment is very powerful. However I do wish it had more documentation, as finding clear examples or in-depth explanations on how to use it effectively can be a bit of a struggle. Still, despite this, I think the tool has a lot of promise, and with a little extra effort, it can become a very valuable part of your reverse engineering toolkit.

Read more