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

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= ping112.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:
- UDP server binds to
127.0.0.1:9768 - IP check - only accepts packets from the "ping" (
112.105.110.103) or "pong" (112.111.110.103) source IPs. anything else getsIP Blocked!!! - hex decode - decodes
0149545b...7a48from hex = 37 bytes of encrypted flag - XOR decryption - received data gets XOR'd with the IP address strings as keys
- 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
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:
sudo ip addr add 112.105.110.103/32 dev lo
sudo ip addr add 112.111.110.103/32 dev lothen sent the flag via UDP from the ping IP:
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

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::fmtoutput as key, its just math from there
flag
0xfun{h0mem4d3_f1rewall_305x908fsdJJ}
smothy out ✌️