Description (en)
checksec:
$ checksec --file Sofia
[*] '/redacted/Sofia'
Arch: arm-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8000)
Stack: Executable
RWX: Has RWX segments
this ones interesting. sofia uses an internal flash fs called WFS. per
an online search:
WFS is a proprietary filesystem developed by Hikvision for its Network
Video Recorders (NVRs) and Digital Video Recorders (DVRs).
[1]
(https://www.ufsexplorer.com/ufs-explorer-video-recovery/?srsltid=AfmBOooXEaS7QruR5OQybE2P_I37nUcCgNEqXv09ZCzdtqv211_fx8DL)
How it works: Instead of formatting hard drives or flash storage with
general-purpose filesystems (like FAT32 or NTFS),
Hikvision devices write video data directly to the storage media
using WFS. [1]
(https://www.ufsexplorer.com/ufs-explorer-video-recovery/?srsltid=AfmBOooXEaS7QruR5OQybE2P_I37nUcCgNEqXv09ZCzdtqv211_fx8DL)
Purpose: This design minimizes fragmentation and maximizes
data-writing speeds for 24/7 video streams.
Note: Because it is proprietary, standard computers
(Windows/Mac/Linux) cannot natively read a WFS-formatted drive. Data
recovery professionals use specialized tools like UFS Explorer to
reconstruct and extract videos from these drives. [1]
(https://www.ufsexplorer.com/ufs-explorer-video-recovery/?srsltid=AfmBOooXEaS7QruR5OQybE2P_I37nUcCgNEqXv09ZCzdtqv211_fx8DL)
ok whatever that means. anyway, per some existing research, wfs holds
a block buffer cache, which is a pool of 128 buffers that hold ru’d
512-byte flash blocks in ram. each buffer has a refcount that tracks
how many threads are using it. then once refcount is 0 obv we free it
and it can be recycled.
one of the functions that handle this (per the proximity browser) is
int __fastcall sub_5B8BE8(int result)
{
int v1; // r3
if ( result )
{
v1 = *(unsigned __int8 *)(result + 18);
*(_BYTE *)(result + 18) = v1 - 1;
if ( !v1 )
return sub_5A882C("Trying to free free buffer\n");
}
return result;
}
interestingly it does warn if v1 (if the buffer doesnt exist) is free
that a double free is happening. it doesnt actually do anything about
it however. also since this is multithreaded, there is a clear as day
toctou. the same can be seen for looking up an entry and refcount
incrementing:
int __fastcall sub_5B8C10(int a1, __int64 a2)
{
int i; // r1
for ( i = dword_AD2DB4[(unsigned __int8)a2]; i; i = *(_DWORD *)(i + 24) )
{
if ( *(_DWORD *)i == a1 && *(_QWORD *)(i + 8) == a2 )
{
++*(_BYTE *)(i + 18);
return i;
}
}
return i;
}
again, no locking. this could mean we could get a toctou and
consequently a uaf vuln. maybe if we fuck with this carefully and look
at the fields we could potentially get arb r/w.
after researching online the struct layout looks like this:
struct {
int device_no;
// ??? size 4
uint64_t block_number;
uint8_t uptodate;
uint8_t dirty;
uint8_t refcount;
uint8_t paddingidk;
uintptr_t hash_prev;
uintptr_t hash_next;
uintptr_t lru_prev;
uintptr_t lru_next;
uintptr_t dataptr;
}
unfortunately i couldnt give a fuck about actualyl exploiting it, but
there is a chance someone could just use the ptrs to leak stuff if
youre careful enough. this struct is 40 bytes big, and there are
allocations of similar size with controllable data. youll have to
groom allocations so these two threads get to play with the buffer
properly instead of getting some random buffre.
to exploit this (by theory):
have thread x do a lookup. it finds your target buf and incremenmts
refcount and returns a ptr to the buffer.
have another thread decrement at the right time to race with thread x.
refcount is now 0, even though thread x is holding it.
the other thread gets a new buffer, walking along the lru linked list
to find a buffer with refcount 0. it finds thread x’s buffer, sets
refcount to 1, evicts it and repurposes it for a different block.
uaf, nice. we can now write stuff over the stale ref.
the buffer eviction logic:
int __fastcall sub_5B8C64(int a1, __int64 a2)
{
int result; // r0
int v6; // r10
int v7; // r3
int v8; // r2
int v9; // r3
int v10; // r2
int v11; // r3
int v12; // r1
while ( 1 )
{
result = sub_5B8C10(a1, a2);
if ( result )
break;
v6 = dword_9D8750;
while ( *(_BYTE *)(v6 + 18) )
{
v6 = *(_DWORD *)(v6 + 32);
if ( v6 == dword_9D8750 )
return 0;
}
*(_BYTE *)(v6 + 18) = 1;
v7 = *(_DWORD *)(v6 + 24);
if ( v7 )
*(_DWORD *)(v7 + 20) = *(_DWORD *)(v6 + 20);
v8 = *(_DWORD *)(v6 + 20);
if ( v8 )
*(_DWORD *)(v8 + 24) = v7;
v9 = *(unsigned __int8 *)(v6 + 8);
if ( dword_AD2DB4[v9] == v6 )
dword_AD2DB4[v9] = *(_DWORD *)(v6 + 24);
if ( !*(_DWORD *)(v6 + 28) || !*(_DWORD *)(v6 + 32) )
sub_5A882C("Free block list corrupted");
v10 = *(_DWORD *)(v6 + 28);
*(_DWORD *)(v10 + 32) = *(_DWORD *)(v6 + 32);
v11 = *(_DWORD *)(v6 + 32);
*(_DWORD *)(v11 + 28) = v10;
if ( dword_9D8750 == v6 )
dword_9D8750 = v11;
if ( *(_BYTE *)(v6 + 17) )
sub_5B8AE8(v6);
*(_BYTE *)(v6 + 17) = 0;
*(_BYTE *)(v6 + 16) = 0;
v12 = dword_AD2DB4[(unsigned __int8)a2];
*(_DWORD *)v6 = a1;
*(_QWORD *)(v6 + 8) = a2;
while ( 1 )
{
if ( !v12 )
{
sub_5B8A8C(v6);
return v6;
}
if ( *(_DWORD *)v12 == a1 && *(_QWORD *)(v12 + 8) == a2 )
break;
v12 = *(_DWORD *)(v12 + 24);
}
*(_DWORD *)v6 = 0;
*(_QWORD *)(v6 + 8) = 0;
*(_BYTE *)(v6 + 18) = 0;
sub_5B8A8C(v6);
}
return result;
}
so from here, we can see some interesting stuff. nmotably, we can see
if ( !*(_DWORD *)(v6 + 28) || !*(_DWORD *)(v6 + 32) )
sub_5A882C("Free block list corrupted");
v10 = *(_DWORD *)(v6 + 28);
*(_DWORD *)(v10 + 32) = *(_DWORD *)(v6 + 32);
v11 = *(_DWORD *)(v6 + 32);
*(_DWORD *)(v11 + 28) = v10;
if ( dword_9D8750 == v6 )
given the context and the vairables being reused, we can probably guess:
if ( !*(_DWORD *)(v6 + 28) || !*(_DWORD *)(v6 + 32) )
sub_5A882C("Free block list corrupted");
prev = *(_DWORD *)(v6 + 28);
*(_DWORD *)(prev + 32) = *(_DWORD *)(v6 + 32);
next = *(_DWORD *)(v6 + 32);
*(_DWORD *)(next + 28) = prev;
if ( dword_9D8750 == v6 )
so, we read buf->lru_prev, set prev->lru_next to buf->lru_next, read
buf->lru_next, then next->lru_prev = buf->lru_prev. if you have
skidded before, this is an unsafe unlink, since theres no validation
whatsoever on the ptrs. this means if you control buf+28 and buf+32 we
can get an arbitrary write. from there, we cna probably chain this to
eventually get an arbitrary read, amybe overwrite a got entry, etc
etc.
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/Raw JSON
{
"containers": {
"cna": {
"affected": [
{
"product": "UAF in XMEye",
"vendor": "unknown",
"versions": [
{
"status": "affected",
"version": "unknown"
}
]
}
],
"credits": [
{
"lang": "en",
"type": "finder",
"value": "evan"
}
],
"descriptions": [
{
"lang": "en",
"value": "checksec:\n\n$ checksec --file Sofia\n[*] '/redacted/Sofia'\n Arch: arm-32-little\n RELRO: No RELRO\n Stack: Canary found\n NX: NX unknown - GNU_STACK missing\n PIE: No PIE (0x8000)\n Stack: Executable\n RWX: Has RWX segments\n\nthis ones interesting. sofia uses an internal flash fs called WFS. per\nan online search:\n\nWFS is a proprietary filesystem developed by Hikvision for its Network\nVideo Recorders (NVRs) and Digital Video Recorders (DVRs).\n [1] \n(https://www.ufsexplorer.com/ufs-explorer-video-recovery/?srsltid=AfmBOooXEaS7QruR5OQybE2P_I37nUcCgNEqXv09ZCzdtqv211_fx8DL)\n How it works: Instead of formatting hard drives or flash storage with\ngeneral-purpose filesystems (like FAT32 or NTFS),\n Hikvision devices write video data directly to the storage media\nusing WFS. [1] \n(https://www.ufsexplorer.com/ufs-explorer-video-recovery/?srsltid=AfmBOooXEaS7QruR5OQybE2P_I37nUcCgNEqXv09ZCzdtqv211_fx8DL)\n Purpose: This design minimizes fragmentation and maximizes\ndata-writing speeds for 24/7 video streams.\n\n Note: Because it is proprietary, standard computers\n(Windows/Mac/Linux) cannot natively read a WFS-formatted drive. Data\nrecovery professionals use specialized tools like UFS Explorer to\nreconstruct and extract videos from these drives. [1]\n(https://www.ufsexplorer.com/ufs-explorer-video-recovery/?srsltid=AfmBOooXEaS7QruR5OQybE2P_I37nUcCgNEqXv09ZCzdtqv211_fx8DL)\nok whatever that means. anyway, per some existing research, wfs holds\na block buffer cache, which is a pool of 128 buffers that hold ru’d\n512-byte flash blocks in ram. each buffer has a refcount that tracks\nhow many threads are using it. then once refcount is 0 obv we free it\nand it can be recycled.\n\none of the functions that handle this (per the proximity browser) is\n\nint __fastcall sub_5B8BE8(int result)\n{\n int v1; // r3\n\n if ( result )\n {\n v1 = *(unsigned __int8 *)(result + 18);\n *(_BYTE *)(result + 18) = v1 - 1;\n if ( !v1 )\n return sub_5A882C(\"Trying to free free buffer\\n\");\n }\n return result;\n}\ninterestingly it does warn if v1 (if the buffer doesnt exist) is free\nthat a double free is happening. it doesnt actually do anything about\nit however. also since this is multithreaded, there is a clear as day\ntoctou. the same can be seen for looking up an entry and refcount\nincrementing:\n\nint __fastcall sub_5B8C10(int a1, __int64 a2)\n{\n int i; // r1\n\n for ( i = dword_AD2DB4[(unsigned __int8)a2]; i; i = *(_DWORD *)(i + 24) )\n {\n if ( *(_DWORD *)i == a1 && *(_QWORD *)(i + 8) == a2 )\n {\n ++*(_BYTE *)(i + 18);\n return i;\n }\n }\n return i;\n}\nagain, no locking. this could mean we could get a toctou and\nconsequently a uaf vuln. maybe if we fuck with this carefully and look\nat the fields we could potentially get arb r/w.\n\nafter researching online the struct layout looks like this:\n\nstruct {\n int device_no;\n // ??? size 4\n uint64_t block_number;\n uint8_t uptodate;\n uint8_t dirty;\n uint8_t refcount;\n uint8_t paddingidk;\n uintptr_t hash_prev;\n uintptr_t hash_next;\n uintptr_t lru_prev;\n uintptr_t lru_next;\n uintptr_t dataptr;\n}\nunfortunately i couldnt give a fuck about actualyl exploiting it, but\nthere is a chance someone could just use the ptrs to leak stuff if\nyoure careful enough. this struct is 40 bytes big, and there are\nallocations of similar size with controllable data. youll have to\ngroom allocations so these two threads get to play with the buffer\nproperly instead of getting some random buffre.\n\nto exploit this (by theory):\n\nhave thread x do a lookup. it finds your target buf and incremenmts\nrefcount and returns a ptr to the buffer.\nhave another thread decrement at the right time to race with thread x.\nrefcount is now 0, even though thread x is holding it.\nthe other thread gets a new buffer, walking along the lru linked list\nto find a buffer with refcount 0. it finds thread x’s buffer, sets\nrefcount to 1, evicts it and repurposes it for a different block.\nuaf, nice. we can now write stuff over the stale ref.\nthe buffer eviction logic:\n\nint __fastcall sub_5B8C64(int a1, __int64 a2)\n{\n int result; // r0\n int v6; // r10\n int v7; // r3\n int v8; // r2\n int v9; // r3\n int v10; // r2\n int v11; // r3\n int v12; // r1\n\n while ( 1 )\n {\n result = sub_5B8C10(a1, a2);\n if ( result )\n break;\n v6 = dword_9D8750;\n while ( *(_BYTE *)(v6 + 18) )\n {\n v6 = *(_DWORD *)(v6 + 32);\n if ( v6 == dword_9D8750 )\n return 0;\n }\n *(_BYTE *)(v6 + 18) = 1;\n v7 = *(_DWORD *)(v6 + 24);\n if ( v7 )\n *(_DWORD *)(v7 + 20) = *(_DWORD *)(v6 + 20);\n v8 = *(_DWORD *)(v6 + 20);\n if ( v8 )\n *(_DWORD *)(v8 + 24) = v7;\n v9 = *(unsigned __int8 *)(v6 + 8);\n if ( dword_AD2DB4[v9] == v6 )\n dword_AD2DB4[v9] = *(_DWORD *)(v6 + 24);\n if ( !*(_DWORD *)(v6 + 28) || !*(_DWORD *)(v6 + 32) )\n sub_5A882C(\"Free block list corrupted\");\n v10 = *(_DWORD *)(v6 + 28);\n *(_DWORD *)(v10 + 32) = *(_DWORD *)(v6 + 32);\n v11 = *(_DWORD *)(v6 + 32);\n *(_DWORD *)(v11 + 28) = v10;\n if ( dword_9D8750 == v6 )\n dword_9D8750 = v11;\n if ( *(_BYTE *)(v6 + 17) )\n sub_5B8AE8(v6);\n *(_BYTE *)(v6 + 17) = 0;\n *(_BYTE *)(v6 + 16) = 0;\n v12 = dword_AD2DB4[(unsigned __int8)a2];\n *(_DWORD *)v6 = a1;\n *(_QWORD *)(v6 + 8) = a2;\n while ( 1 )\n {\n if ( !v12 )\n {\n sub_5B8A8C(v6);\n return v6;\n }\n if ( *(_DWORD *)v12 == a1 && *(_QWORD *)(v12 + 8) == a2 )\n break;\n v12 = *(_DWORD *)(v12 + 24);\n }\n *(_DWORD *)v6 = 0;\n *(_QWORD *)(v6 + 8) = 0;\n *(_BYTE *)(v6 + 18) = 0;\n sub_5B8A8C(v6);\n }\n return result;\n}\nso from here, we can see some interesting stuff. nmotably, we can see\n\n if ( !*(_DWORD *)(v6 + 28) || !*(_DWORD *)(v6 + 32) )\n sub_5A882C(\"Free block list corrupted\");\n v10 = *(_DWORD *)(v6 + 28);\n *(_DWORD *)(v10 + 32) = *(_DWORD *)(v6 + 32);\n v11 = *(_DWORD *)(v6 + 32);\n *(_DWORD *)(v11 + 28) = v10;\n if ( dword_9D8750 == v6 )\ngiven the context and the vairables being reused, we can probably guess:\n\n if ( !*(_DWORD *)(v6 + 28) || !*(_DWORD *)(v6 + 32) )\n sub_5A882C(\"Free block list corrupted\");\n prev = *(_DWORD *)(v6 + 28);\n *(_DWORD *)(prev + 32) = *(_DWORD *)(v6 + 32);\n next = *(_DWORD *)(v6 + 32);\n *(_DWORD *)(next + 28) = prev;\n if ( dword_9D8750 == v6 )\nso, we read buf->lru_prev, set prev->lru_next to buf->lru_next, read\nbuf->lru_next, then next->lru_prev = buf->lru_prev. if you have\nskidded before, this is an unsafe unlink, since theres no validation\nwhatsoever on the ptrs. this means if you control buf+28 and buf+32 we\ncan get an arbitrary write. from there, we cna probably chain this to\neventually get an arbitrary read, amybe overwrite a got entry, etc\netc.\n_______________________________________________\nSent through the Full Disclosure mailing list\nhttps://nmap.org/mailman/listinfo/fulldisclosure\nWeb Archives & RSS: https://seclists.org/fulldisclosure/"
}
],
"providerMetadata": {
"dateUpdated": "2026-10-02T04:57:33Z",
"orgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
"shortName": "VULNARCHIVE"
},
"references": [
{
"tags": [
"technical-description"
],
"url": "https://vuln.freearchive.org/archive/full-disclosure/2026/Sep/42"
},
{
"tags": [
"technical-description"
],
"url": "https://seclists.org/fulldisclosure/2026/Sep/42"
},
{
"url": "https://nmap.org/mailman/listinfo/fulldisclosure"
},
{
"url": "https://seclists.org/fulldisclosure/"
},
{
"url": "https://www.ufsexplorer.com/ufs-explorer-video-recovery/?srsltid=AfmBOooXEaS7QruR5OQybE2P_I37nUcCgNEqXv09ZCzdtqv211_fx8DL"
}
],
"source": {
"defect": [
"https://seclists.org/fulldisclosure/2026/Sep/42"
],
"discovery": "EXTERNAL"
},
"title": "UAF in XMEye Security Camera",
"x_gcve": [
{
"recordType": "advisory",
"relationships": [],
"vulnId": "GCVE-1988-2026-0417",
"x_vulnarchive": {
"archiveUrl": "https://vuln.freearchive.org/archive/full-disclosure/2026/Sep/42",
"automated": true,
"contentSha256": "b4a06f6e6b8a0aad588f146b2048eb0567531fd363aee694f5a88ffa1db92e79",
"evidenceScore": 6,
"messageId": "",
"originalUrl": "https://seclists.org/fulldisclosure/2026/Sep/42",
"policy": "vulnarchive-1",
"sourceFormat": "text/html",
"sourcePublishedAt": "2026-09-15T03:44:45Z"
}
}
]
}
},
"cveMetadata": {
"assignerOrgId": "4e2abfbf-4a2a-4b76-a4e0-d77c18ba156c",
"assignerShortName": "VULNARCHIVE",
"datePublished": "2026-10-02T04:57:33Z",
"dateUpdated": "2026-10-02T04:57:33Z",
"state": "PUBLISHED",
"vulnId": "GCVE-1988-2026-0417"
},
"dataType": "CVE_RECORD",
"dataVersion": "5.2"
}