picoCTF 2025 Writeup - Reverse Engineering
Flag Hunters - 75pt
Description
Lyrics jump from verses to the refrain kind of like a subroutine call. There's a hidden refrain this program doesn't print by default. Can you get it to print it? There might be something in it for you.
The program's source code can be downloaded here.
Connect to the program with netcat:
$ nc verbal-sleep.picoctf.net 56688
Hint
- This program can easily get into undefined states. Don't be shy about Ctrl-C.
- Unsanitized user input is always good, right?
- Is there any syntax that is ripe for subversion?
コードは以下。
import re
import time
# Read in flag from file
flag = open('flag.txt', 'r').read()
secret_intro = \
'''Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, '''\
+ flag + '\n'
song_flag_hunters = secret_intro +\
'''
[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
CROWD (Singalong here!);
RETURN
[VERSE1]
Command line wizards, we’re starting it right,
Spawning shells in the terminal, hacking all night.
Scripts and searches, grep through the void,
Every keystroke, we're a cypher's envoy.
Brute force the lock or craft that regex,
Flag on the horizon, what challenge is next?
REFRAIN;
Echoes in memory, packets in trace,
Digging through the remnants to uncover with haste.
Hex and headers, carving out clues,
Resurrect the hidden, it's forensics we choose.
Disk dumps and packet dumps, follow the trail,
Buried deep in the noise, but we will prevail.
REFRAIN;
Binary sorcerers, let’s tear it apart,
Disassemble the code to reveal the dark heart.
From opcode to logic, tracing each line,
Emulate and break it, this key will be mine.
Debugging the maze, and I see through the deceit,
Patch it up right, and watch the lock release.
REFRAIN;
Ciphertext tumbling, breaking the spin,
Feistel or AES, we’re destined to win.
Frequency, padding, primes on the run,
Vigenère, RSA, cracking them for fun.
Shift the letters, matrices fall,
Decrypt that flag and hear the ether call.
REFRAIN;
SQL injection, XSS flow,
Map the backend out, let the database show.
Inspecting each cookie, fiddler in the fight,
Capturing requests, push the payload just right.
HTML's secrets, backdoors unlocked,
In the world wide labyrinth, we’re never lost.
REFRAIN;
Stack's overflowing, breaking the chain,
ROP gadget wizardry, ride it to fame.
Heap spray in silence, memory's plight,
Race the condition, crash it just right.
Shellcode ready, smashing the frame,
Control the instruction, flags call my name.
REFRAIN;
END;
'''
MAX_LINES = 100
def reader(song, startLabel):
lip = 0
start = 0
refrain = 0
refrain_return = 0
finished = False
# Get list of lyric lines
song_lines = song.splitlines()
# Find startLabel, refrain and refrain return
for i in range(0, len(song_lines)):
if song_lines[i] == startLabel:
start = i + 1
elif song_lines[i] == '[REFRAIN]':
refrain = i + 1
elif song_lines[i] == 'RETURN':
refrain_return = i
# Print lyrics
line_count = 0
lip = start
while not finished and line_count < MAX_LINES:
line_count += 1
for line in song_lines[lip].split(';'):
if line == '' and song_lines[lip] != '':
continue
if line == 'REFRAIN':
song_lines[refrain_return] = 'RETURN ' + str(lip + 1)
lip = refrain
elif re.match(r"CROWD.*", line):
crowd = input('Crowd: ')
song_lines[lip] = 'Crowd: ' + crowd
lip += 1
elif re.match(r"RETURN [0-9]+", line):
lip = int(line.split()[1])
elif line == 'END':
finished = True
else:
print(line, flush=True)
time.sleep(0.5)
lip += 1
reader(song_flag_hunters, '[VERSE1]')
このスクリプトは詩(song_flag_hunters)を出力するスクリプトであるが、いくつか特殊な文字列があり、その部分だけは以下のような特殊な処理をする。
- RETURN n: n行目に移動する
- REFRAIN: RETURN nのnを現在の次の行に書き換えて、
[REFRAIN]
に移動する - CROWD: 入力を受け取り、CROWDの行をCrowd: <入力>に書き換える
また、このスクリプトは、詩を一行ずつ読み込んで処理するが、一行の中にセミコロンがある場合はセミコロンで分割して、処理を実行する。
フラグは詩(song_flag_hunters)の最初の部分に記載されているが、start_labelが[VERSE1]に指定されているので、[VERSE1]の行から出力してしまいそのままだと出力されない。
そこで、Crowdのところで以下のように入力する。
Crowd: ;RETURN 0
こうすると、2度目のCrowdを出力する時に、RETURN 0を実行してしまい、0行目に移動するのでフラグを見ることができる。
$ nc verbal-sleep.picoctf.net 56688
Command line wizards, we’re starting it right,
Spawning shells in the terminal, hacking all night.
Scripts and searches, grep through the void,
Every keystroke, we're a cypher's envoy.
Brute force the lock or craft that regex,
Flag on the horizon, what challenge is next?
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd: ;RETURN 0
Echoes in memory, packets in trace,
Digging through the remnants to uncover with haste.
Hex and headers, carving out clues,
Resurrect the hidden, it's forensics we choose.
Disk dumps and packet dumps, follow the trail,
Buried deep in the noise, but we will prevail.
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
Crowd:
Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, picoCTF{70637h3r_f0r3v3r_75053bc3}
Binary Instrumentation 1 - 200pt
Description
I have been learning to use the Windows API to do cool stuff! Can you wake up my program to get the flag?
Download the exe here. Unzip the archive with the passwordpicoctf
Hints
- Frida is an easy-to-install, lightweight binary instrumentation toolkit
- Try using the CLI tools like frida-trace to auto-generate handlers
起動してみるとSleepが呼ばれているようである。
> .\bininst1.exe
Hi, I have the flag for you just right here!
I'll just take a quick nap before I print it out for you, should only take me a decade or so!
zzzzzzzz....
ヒント1にあるようにFridaを使ってSleepにフックをかける。
> frida-trace -i Sleep bininst1.exe
を実行するとSleep関数のフックのテンプレートファイル__handlers__/KERNEL32.DLL/Sleep.jsが作成される。
このファイルを以下のように修正しSleepを1ミリ秒にするとフラグが表示される
defineHandler({
onEnter(log, args, state) {
args[0] = ptr(1);
log('Sleep()');
log('args[0]' + args[0]);
},
onLeave(log, retval, state) {
}
});
> frida-trace -i Sleep .\bininst1.exe
...
Hi, I have the flag for you just right here!
I'll just take a quick nap before I print it out for you, should only take me a decade or so!
zzzzzzzz....
/* TID 0x89a4 */
16 ms Sleep()
16 ms args[0]0x1
16 ms | Sleep()
16 ms | args[0]0x1
1307 ms Sleep()
1307 ms args[0]0x1
1307 ms | Sleep()
1307 ms | args[0]0x1
Ok, I'm Up! The flag is: cGljb0NURnt3NGtlX20zX3VwX3cxdGhfZnIxZGFfZjI3YWNjMzh9
出力されたフラグをbase64デコードすると
picoCTF{w4ke_m3_up_w1th_fr1da_f27acc38}
Tap into Hash - 200pt
Description
Can you make sense of this source code file and write a function that will decode the given encrypted file content?
Find the encrypted file here. It might be good to analyze source file to get the flag.
Hints
- Do you know what blockchains are? If so, you know that hashing is used in blockchains.
- Download the encrypted flag file and the source file and reverse engineer the source file.
提供されるコードは以下。
import time
import base64
import hashlib
import sys
import secrets
class Block:
def __init__(self, index, previous_hash, timestamp, encoded_transactions, nonce):
self.index = index
self.previous_hash = previous_hash
self.timestamp = timestamp
self.encoded_transactions = encoded_transactions
self.nonce = nonce
def calculate_hash(self):
block_string = f"{self.index}{self.previous_hash}{self.timestamp}{self.encoded_transactions}{self.nonce}"
return hashlib.sha256(block_string.encode()).hexdigest()
def proof_of_work(previous_block, encoded_transactions):
index = previous_block.index + 1
timestamp = int(time.time())
nonce = 0
block = Block(index, previous_block.calculate_hash(),
timestamp, encoded_transactions, nonce)
while not is_valid_proof(block):
nonce += 1
block.nonce = nonce
return block
def is_valid_proof(block):
guess_hash = block.calculate_hash()
return guess_hash[:2] == "00"
def decode_transactions(encoded_transactions):
return base64.b64decode(encoded_transactions).decode('utf-8')
def get_all_blocks(blockchain):
return blockchain
def blockchain_to_string(blockchain):
block_strings = [f"{block.calculate_hash()}" for block in blockchain]
return '-'.join(block_strings)
def encrypt(plaintext, inner_txt, key):
midpoint = len(plaintext) // 2
first_part = plaintext[:midpoint]
second_part = plaintext[midpoint:]
modified_plaintext = first_part + inner_txt + second_part
block_size = 16
plaintext = pad(modified_plaintext, block_size)
key_hash = hashlib.sha256(key).digest()
ciphertext = b''
for i in range(0, len(plaintext), block_size):
block = plaintext[i:i + block_size]
cipher_block = xor_bytes(block, key_hash)
ciphertext += cipher_block
return ciphertext
def pad(data, block_size):
padding_length = block_size - len(data) % block_size
padding = bytes([padding_length] * padding_length)
return data.encode() + padding
def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))
def generate_random_string(length):
return secrets.token_hex(length // 2)
random_string = generate_random_string(64)
def main(token):
key = bytes.fromhex(random_string)
print("Key:", key)
genesis_block = Block(0, "0", int(time.time()), "EncodedGenesisBlock", 0)
blockchain = [genesis_block]
for i in range(1, 5):
encoded_transactions = base64.b64encode(
f"Transaction_{i}".encode()).decode('utf-8')
new_block = proof_of_work(blockchain[-1], encoded_transactions)
blockchain.append(new_block)
all_blocks = get_all_blocks(blockchain)
blockchain_string = blockchain_to_string(all_blocks)
encrypted_blockchain = encrypt(blockchain_string, token, key)
print("Encrypted Blockchain:", encrypted_blockchain)
if __name__ == "__main__":
text = sys.argv[1]
main(text)
暗号化されたファイルは以下。
Key: b'\x8e\xdc\x08\xb8S\xee6\x0c\xf5\xfd\xceP\x15\xbf\xf6\xe2\x90\xf3\xd7F?,!\x1c\xb0D\x0cO\xcc\x04q\xb8'
Encrypted Blockchain: b"\xb4\xc8\xbd\xec@A\xbd-\x1d\xfd\x16\xe1\xe3sW\x18\xb1\x99\xea\xb8\x15\x10\xe8{\x19\xacE\xb3\xb4w\nH\xe7\x9d\xea\xe9EC\xb9(N\xa8\x14\xe1\xb7t]\x1c\xb7\xc8\xb9\xbaAF\xea}L\xadF\xb4\xb1&\n\x19\xac\xcc\xbc\xedGI\xef~\x16\xab\x10\xb6\xb7'\x0cN\xe0\xca\xee\xba\x15D\xbcz\x17\xfa\x17\xe3\xe0sY\x14\xe5\xca\xb5\xecAB\xea{\x1e\xffD\xe4\xb0%\x0cO\xb0\xc9\xee\xefC\x12\xeb|N\xff\x16\xe8\xb4'[\x15\xe0\xd1\xbc\xec\x10F\xe8q\x17\xa8\x10\xe1\xedu\\N\xb6\xc5\xef\xba\x10@\xe8y\x1a\xadC\xe3\xb0p\nO\xb0\xce\xfc\xb5\x15\x1e\xcd\x1di\xb5B\xbc\xba \x05s\xb2\xaf\xde\xb4 \x18\xdc+{\xffQ\xb3\x8d\x1c6y\xeb\xb1\xbc\xaeBH\xed\x01p\xbfc\xaa\xb8\t4V\xc3\xb7\xd3\xe8G\x12\xbfy\x1c\xfd\x11\xad\xe4r\\\x1f\xb5\xc8\xbf\xeeCC\xefy\x18\xadC\xb1\xe3uXM\xe7\xc4\xe9\xeb\x17\x12\xeb{\x1b\xf7\x15\xb6\xf8s^\x1c\xe7\xca\xba\xe5\x10A\xb8-\x18\xffA\xe4\xe7u]J\xb1\xcf\xb9\xef\x12C\xbd+L\xfd\x19\xe6\xed'XN\xe0\xcf\xe9\xef\x17@\xbczI\xa8\x18\xb1\xe1vV\x1d\xe5\xcb\xbf\xec\x12\x15\xec|L\xabD\xb4\xb0n^\x1c\xb8\x98\xea\xea\x10A\xec/\x1c\xfa\x17\xb6\xecp\x08J\xb9\xc4\xed\xeb\x10\x17\xb6+\x1c\xf6\x11\xe5\xe0s\x0bI\xe3\xca\xb9\xbaGH\xb6}\x18\xf7A\xe0\xe5{X\x1f\xb4\x99\xe9\xbe@H\xbf*I\xfd\x14\xb6\xe7 l."
ソースコードを読むと、スクリプトの第一引数に渡されたフラグをブロックチェーンの文字列(blockchain_string)の間に挟んだあとに暗号化しているようである。そのため、暗号化したブロックチェーンをブロックごとにkey_hashとxorすればフラグを間に挟んだブロックチェーン文字列を復号できそう。
def decrypt(encrypted_blockchain, key):
key_hash = hashlib.sha256(key).digest()
block_size = 16
plaintext = b''
for i in range(0, len(encrypted_blockchain), block_size):
block = encrypted_blockchain[i:i + block_size]
cipher_block = xor_bytes(block, key_hash)
plaintext += cipher_block
print(plaintext)
if __name__ == "__main__":
key = b'\x8e\xdc\x08\xb8S\xee6\x0c\xf5\xfd\xceP\x15\xbf\xf6\xe2\x90\xf3\xd7F?,!\x1c\xb0D\x0cO\xcc\x04q\xb8'
encrypted_blockchain = b"\xb4\xc8\xbd\xec@A\xbd-\x1d\xfd\x16\xe1\xe3sW\x18\xb1\x99\xea\xb8\x15\x10\xe8{\x19\xacE\xb3\xb4w\nH\xe7\x9d\xea\xe9EC\xb9(N\xa8\x14\xe1\xb7t]\x1c\xb7\xc8\xb9\xbaAF\xea}L\xadF\xb4\xb1&\n\x19\xac\xcc\xbc\xedGI\xef~\x16\xab\x10\xb6\xb7'\x0cN\xe0\xca\xee\xba\x15D\xbcz\x17\xfa\x17\xe3\xe0sY\x14\xe5\xca\xb5\xecAB\xea{\x1e\xffD\xe4\xb0%\x0cO\xb0\xc9\xee\xefC\x12\xeb|N\xff\x16\xe8\xb4'[\x15\xe0\xd1\xbc\xec\x10F\xe8q\x17\xa8\x10\xe1\xedu\\N\xb6\xc5\xef\xba\x10@\xe8y\x1a\xadC\xe3\xb0p\nO\xb0\xce\xfc\xb5\x15\x1e\xcd\x1di\xb5B\xbc\xba \x05s\xb2\xaf\xde\xb4 \x18\xdc+{\xffQ\xb3\x8d\x1c6y\xeb\xb1\xbc\xaeBH\xed\x01p\xbfc\xaa\xb8\t4V\xc3\xb7\xd3\xe8G\x12\xbfy\x1c\xfd\x11\xad\xe4r\\\x1f\xb5\xc8\xbf\xeeCC\xefy\x18\xadC\xb1\xe3uXM\xe7\xc4\xe9\xeb\x17\x12\xeb{\x1b\xf7\x15\xb6\xf8s^\x1c\xe7\xca\xba\xe5\x10A\xb8-\x18\xffA\xe4\xe7u]J\xb1\xcf\xb9\xef\x12C\xbd+L\xfd\x19\xe6\xed'XN\xe0\xcf\xe9\xef\x17@\xbczI\xa8\x18\xb1\xe1vV\x1d\xe5\xcb\xbf\xec\x12\x15\xec|L\xabD\xb4\xb0n^\x1c\xb8\x98\xea\xea\x10A\xec/\x1c\xfa\x17\xb6\xecp\x08J\xb9\xc4\xed\xeb\x10\x17\xb6+\x1c\xf6\x11\xe5\xe0s\x0bI\xe3\xca\xb9\xbaGH\xb6}\x18\xf7A\xe0\xe5{X\x1f\xb4\x99\xe9\xbe@H\xbf*I\xfd\x14\xb6\xe7 l."
decrypt(encrypted_blockchain, key)
実行すると以下になる
b'5410603d236160940efdcaf26beca4ddfaf5327aaf41b730645f77d4ccfdded5-00118a79e0fbdbba6bfc52384735078d69073d211d4efbc15b35ce5a168ad59a-00f7f88f01862b79cff1f05cc3e3dc12picoCTF{block_3SRhViRbT1qcX_XUjM0r49cH_qCzmJZzBK_41c10331}1123443252a07cca666af8e7ace2495f-000f669f06d71a4263f0353d23bc3968d6ba3e3a123ff8a4581d730ddb5cedde-009df6f0bf347f93ff88a7ff8b381550eeb65f198479a008635eeb691cf34f2c\x02\x02'
picoCTF{}の部分を抽出すると
picoCTF{block_3SRhViRbT1qcX_XUjM0r49cH_qCzmJZzBK_41c10331}
Chronohack - 200pt
Description
Can you guess the exact token and unlock the hidden flag?
Our school relies on tokens to authenticate students. Unfortunately, someone leaked an important file for token generation. Guess the token to get the flag.
Additional details will be available after launching your challenge instance.
Hints
- https://www.epochconverter.com/
- https://learn.snyk.io/lesson/insecure-randomness/
- Time tokens generation
- Generate tokens for a range of seed values very close to the target time
インスタンスを起動してncコマンドでアクセスして、トークンを推測して入力する問題。
tokenを生成するコードは以下提供される。
import random
import time
def get_random(length):
alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
random.seed(int(time.time() * 1000)) # seeding with current time
s = ""
for i in range(length):
s += random.choice(alphabet)
return s
def flag():
with open('/flag.txt', 'r') as picoCTF:
content = picoCTF.read()
print(content)
def main():
print("Welcome to the token generation challenge!")
print("Can you guess the token?")
token_length = 20 # the token length
token = get_random(token_length)
try:
n=0
while n < 50:
user_guess = input("\nEnter your guess for the token (or exit):").strip()
n+=1
if user_guess == "exit":
print("Exiting the program...")
break
if user_guess == token:
print("Congratulations! You found the correct token.")
flag()
break
else:
print("Sorry, your token does not match. Try again!")
if n == 50:
print("\nYou exhausted your attempts, Bye!")
except KeyboardInterrupt:
print("\nKeyboard interrupt detected. Exiting the program...")
if __name__ == "__main__":
main()
tokenは現在時刻のエポック秒をシードとした擬似乱数である。ncコマンドでのアクセス時にエポック秒を取得しているので、こちらもエポック秒を取得しておき、その周辺の値をシードとして乱数からトークンを生成すれば良い。
ただし、ローカルで試しているときとリモートで実行するときとで時刻差が異なる。ローカルで実行したときは現在の時刻+0〜50の値をシードとしてフラグ取得はできたが、ncコマンドでリモート実行した時は、現在の時刻+50〜100をシードとして用いることでフラグを取得することができた。
コードは以下
import socket
import time
import random
from pwn import *
def get_random(length, seed):
alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
random.seed(seed)
s = ""
for i in range(length):
s += random.choice(alphabet)
return s
def main():
host = "verbal-sleep.picoctf.net"
port = 55047
max_attempts = 50
token_length = 20
current_time = int(time.time() * 1000) # 現在の時刻(ミリ秒)
time_offsets = [i for i in range(50, 100)] # 50ミリ秒は接続に時間がかかるとした
io = remote(host, port)
# io = process(["python", "./token_generator.py"])
i = 0
while True:
response = io.recvrepeat(0.5).decode()
print(response)
if "Congratulations!" in response:
print("Token found!")
break
elif "Enter your guess for the token (or exit):" in response:
seed = current_time + time_offsets[i]
print(f"seed = {seed}")
token = get_random(token_length, seed)
print(f"Trying token: {token}")
io.sendline(token.encode())
i += 1
else:
print("Unexpected response:", response)
break
if __name__ == "__main__":
main()
picoCTF{UseSecure#$_Random@j3n3r@T0rsb5f8a5af}
Quantum Scrambler - 200pt
Description
We invented a new cypher that uses "quantum entanglement" to encode the flag. Do you have what it takes to decode it?
Connect to the program with netcat:
$ nc verbal-sleep.picoctf.net 60759
The program's source code can be downloaded here.
Hints
- Run
eval
on the cypher to interpret it as a python object- Print the outer list one object per line
- Feed in a known plaintext through the scrambler
ncコマンドを実行すると長いpythonの配列のテキストが取得できるので保存しておく
$ nc verbal-sleep.picoctf.net 60759 > output.txt
ncの接続先が実行しているのは以下のコードである。
import sys
def exit():
sys.exit(0)
def scramble(L):
A = L
i = 2
while (i < len(A)):
A[i-2] += A.pop(i-1)
A[i-1].append(A[:i-2])
i += 1
return L
def get_flag():
flag = open('flag.txt', 'r').read()
flag = flag.strip()
hex_flag = []
for c in flag:
hex_flag.append([str(hex(ord(c)))])
return hex_flag
def main():
flag = get_flag()
cypher = scramble(flag)
print(cypher)
if __name__ == '__main__':
main()
scrambleの逆関数を作る。
def unscramble(L):
A = L
i = len(A) - 1 # 逆順に処理
while i >= 2:
# A[i-1] の最後の要素は、元のリストの前半部分のコピー
prefix = A[i-1].pop() # 取り出す
# A[i-2] から A[i-1] の内容を除去して元に戻す
original_element = A[i-2][-len(A[i-1]):] # 取り出す部分を特定
A[i-2] = A[i-2][:-len(A[i-1])] # その部分を除去
# A[i-1] を元のリストに復元
A.insert(i-1, original_element)
i -= 1 # 逆方向に進む
return L
def main(file):
scrambled_flag = eval(open(file, 'r').read())
flag_hex = unscramble(scrambled_flag)
for c in flag_hex:
print(chr(int(c[0], 16)), end='')
if __name__ == '__main__':
main(sys.argv[1])
実行するとフラグが出力される。
$ python unscramble.py output.txt
picoCTF{python_is_weirde2a45ca5}
Binary Instrumentation 2 - 300pt
Description
I've been learning more Windows API functions to do my bidding. Hmm... I swear this program was supposed to create a file and write the flag directly to the file. Can you try and intercept the file writing function to see what went wrong?
Download the exe here. Unzip the archive with the passwordpicoctf
Hints
- Frida is an easy-to-install, lightweight binary instrumentation toolkit
- Try using the CLI tools like frida-trace
- You can specify the exact function name you want to trace
Windows APIを使ってファイルを作成してフラグを書き込むプログラムを書いたがうまくいかない。フックして確認して欲しいという問題。
ファイル作成はCreateFileA、書き込みはWriteFileだろうということでFridaでtraceする。
> frida-trace -i CreateFileA -i WriteFile .\bininst2.exe
16 ms CreateFileA()
CreateFileは呼ばれているので、CreateFileの第一引数を見てファイル名を確認する。
__handlers__/KERNEL32.DLL/CreateFileA.js
を修正
defineHandler({
onEnter(log, args, state) {
log('CreateFileA()');
log('filename: ' + args[0].readAnsiString());
},
onLeave(log, retval, state) {
}
});
> frida-trace -i CreateFileA -i WriteFile .\bininst2.exe
...
16 ms CreateFileA()
16 ms filename: <Insert path here>
ファイル名のところに<Insert path here>と出力されるのので、適当なファイル名を入力する
defineHandler({
onEnter(log, args, state) {
log('CreateFileA()');
log('filename: ' + args[0].readAnsiString());
this.newFileName = Memory.allocUtf8String('.\\flag.txt');
args[0] = this.newFileName;
},
onLeave(log, retval, state) {
}
});
> frida-trace -i CreateFileA -i WriteFile .\bininst2.exe
16 ms CreateFileA()
16 ms filename: <Insert path here>
16 ms | CreateFileA()
16 ms WriteFile()
今度はWriteFileが呼ばれるようになったので、書き込もうとしているバッファ(第二引数)を出力するように__handlers__/KERNEL32.DLL/WriteFile.js
を修正
defineHandler({
onEnter(log, args, state) {
log('WriteFile()');
log('buffer: ' + args[1].readAnsiString())
},
onLeave(log, retval, state) {
}
});
> frida-trace -i CreateFileA -i WriteFile .\bininst2.exe
16 ms CreateFileA()
16 ms filename: <Insert path here>
16 ms | CreateFileA()
16 ms WriteFile()
16 ms buffer: cGljb0NURntmcjFkYV9mMHJfYjFuX2luNXRydW0zbnQ0dGlvbiFfYjIxYWVmMzl9
16 ms | WriteFile()
base64っぽい文字列が出力されたので、デコードする
picoCTF{fr1da_f0r_b1n_in5trum3nt4tion!_b21aef39}
perplexed - 400pt
Description
Download the binary here.
バイナリをダウンロードする。何も情報がないのでfileコマンドで確認。
$ file perplexed
perplexed: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=85480b12e666f376909d57d282a1ef0f30e93db
実行するとパスワードを入力して間違えているとWrongと出力される
$ ./perplexed
Enter the password: a
Wrong :(
Decompiler Explorerなどでデコンパイルする。check関数で0が返らないと失敗するようである。check関数をわかりやすいコードに修正する。
int64_t check(char* input)
{
if (strlen(input) != 27)
return 1;
int64_t secret;
memcpy(&secret, "\xe1\xa7\x1e\xf8\x75\x23\x7b\x61\xb9\x9d\xfc\x5a\x5b\xdf\x69\xd2\xfe\x1b\xed\xf4\xed\x67\xf4", 23);
int32_t input_index = 0;
int32_t bit_index = 0;
for (int32_t i = 0; i <= 22; i += 1)
{
for (int32_t j = 0; j <= 7; j += 1)
{
if (bit_index == 0)
bit_index += 1;
int32_t bit;
// input[input_index]の左からbit_index番目のビットが1かどうか
bit = (input[input_index] & 1 << (7 - bit_index)) > 0;
// input[input_index]とsecret[i]の左からj番目のビットが同じかどうか
if (bit != (*(&secret + i) & 1 << (7 - j)) > 0) {
return 1;
}
bit_index += 1;
if (bit_index == 8)
{
bit_index = 0;
input_index += 1;
}
if (input_index == strlen(input)) {
// 最後まで到達
return 0;
}
}
}
return 0;
}
入力した文字列の8bitごとのLSBから7bitの各ビットと、secretの7bitごとの各ビットが一致するかどうかをチェックしている。
逆にsecretをビット列にして7bitごとに区切ってMSBに0を追加して8bitにしてくっつければ、正しい入力を得ることができる。
secretのhexは以下。
e1 a7 1e f8 75 23 7b 61 b9 9d fc 5a 5b df 69 d2 fe 1b ed f4 ed 67 f4
これをビット列にする
11100001 10100111 00011110 11111000 01110101 00100011 01111011 01100001 10111001 10011101 11111100 01011010 01011011 11011111 01101001 11010010 11111110 00011011 11101101 11110100 11101101 01100111 11110100
7bitずつ区切って0を先頭にくっつける
01110000 01101001 01100011 01101111 01000011 01010100 01000110 01111011 00110000
01101110 00110011 01011111 01100010 01101001 00110111 01011111 00110100 01110100
01011111 01100001 01011111 00110111 01101001 01101101 00110011 01111101
ビット列から文字列に変換する。
picoCTF{0n3_bi7_4t_a_7im3}
Discussion