Skip to content

[SECURITY] Heap-buffer-overflow at src/genmkvpwd.c:68 through an unchecked transition level #6039

Description

@shootduck

Vulnerability description

The binary-search path in show_pwd_r uses a cumulative Markov score to index nbparts but does not check that the next score is at most gmax_level. This differs from nb_parts, which returns zero before accessing the table when its level argument exceeds max_lvl.

while ((curchar = charsorted[pwd->password[pwd->len - 2] * 256 + k])
       != pwd->password[pwd->len - 1]) {
    i -= nbparts[curchar + pwd->len * 256 +
                 (pwd->level + proba2[pwd->password[pwd->len - 2] *
                                       256 + curchar]) *
                 256 * gmax_len];
    k++;
}

The supplied stats file creates an initial password beginning with character 97 (a) at level 200. Its first sorted child is character 1 with transition score 251. The lookup at src/genmkvpwd.c:68 therefore uses level 451 even though gmax_level is 400:

index = 1 + 2 * 256 + (200 + 251) * 256 * 8
      = 924,161

The allocation at src/genmkvpwd.c:274 has 256 * 401 * 9 = 923,904 uint64_t elements. Element 924,161 is 257 elements, or 2,056 bytes, past the end of the allocation. AddressSanitizer reports the resulting 8-byte heap-buffer-overflow.

Version and commit

John the Ripper 1.9.0-jumbo-1+bleeding, commit 9a336d8 (built version string: 1.9.0-jumbo-1+bleeding-9a336d800a).

Environment

  • Ubuntu 24.04.4 LTS; Linux 6.8.0-136-generic; x86_64.

  • GCC 13.3.0, glibc 2.39, and GCC AddressSanitizer.

  • The binary was configured with --without-openssl --enable-asan, debug information, and frame pointers.

Steps to reproduce

  1. On Ubuntu, install the build prerequisites:

    sudo apt-get update
    sudo apt-get install -y build-essential git perl
  2. Check out the vulnerable revision and set POC_STATS to the absolute path of the included stats artifact:

    export JTR=$PWD/john
    export POC_STATS=/absolute/path/to/stats
    git clone https://github.com/openwall/john.git "$JTR"
    git -C "$JTR" checkout 9a336d800a091bec9650c29282485145f31c9ffc
  3. Build genmkvpwd with AddressSanitizer:

    cd "$JTR/src"
    ./configure --quiet --without-openssl --enable-asan CC=gcc \
      CFLAGS='-O0 -g -fno-omit-frame-pointer' \
      CFLAGS_EXTRA='-O0 -g -fno-omit-frame-pointer' \
      LDFLAGS='-fsanitize=address'
    make -j"$(nproc)"
  4. Run the PoC. AddressSanitizer terminates the process with heap-buffer-overflow:

    cd "$JTR/run"
    ./genmkvpwd "$POC_STATS" 400 8 1 2> sanitizer_report.txt

Sanitizer report

The following is the complete, unmodified report included as sanitizer_report.txt.

allocated 7218 KB for nbparts
256 possible passwords
starting with a (1 to 256, 99.609375% of the scope)
=================================================================
==992521==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7530aa7ff808 at pc 0x587b94be235e bp 0x7ffee6c313a0 sp 0x7ffee6c31390
READ of size 8 at 0x7530aa7ff808 thread T0
    #0 0x587b94be235d in show_pwd_r /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:68
    #1 0x587b94be31b8 in show_pwd /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:121
    #2 0x587b94be495b in main /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:301
    #3 0x7530adc2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #4 0x7530adc2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #5 0x587b94be1784 in _start (/home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/run/genmkvpwd+0x4784) (BuildId: 4875d0d0571be6b5aca6c0dbdb99f1729b7a84ad)

0x7530aa7ff808 is located 2056 bytes after 7391232-byte region [0x7530aa0f2800,0x7530aa7ff000)
allocated by thread T0 here:
    #0 0x7530ae0fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x587b94be6b27 in mem_alloc /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/memory.c:92
    #2 0x587b94be4208 in main /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:274
    #3 0x7530adc2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #4 0x7530adc2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #5 0x587b94be1784 in _start (/home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/run/genmkvpwd+0x4784) (BuildId: 4875d0d0571be6b5aca6c0dbdb99f1729b7a84ad)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/shootduck/hotracer-experiments/0-day/hotracer/pilot/genmkvpwd/2026-08-01-1.9.0-jumbo-1+bleeding-9a336d8/john/build-asan/src/genmkvpwd.c:68 in show_pwd_r
Shadow bytes around the buggy address:
  0x7530aa7ff580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7530aa7ff600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7530aa7ff680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7530aa7ff700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7530aa7ff780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x7530aa7ff800: fa[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7530aa7ff880: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7530aa7ff900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7530aa7ff980: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7530aa7ffa00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7530aa7ffa80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
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
==992521==ABORTING

Potential fix

Make traversal lookups obey the same state bounds as nb_parts. The helper below returns zero for an impossible state, which is the semantic value nb_parts uses when level exceeds max_lvl. The traversal loops then skip zero-count children rather than indexing the table or emitting a nonexistent child. Initializing the local password state and checking the prefix avoids beginning a binary-search traversal with an invalid prefix.

I applied this diff to a fresh checkout of the vulnerable commit, rebuilt it with the ASan command above, and reran this PoC and the invalid-initial-level PoC. Both exited normally with no AddressSanitizer diagnostic. Two valid comparison runs, using arguments 10 8 0 1 and 400 8 0 1 with their respective stats files, produced byte-for-byte identical output before and after the change.

diff --git a/src/genmkvpwd.c b/src/genmkvpwd.c
index de62d03..4365d6e 100644
--- a/src/genmkvpwd.c
+++ b/src/genmkvpwd.c
@@ -19,22 +19,44 @@
 #include "memory.h"
 #include "mkvlib.h"
 
+/* nb_parts() defines states beyond either limit as having zero completions. */
+static uint64_t nbparts_at(unsigned char letter, unsigned int len,
+	unsigned long level)
+{
+	if (len > gmax_len || level > gmax_level)
+		return 0;
+
+	return nbparts[(size_t)letter + (size_t)len * 256 +
+	               (size_t)level * 256 * gmax_len];
+}
+
 static void show_pwd_rnbs(struct s_pwd * pwd)
 {
 	uint64_t i;
+	uint64_t child_parts;
 	unsigned int k;
 	unsigned long lvl;
+	unsigned long child_level;
+
+	if (pwd->len > gmax_len || pwd->level > gmax_level)
+		return;
 
 	k=0;
-	i = nbparts[pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len];
+	i = nbparts_at(pwd->password[pwd->len-1], pwd->len, pwd->level);
 	pwd->len++;
 	lvl = pwd->level;
 	pwd->password[pwd->len] = 0;
 	while(i>1)
 	{
 		pwd->password[pwd->len-1] = charsorted[ pwd->password[pwd->len-2]*256 + k ];
-		pwd->level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
-		i -= nbparts[ pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len ];
+		child_level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
+		child_parts = nbparts_at(pwd->password[pwd->len-1], pwd->len, child_level);
+		if (!child_parts) {
+			k++;
+			continue;
+		}
+		pwd->level = child_level;
+		i -= child_parts;
 		if (pwd->len<=gmax_len)
 		{
 			show_pwd_rnbs(pwd);
@@ -53,25 +75,39 @@ static void show_pwd_rnbs(struct s_pwd * pwd)
 static void show_pwd_r(struct s_pwd * pwd, unsigned int bs)
 {
 	uint64_t i;
+	uint64_t child_parts;
 	unsigned int k;
 	unsigned long lvl;
+	unsigned long child_level;
 	unsigned char curchar;
 
+	if (pwd->len > gmax_len || pwd->level > gmax_level)
+		return;
+
 	k=0;
-	i = nbparts[pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len];
+	i = nbparts_at(pwd->password[pwd->len-1], pwd->len, pwd->level);
 	pwd->len++;
 	lvl = pwd->level;
 	if (bs)
 	{
 		while( (curchar=charsorted[ pwd->password[pwd->len-2]*256 + k ]) != pwd->password[pwd->len-1] )
 		{
-			i -= nbparts[ curchar + pwd->len*256 + (pwd->level + proba2[ pwd->password[pwd->len-2]*256 + curchar ])*256*gmax_len  ];
+			child_level = lvl + proba2[ pwd->password[pwd->len-2]*256 + curchar ];
+			i -= nbparts_at(curchar, pwd->len, child_level);
 			k++;
 		}
-		pwd->level += proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
+		child_level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
+		child_parts = nbparts_at(pwd->password[pwd->len-1], pwd->len, child_level);
+		if (!child_parts) {
+			pwd->len--;
+			pwd->password[pwd->len] = 0;
+			pwd->level = lvl;
+			return;
+		}
+		pwd->level = child_level;
 		if (pwd->password[pwd->len]!=0)
 			show_pwd_r(pwd, 1);
-		i -= nbparts[ pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len ];
+		i -= child_parts;
 		printf("%s\n", pwd->password);
 		gidx++;
 		k++;
@@ -80,8 +116,14 @@ static void show_pwd_r(struct s_pwd * pwd, unsigned int bs)
 	while(i>1)
 	{
 		pwd->password[pwd->len-1] = charsorted[ pwd->password[pwd->len-2]*256 + k ];
-		pwd->level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
-		i -= nbparts[ pwd->password[pwd->len-1] + pwd->len*256 + pwd->level*256*gmax_len ];
+		child_level = lvl + proba2[ pwd->password[pwd->len-2]*256 + pwd->password[pwd->len-1] ];
+		child_parts = nbparts_at(pwd->password[pwd->len-1], pwd->len, child_level);
+		if (!child_parts) {
+			k++;
+			continue;
+		}
+		pwd->level = child_level;
+		i -= child_parts;
 		if (pwd->len<=gmax_len)
 		{
 			show_pwd_r(pwd, 0);
@@ -99,7 +141,7 @@ static void show_pwd_r(struct s_pwd * pwd, unsigned int bs)
 
 static void show_pwd(uint64_t start, uint64_t end, unsigned int max_level, unsigned int max_len)
 {
-	struct s_pwd pwd;
+	struct s_pwd pwd = { 0 };
 	unsigned int i;
 	unsigned int bs;
 
@@ -114,10 +156,14 @@ static void show_pwd(uint64_t start, uint64_t end, unsigned int max_level, unsig
 	if (bs)
 	{
 		print_pwd(start, &pwd, max_level, max_len);
+		if (!pwd.password[0])
+			return;
 		while(charsorted[i] != pwd.password[0])
 			i++;
 		pwd.len = 1;
 		pwd.level = proba1[pwd.password[0]];
+		if (pwd.level > max_level)
+			return;
 		show_pwd_r(&pwd, 1);
 		printf("%s\n", pwd.password);
 		i++;

Artifacts

The PoC and the sanitizer report are packaged into this ZIP file.

artifacts.zip

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugdupeThis issue already existed. When using this, always link to the original issue(s).maintenance/cleanup

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions