Pingpong

0xFun CTFby smothy

pingpong - Reverse Engineering

Points: 325 | Difficulty: Medium | Author: blknova Flag: 0xfun{h0mem4d3_f1rewall_305x908fsdJJ} Solved by: Smothy @ 0xN1umb

ping pong vibes

what we got

single stripped Rust binary called pingpong. challenge description said "ONLY attempt if you love table tennis" lmao ok sure

$ file pingpong ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped

quick strings gave us the juice immediately:

112.105.110.103 112.111.110.103 0149545b5f4b5d1e5c545d1a55036c5700404b46505d426e02001b4909030957414a7b7a48 IP Blocked!!! A personal message to : Now you're pinging the pong!

those IPs? convert the octets to ASCII:

  • 112.105.110.103 = p.i.n.g = ping
  • 112.111.110.103 = p.o.n.g = pong

ngl thats kinda creative

the solve

understanding the binary

threw it in IDA Pro. Rust decompilation is pain but we mapped out the logic:

  1. UDP server binds to 127.0.0.1:9768
  2. IP check - only accepts packets from the "ping" (112.105.110.103) or "pong" (112.111.110.103) source IPs. anything else gets IP Blocked!!!
  3. hex decode - decodes 0149545b...7a48 from hex = 37 bytes of encrypted flag
  4. XOR decryption - received data gets XOR'd with the IP address strings as keys
  5. comparison - XOR result compared against the encrypted bytes via bcmp. match = success

the XOR scheme

this was the fun part. the binary uses Display::fmt on the Rust Ipv4Addr to convert the IPs to their string representation, then uses those strings as XOR keys.

key assignment with a ping/pong toggle:

r14 = 0 (0=ping, 1=pong) for each byte position r13 (1..36): key = ping_str if r14==0, pong_str if r14==1 key_index = r13 % 15 (15 = length of IP string) plaintext[r13] ^= key[key_index] if key_index == 0: toggle r14 (ping <-> pong)

so the key alternates between the two IP strings every 15 bytes:

  • bytes 0-15: "112.105.110.103" (ping)
  • bytes 16-30: "112.111.110.103" (pong)
  • bytes 31-36: "112.105.110.103" (ping again)

decrypt

python
enc = bytes.fromhex("0149545b5f4b5d1e5c545d1a55036c5700404b46505d426e02001b4909030957414a7b7a48")
ping = b"112.105.110.103"
pong = b"112.111.110.103"

flag = bytearray(len(enc))
flag[0] = enc[0] ^ ping[0]  # first byte always ping

r14 = 0  # start with ping
for i in range(1, 37):
    key = ping if r14 == 0 else pong
    idx = i % 15
    flag[i] = enc[i] ^ key[idx]
    if idx == 0:
        r14 = 1 - r14

print(flag.decode())
# 0xfun{h0mem4d3_f1rewall_305x908fsdJJ}

verifying it fr

to actually talk to the server we had to add the "ping" IP to our loopback interface since the binary blocks all other source IPs:

bash
sudo ip addr add 112.105.110.103/32 dev lo
sudo ip addr add 112.111.110.103/32 dev lo

then sent the flag via UDP from the ping IP:

python
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('112.105.110.103', 0))
s.sendto(flag, ('127.0.0.1', 9768))
data, _ = s.recvfrom(1024)
print(data)  # b"Now you're pinging the pong!"

server said Now you're pinging the pong! - we're in

hacker moment

key takeaways

  • Rust binaries are a pain to decompile but the logic is still there if you squint hard enough
  • the IP addresses spelling "ping" and "pong" in ASCII was lowkey clever
  • the "IP Blocked!!!" message was the hint that source IP matters - had to bind to the magic IPs
  • once you see the XOR with Display::fmt output as key, its just math from there

flag

0xfun{h0mem4d3_f1rewall_305x908fsdJJ}

smothy out ✌️