CTF Time: Compile JPHide on Kali Linux

Recently I started participating in CTF (capture the flag) games. One of the challenges that I needed to solve, was to recover a message hidden in a JPEG file. To solve this challenge I needed an old steganography tool called jphide.

The tool is no longer maintained, but you can still find copies of its source code on Github, e.g. h3xx/jphs.

Let’s see how we can compile it on a “stock” Kali Linux image. BTW The best way to start with Kali Linux is to grab images from offensive-security.com site and load them into VM Ware Player or VirtualBox. The default user:password is kali:kali.

TIP Since I got addicted to iTerm2, I often prefer to SSH into Kali VM from iTerm2 instead of using Kali itself. Stock Kali comes with a preinstalled SSH server, we just need to enable it with sudo service ssh start.

OK finally its time to compile jphide:

$ git clone --depth 1  https://github.com/h3xx/jphs

First we need to compile jpeg-8a library:

$ cd jphs/jpeg-8a
$ ./configure
$ make all

After compilation, a new folder called .libs should be created:

$ ls .libs/lib*
.libs/libjpeg.so  .libs/libjpeg.so.8  .libs/libjpeg.so.8.0.1
$ cd .. # we are done here

Because we have not installed libjpeg.so.8 system-wide we need to modify Makefile before we can compile the main program:

--- a/Makefile
+++ b/Makefile
@@ -15,8 +15,8 @@ JP_CFLAGS = $(CFLAGS_COMMON) \
            -I./jpeg-8a
 BF_CFLAGS = $(CFLAGS_COMMON)

-LIBS = -ljpeg
-LDFLAGS = $(LIBS)
+LDFLAGS = -L./jpeg-8a/.libs
+LDLIBS = -ljpeg

 ## programs
 INSTALL = install

As a side note let’s notice that author of this Makefile made a cardinal sin of linking: never, Never, NEVER put linked libraries into LDFLAGS. Always pass them to linker using LDLIBS variable.

To patch the Makefile just save the diff as e.g. patch1 and then execute patch < patch1 in jphs directory.

After patching Makefile we are ready to build the tools:

$ make clean
$ make

On some systems you may encounter a compilation problem on line 208 of jpseek.c:

if ((f = open(seekfilename,O_WRONLY|O_TRUNC|O_CREAT)) < 0)

You can fix it by adding a third parameter to open call:

if ((f = open(seekfilename,O_WRONLY|O_TRUNC|O_CREAT, 0644)) < 0)

EDIT: I forgot to mention this in the original post. Because we did not installed jpeg-8a library system wide, we need to set LD_LIBRARY_PATH variable:

$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$(realpath ./jpeg-8a/.libs/)
$ echo $LD_LIBRARY_PATH
:/home/kali/jphs/jpeg-8a/.libs

Now the program should run without any problems:

$ ./jphide
jphide, version 0.3 (c) 1998 Allan Latham <alatham@flexsys-group.com>

This is licenced software but no charge is made for its use.
NO WARRANTY whatsoever is offered with this product.
NO LIABILITY whatsoever is accepted for its use.
You are using this entirely at your OWN RISK.
See the GNU Public Licence for full details.

Usage:

jphide input-jpg-file output-jpg-file hide-file

Bonus - cracking mode

Because during CTF you often have to crack the password using /usr/share/dict/words or rockyou.txt lists, its convenient to have jpseek version that can quickly check which passwords are good candidates for further analysis.

$ cp jpseek.c jpcrack.c

The patch assumes that you have added the third parameter to the open call.

$ patch jpcrack.c crack-patch

And here is the crack-patch itself:

--- jpseek.c
+++ jpcrack.c
@@ -6,12 +6,11 @@
  * Use permitted under terms of GNU Public Licence only.
  *
  */
-
+#define _GNU_SOURCE
 #include <stdio.h>
 #include <string.h>
 #include <stdlib.h>
 #include <ctype.h>
-#include <fcntl.h>
 #include <unistd.h>
 #include <getopt.h>
 #include <errno.h>
@@ -20,11 +19,13 @@
 #include <sys/time.h>		
 #include <sys/resource.h>		
 #include <sys/stat.h>		
+#include <fcntl.h>
 #include "bf.h"
 #include "cdjpeg.h"
 #include "version.h"
 
 #define NKSTREAMS 4
+#define PASSLEN 120
 
 static jvirt_barray_ptr * coef_arrays;
 static unsigned int  cpos[NKSTREAMS];
@@ -187,10 +188,10 @@
 
 
 static int jpseek (const char* infilename,
-                   const char* seekfilename)
+		   const char* pass)
 {
+ int result = 0;
  int b,count,i,j,len0,len1,len2;
- char *pass;
  unsigned char iv[9];
  unsigned char v;
  struct jpeg_decompress_struct srcinfo;
@@ -201,14 +202,10 @@
 
  input_file = fopen(infilename,"r");
  if (input_file == NULL) {
-  perror("Can't open input file");
-  return (1);
+	perror("Can't open input file");
+	exit(2);
  }	
 
- if ((f = open(seekfilename,O_WRONLY|O_TRUNC|O_CREAT, 0644)) < 0) {
-  perror("Can't open seek file");
-  return (1);
- }
 
  srcinfo.err = jpeg_std_error(&jsrcerr);
  jpeg_create_decompress(&srcinfo);
@@ -232,14 +229,9 @@
   iv[i] = (((short**)(((void**)(((void**)coef_arrays)[0]))[0]))[0])[i]; 
  }
 
- pass = getpass("Passphrase: ");
- if (strlen(pass) == 0) {
-  fprintf(stderr,"Nothing done\n");
-  exit(0);
- }  
  if (strlen(pass) > 120) {
   fprintf(stderr,"Truncated to 120 characters\n");
-  pass[120] = 0;
+	exit(3);
  }
 
  Blowfish_ExpandUserKey(pass,strlen(pass),bkey);
@@ -266,8 +258,8 @@
   v = 0;
   for(j=0;j<8;j++) {
    if ((b = get_bit()) < 0) {
-    fprintf(stderr,"File not completely recovered\n");
-    exit(1);
+	result = 1;
+	goto end;
    }
    b = b << j;
    v |= b;
@@ -291,25 +283,27 @@
  while(count < length) {
   for(j=0;j<8;j++) {
    if ((b = get_bit()) < 0) {
-    fprintf(stderr,"File not completely recovered\n");
-    exit(1);
+	result = 1;
+	goto end;
    }
    b ^= get_code_bit(1);
    v = v << 1;
    v |= b;
    tail--;
   }
-  write(f,&v,1);
+  //write(f,&v,1);
   count++;
  }
 
+end:
+
  jpeg_finish_decompress(&srcinfo);
  jpeg_destroy_decompress(&srcinfo);
 
- close(f);
+ //close(f);
  fclose(input_file);
 
- return(0);
+ return result;
 
 }
 
@@ -332,14 +326,40 @@
 static void usage()
 {
  fprintf(stderr,"Usage:\n\n\
-jpseek input-file seek-file\n\n");
+jpseek input-file passwords-list-file\n\n");
  exit(1);
 }
 
 int main(int argc, char **argv)
 {
- intro();
- if (argc != 3) usage();
- return (jpseek(argv[1],argv[2]));
+ if (argc != 3) { 
+   usage(); exit(10);
+}
+
+ char buff[128] = {0};
+
+ FILE* pass_file = fopen(argv[2],"r");
+ if (pass_file == NULL) {
+	perror("Can't open passwords file");
+	exit(20);
+ }	
+
+ long counter = 0;
+ while (fgets(buff, 120, pass_file) != NULL) {
+	// get rid of \n
+	buff[strcspn(buff, "\n")] = 0;
+	counter++;
+
+	printf("[trying=%s]\n", buff);
+	if (jpseek(argv[1], buff) == 0) {
+		puts(buff);
+	}
+
+	if ((counter % 100000) == 0) {
+		fprintf(stderr, "(stderr) currently at %s\n", buff);
+	}
+ }
+
+ return 0;
 }

And finally the diff for Makefile:

-- Makefile.orig	2021-01-31 08:49:57.904938275 -0500
+++ Makefile	2021-01-31 08:50:12.068602369 -0500
@@ -1,6 +1,7 @@
 # variables
 HDOBJECTS = jphide.o bf.o
 SKOBJECTS = jpseek.o bf.o
+CROBJECTS = jpcrack.o bf.o
 
 ## flags
 CFLAGS_COMMON = -O2
@@ -29,19 +30,21 @@
 BINDIR = $(PREFIX)/bin
 
 # targets
-TARGETS = jphide jpseek
+TARGETS = jphide jpseek jpcrack
 all: $(TARGETS)
 jphide: $(HDOBJECTS)
 jpseek: $(SKOBJECTS)
+jpcrack: $(CROBJECTS)
 
 # object rules
 bf.o:			CFLAGS=$(BF_CFLAGS)
-jphide.o jpseek.o:	CFLAGS=$(JP_CFLAGS)
+jphide.o jpseek.o jpcrack.o:	CFLAGS=$(JP_CFLAGS)
 
 # dependencies
 bf.c: bf.h bf_config.h
 jphide.c: ltable.h version.h bf.h
 jpseek.c: ltable.h version.h bf.h
+jpcrack.c: ltable.h version.h bf.h
 
 # other targets
 clean:

Demo time

Let’s hide a message in a cat picture:

$ wget https://upload.wikimedia.org/wikipedia/commons/4/4b/Domestic_Cat_Demonstrating_Dilated_Slit_Pupils.jpg -O cat.jpg
$ echo "very secret message" > msg.txt
$ ./jphide cat.jpg flag.jpg msg.txt
# Enter password
$ cat passlist.txt                   
cat
dog
abcd
1234
$ ./jpcrack flag.jpg passlist.txt 
[trying=cat]
[trying=dog]
[trying=abcd]
abcd
[trying=1234]
# Looks like 'abcd' is a good candidate
$ ./jpseek flag.jpg out.txt
$ cat out.txt
very secret message

You can get rid of [trying... text by commenting appropriate printf in the jpcrack.c.