Assigned CVE: CVE-2024-21502
Snyk advisory: SNYK-PYTHON-FASTECDSA-6262045
GitHub commit: fix memory corruption issue
Package manager: pip
Affected module: fastecdsa
GitHub repo: AntonKueltz/fastecdsa
Module description:
This is a python package for doing fast elliptic curve cryptography, specifically digital signatures.
Memory corruption in Python external module. Possible risk: denial of service, sensitive info leakage, remote code execution.
Vulnerability: uninitialized variable on the stack. Since the variable is used and interpreted as user-defined type, it leads to undefined behaviour. Depends on the variable's actual value it could be arbitrary free()
, arbitrary realloc()
, null pointer dereference and other.
I've tested it on Ubuntu 22.04.3 LTS, kernel version: 5.15.0-84-generic.
There is a simple PoC in file poc.py, the trigger is located in line 25. You could use Dockerfile in order to preserve the environment.
- Build the image
docker build --tag fastecdsa-poc .
- Run the image
docker run --rm fastecdsa-poc
- Expected behaviour
$ docker run --rm fastecdsa-poc
3.11.8 (main, Feb 13 2024, 09:58:12) [GCC 12.2.0]
X: 0x0
Y: 0x0
(On curve <MyCurve>)
free(): invalid pointer
Aborted (core dumped)
Actual source code is here: https://github.com/AntonKueltz/fastecdsa/tree/v2.3.1
The vulnerability is located in file src/curveMath.c. Function curvemath_mul is used to calculate point multiplication. This is a binding, so the function could be called from Python code directly.
static PyObject * curvemath_mul(PyObject *self, PyObject *args) {
char * x, * y, * d, * p, * a, * b, * q, * gx, * gy;
if (!PyArg_ParseTuple(args, "sssssssss", &x, &y, &d, &p, &a, &b, &q, &gx, &gy)) {
return NULL;
}
PointZZ_p result;
mpz_t scalar;
mpz_init_set_str(scalar, d, 10);
CurveZZ_p * curve = buildCurveZZ_p(p, a, b, q, gx, gy, 10);;
PointZZ_p * point = buildPointZZ_p(x, y, 10);
pointZZ_pMul(&result, point, scalar, curve);
destroyPointZZ_p(point);
destroyCurveZZ_p(curve);
char * resultX = mpz_get_str(NULL, 10, result.x);
char * resultY = mpz_get_str(NULL, 10, result.y);
mpz_clears(result.x, result.y, scalar, NULL);
PyObject * ret = Py_BuildValue("ss", resultX, resultY);
free(resultX);
free(resultY);
return ret;
}
Please notice that variable PointZZ_p result
is unitialized. Then it's passed to functions pointZZ_pMul
and mpz_clears
. Our target is the second function mpz_clears
since it calls free()
internally. We need to remain the variable uninitialized after calling pointZZ_pMul
.
Let's look at the function pointZZ_pMul. Here is the code at the beginning:
void pointZZ_pMul(PointZZ_p * rop, const PointZZ_p * point, const mpz_t scalar, const CurveZZ_p * curve) {
// handle the identity element
if(pointZZ_pIsIdentityElement(point)) {
return pointZZ_pSetToIdentityElement(rop);
}
PointZZ_p R0, R1, tmp;
mpz_inits(R1.x, R1.y, tmp.x, tmp.y, NULL);
mpz_init_set(R0.x, point->x);
mpz_init_set(R0.y, point->y);
pointZZ_pDouble(&R1, point, curve);
// truncated because the last part is not relevant
}
The first parameter (PointZZ_p * rop
) is not initialized again (it's responsibility of the caller). Passing condition pointZZ_pIsIdentityElement(point)
is trivial because we can construct arbitrary curve and arbitrary point on it. Let's look at the function pointZZ_pSetToIdentityElement:
void pointZZ_pSetToIdentityElement(PointZZ_p * op) {
mpz_set_ui(op->x, 0);
mpz_set_ui(op->y, 0);
}
The parameter PointZZ_p * op
is still not initialized. This is an undefined behaviour again.
So, the complete path below:
-
Python code (point multiplication)
-
Call function
curvemath_mul
(unitialized variableresult
) -
Call function
pointZZ_pMul
(unitialized argumentrop
) -
Call function
pointZZ_pSetToIdentityElement
(unitialized argumentop
) -
Return from function
pointZZ_pSetToIdentityElement
(argumentop
is still unitialized) -
Return from function
pointZZ_pMul
(argumentrop
is still unitialized) -
Call function
mpz_clears
(unitialized arguments) -
Call function
free
(argument is not initialized)
Since the stack can be controlled by attacker, the vulnerability could be used to corrupt allocator structure. It leads to possible heap exploitation.
Add initialization of variable:
PointZZ_p result;
mpz_inits(result.x, result.y, NULL);
Some time ago I've created a curve with b=0. Since the point (0, 0) is on created curve, the vulnerability was trigged accidentally. I started the investigation and found the root cause.
Thanks for finding and disclosing this, I've fixed this in release v2.3.2. Confirmed locally -