We’re given a dispense
binary and some captured traffic.
Decompiling the binary immediately reveals strings BN_CTX_new
and BN_new
, hinting towards OpenSSL being statically linked into the executable image.
OpenSSL lets us comfortably recover symbols by extracting function names, file names, and line numbers from run-time assertions and comparing with the structure of the code structure in source:
__int64 __fastcall main(int a1, char **a2, char **a3)
{
unsigned __int64 v3; // r14
FILE *flag_fd; // rax
FILE *v5; // rbx
FILE *psk_fd; // rax
FILE *v7; // rbx
int sock; // eax MAPDST
int v10; // eax
int v11; // r15d
const char *enc_msg_bs; // r12
__int64 j; // rbp
__int64 i; // rbp
__int64 result; // rax
const char *v16; // rdi
unsigned __int64 v17; // [rsp+8h] [rbp-40h]
const unsigned __int8 *randbytes; // [rsp+10h] [rbp-38h]
v3 = __readfsqword(0xFFFFFFE0);
__writefsqword(0xFFFFFFE0, v3 - 1680);
*(_QWORD *)(v3 - 1632) = 3097821186LL;
*(_QWORD *)(v3 - 1624) = 0LL;
memset((void *)(v3 - 1280), 0, 0x401uLL);
*(_OWORD *)(v3 - 1600) = 0LL;
*(_OWORD *)(v3 - 1584) = 0LL;
*(_OWORD *)(v3 - 1568) = 0LL;
*(_OWORD *)(v3 - 1552) = 0LL;
*(_OWORD *)(v3 - 1648) = 0LL;
*(_OWORD *)(v3 - 1664) = 0LL;
*(_DWORD *)(v3 - 4) = 16;
*(_OWORD *)(v3 - 1680) = 0LL;
flag_fd = fopen("flag.txt", "rb");
if ( !flag_fd )
goto LABEL_26;
v5 = flag_fd;
randbytes = (const unsigned __int8 *)(v3 - 1680);
if ( !fread((void *)(v3 - 1600), 1uLL, 0x40uLL, flag_fd) )
{
LABEL_27:
v16 = "fread\n";
goto LABEL_37;
}
fclose(v5);
psk_fd = fopen("psk", "rb");
if ( !psk_fd )
{
LABEL_26:
v16 = "fopen\n";
LABEL_37:
perror(v16);
result = 0xFFFFFFFFLL;
goto LABEL_38;
}
v7 = psk_fd;
if ( !fread((void *)(v3 - 1648), 1uLL, 0x10uLL, psk_fd) )
goto LABEL_27;
v17 = v3 - 1600;
fclose(v7);
sock = socket(2, 1, 0);
if ( sock == -1 )
{
v16 = "socket";
goto LABEL_37;
}
*(_DWORD *)(v3 - 252) = 1;
if ( setsockopt(sock, 1, 2, (const void *)(v3 - 252), 4u) == -1 )
{
v16 = "setsockopt";
goto LABEL_37;
}
if ( bind(sock, (const struct sockaddr *)(v3 - 1632), 0x10u) == -1 )
{
v16 = "bind";
goto LABEL_37;
}
if ( listen(sock, 1337) == -1 )
{
v16 = "listen";
goto LABEL_37;
}
v10 = accept(sock, (struct sockaddr *)(v3 - 1664), (socklen_t *)(v3 - 4));
if ( v10 == -1 )
{
v16 = "accept";
goto LABEL_37;
}
v11 = v10;
if ( (unsigned int)getentropy(randbytes, 16LL) == -1 )
{
v16 = "getentropy";
goto LABEL_37;
}
getnameinfo((const struct sockaddr *)(v3 - 1664), *(_DWORD *)(v3 - 4), (char *)(v3 - 1280), 0x401u, 0LL, 0, 8);
if ( !(unsigned int)ossl_a2ucompare((void *)(v3 - 1280), "🔒.hxp.io") )
{
*(_OWORD *)(v3 - 1304) = 0LL;
*(_OWORD *)(v3 - 1320) = 0LL;
*(_OWORD *)(v3 - 1336) = 0LL;
*(_OWORD *)(v3 - 1352) = 0LL;
*(_OWORD *)(v3 - 1368) = 0LL;
*(_OWORD *)(v3 - 1384) = 0LL;
*(_OWORD *)(v3 - 1400) = 0LL;
*(_OWORD *)(v3 - 1416) = 0LL;
*(_OWORD *)(v3 - 1432) = 0LL;
*(_OWORD *)(v3 - 1448) = 0LL;
*(_OWORD *)(v3 - 1464) = 0LL;
*(_OWORD *)(v3 - 1480) = 0LL;
*(_OWORD *)(v3 - 1496) = 0LL;
*(_OWORD *)(v3 - 1512) = 0LL;
*(_OWORD *)(v3 - 1528) = 0LL;
*(_DWORD *)(v3 - 1288) = 0;
strcpy((char *)(v3 - 1616), "hxp{n0t_4_fl4g}");
if ( !AES_set_encrypt_key((const unsigned __int8 *)(v3 - 1648), 128, (AES_KEY *)(v3 - 1528)) )
{
((void (__fastcall *)(unsigned __int64, unsigned __int64, __int64, unsigned __int64, unsigned __int64, __int64))AES_cbc_encrypt)(
v17,
v17,
64LL,
v3 - 1528,
v3 - 1616,
1LL);
for ( i = 0LL; i != 64; ++i )
dprintf(v11, "%02hhx", *(unsigned __int8 *)(v3 + i - 1600));
dprintf(v11, "\n");
goto LABEL_25;
}
LABEL_34:
v16 = "enc";
goto LABEL_37;
}
ctx = BN_CTX_new();
if ( !ctx )
{
v16 = "BN_CTX_new";
goto LABEL_37;
}
rsa_n = BN_new();
rsa_e = BN_new();
msg = BN_new();
enc_msg = BN_new();
if ( !enc_msg || !rsa_n || !rsa_e || !msg )
{
v16 = "BN_new";
goto LABEL_37;
}
BN_hex2bn(
&rsa_n,
"6878703c33796f753c336878700000000000000000000000009debf6dfc0d75203bde49a1f3ba3d949f08fa84b6d0a770f730306c32d17956654"
"4333df001dcf71d1030707aabf254b8777e1c9c0d857133d5ccabfa0232858588e17ba23dc2a34880ced41edb9f0814695e6ab4ba2d5fc758f43"
"1de65e8e34b508a1facda541");
BN_hex2bn(&rsa_e, "309");
msg = BN_bin2bn(randbytes, 16LL, msg);
BN_mod_exp(enc_msg, msg, rsa_e, rsa_n, ctx);
enc_msg_bs = (const char *)BN_bn2hex(enc_msg);
dprintf(v11, "%s\n", enc_msg_bs);
*(_OWORD *)(v3 - 248) = 0LL;
*(_OWORD *)(v3 - 232) = 0LL;
*(_OWORD *)(v3 - 216) = 0LL;
*(_OWORD *)(v3 - 200) = 0LL;
*(_OWORD *)(v3 - 184) = 0LL;
*(_OWORD *)(v3 - 168) = 0LL;
*(_OWORD *)(v3 - 152) = 0LL;
*(_OWORD *)(v3 - 136) = 0LL;
*(_OWORD *)(v3 - 120) = 0LL;
*(_OWORD *)(v3 - 104) = 0LL;
*(_OWORD *)(v3 - 88) = 0LL;
*(_OWORD *)(v3 - 72) = 0LL;
*(_OWORD *)(v3 - 56) = 0LL;
*(_OWORD *)(v3 - 40) = 0LL;
*(_OWORD *)(v3 - 24) = 0LL;
*(_DWORD *)(v3 - 8) = 0;
strcpy((char *)(v3 - 1616), "hxp{n0t_4_fl4g}");
if ( AES_set_encrypt_key(randbytes, 128, (AES_KEY *)(v3 - 248)) )
goto LABEL_34;
((void (__fastcall *)(unsigned __int64, unsigned __int64, __int64, unsigned __int64, unsigned __int64, __int64))AES_cbc_encrypt)(
v17,
v17,
64LL,
v3 - 248,
v3 - 1616,
1LL);
for ( j = 0LL; j != 64; ++j )
dprintf(v11, "%02hhx", *(unsigned __int8 *)(v3 + j - 1600));
dprintf(v11, "\n");
OPENSSL_free(enc_msg_bs, "dispense.c", 499LL);
BN_free((__int64 *)enc_msg);
BN_free((__int64 *)msg);
BN_free((__int64 *)rsa_e);
BN_free((__int64 *)rsa_n);
BN_CTX_free(ctx);
LABEL_25:
close(v11);
result = 0LL;
LABEL_38:
__writefsqword(0xFFFFFFE0, v3);
return result;
}
The logic of the function is roughly summarized as follows:
psk
into memorypsk
as an AES key to encrypt flag and send it to the clientThe RSA parameters are (N, e) = (0x6878703c33796f753c336878700000000000000000000000009debf6dfc0d75203bde49a1f3ba3d949f08fa84b6d0a770f730306c32d179566544333df001dcf71d1030707aabf254b8777e1c9c0d857133d5ccabfa0232858588e17ba23dc2a34880ced41edb9f0814695e6ab4ba2d5fc758f431de65e8e34b508a1facda541, 0x309)
, which look suspiciously backdoored.
The t.pcap
file contains 777 tcp connections from either 42.226.106.30
or 92.243.26.60
to 42.226.106.54
. The astute reader will notice two things:
So lets think about what would happen on any connection attempt from either 42.226.106.30
or 92.243.26.60
. Remember, the behaviour of the binary depends on whether the client connects from 🔒.hxp.io or from somewhere else. According to getnameinfo(3)
the function is the inverse of getaddrinfo(3): it converts a socket address to a corresponding host and service, in a protocol-independent manner.
It does so by performing reverse DNS lookups. So what do the two addresses resolve to?
➜ flag_dispenser dig -x 42.226.106.30
; <<>> DiG 9.16.37-Debian <<>> -x 42.226.106.30
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17325
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;30.106.226.42.in-addr.arpa. IN PTR
;; ANSWER SECTION:
30.106.226.42.in-addr.arpa. 21600 IN PTR hn.kd.ny.adsl.
;; Query time: 300 msec
;; SERVER: 192.168.178.1#53(192.168.178.1)
;; WHEN: Sun Mar 12 16:40:16 CET 2023
;; MSG SIZE rcvd: 82
➜ flag_dispenser dig -x 92.243.26.60
; <<>> DiG 9.16.37-Debian <<>> -x 92.243.26.60
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27019
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
dig: 'xn--ls8haaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.xn--ls8haaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.xn--ls8haaaaaaaaaaaaaa.xn--a.xn--a.xn--a.xn--a.xn--a.xn--a.xn--a.xn--a.xn--a.xn--a.xn--a.xn--a.xn--a.xn--a.kirschju.re.' is not a legal IDNA2008 name (string contains a disallowed character), use +noidnout
Hm … 42.226.106.30
doesn’t have a reverse DNS record set, but 92.243.26.60
resolves to 💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩.💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩.💩💩💩💩💩💩💩💩💩💩💩💩💩💩💩[...].kirschju.re
. That doesn’t look right. Googling for punycode and OpenSSL immediately points to the Halloween 2022 OpenSSL CVEs CVE-2022-3602 and CVE-2022-3786. One of them allows to overflow a stack-allocated buffer inside OpenSSL with .
(dot) characters. When inserting the values from the reverse DNS request into the vulnerable program, one can see that the randbytes
variable at the very top (1680 bytes) of the tack stack frame gets overwritten by .
s almost entirely. That means that we know the AES key used to encrypt the flag that is sent to the client!
Taking for example the encryptd flag from tcpstream #1
from wireshark (e86ff18a103d528ac01d0fbba5d55491f678ee3a7c6dd53135243ddf2e7852b7daa32347eaad1c6c869d6d569e366578c0a442da2e091a24eed12b1e7772a9fb
) we write a quick bruteforcer as follows:
#!/usr/bin/env python3
from Crypto.Cipher import AES
enc = bytes.fromhex("e86ff18a103d528ac01d0fbba5d55491f678ee3a7c6dd53135243ddf2e7852b7daa32347eaad1c6c869d6d569e366578c0a442da2e091a24eed12b1e7772a9fb")
# We need to recover three bytes from the AES key
for i in range(2**24):
k = b"." * 13 + int.to_bytes(i, 3, "little")
aes = AES.new(k, iv = b"hxp{n0t_4_fl4g}\x00", mode = AES.MODE_CBC)
dec = aes.decrypt(enc)
if b"hxp" in dec:
print(dec)
Which prints (among other garbage) the flag hxp{th3y_pr0m153d_cr1t1c4l_but_0nly_g4v3_h1gh}