Quantcast
Channel: Cheeky4n6Monkey - Learning About Digital Forensics
Viewing all 76 articles
Browse latest View live

M57.biz Practice Investigation

$
0
0
INTRODUCTION AND SETUP

The first image my study partner ( http://computerforensicgraduate.wordpress.com/ ) and I decided on is located here:
http://digitalcorpora.org/corpora/scenarios/m57-jean

Its an investigation into how a spreadsheet was exfiltrated from a laptop. The laptop image is contained on 2 EnCase .E0 files (3 Gb total) which you can look at using a similar methodology to whats listed in "Digital Forensics with Open Source Tools" by Altheide & Carvey (the "Simon and Simon" of Computer Forensics, if I might be so bold / old).

Note: the case briefing pdf lists a different filename / filetype for the spreadsheet. I tried doing a "m57plan.xlsx" keyword search but didn't find it - using FTK Imager I found it as "m57plan.xls". Double-DOH! Live and learn ... take client briefings with a grain of salt?

We have both installed VMware Player 3 thru which we use the SANS SIFT Ubuntu virtual workstation (1.8 Gb download).
The SIFT workstation already contains several of the tools mentioned in Altheide & Carvey plus more. There's unallocated file carving, email extraction from PST files, RegRipper, FTK Imager just to name a few and all for FREE!
Be sure to download the VM "Distro version" ZIP file and not the bootable ISO image. SANS have set it up so you can unzip that file and then use VMWare Player 3 to open the "SIFT Workstation 2.1.vmx" file (via File, Open a New VM and then select the .vmx file). Keep the ZIP file after extracting it so that after each case you can delete the SIFT VM in VMWare player and start again fresh. Anyhow, once you've told VMWare Player where to find the .vmx file you just "play it" by double clicking on it. Everything should be automatic from then on and hopefully you get the login window.

Ubuntu will probably run a bit slower via VMware than if installed seperately but I found it OK using a circa 2003 single core Athlon64 with 2 Gb RAM running WinXP. And this way, I didn't need to spend time reformatting or dual booting the sucker and/or if I stuff up the SIFT, I can easily reset to a known good state. There's a pretty helpful forum at http://ubuntuforums.org/ if you have Ubuntu issues.

Tools Used:

VMWare Player 3.1.5 ( http://downloads.vmware.com/d/info/desktop_downloads/vmware_player/3_0 ) - you might have to sign up first (for free)
SANS SIFT Workstation ( http://computer-forensics.sans.org/community/downloads ) - requires a SANS login (free)
Forensic Corpora Jean Encase Image ( http://digitalcorpora.org/corpora/scenarios/m57-jean )

Aim: 

To find out:
- When did Jean create this spreadsheet?
- How did it get from her computer?
- Who else from the company is involved?

Setup Method:

A.Install SANS SIFT Virtual machine under VMWare Player 3 (as described earlier).

B.Download/Copy Jean's Encase files (.E01 & .E02) to the SANS SIFT VM "/cases" directory.
I used Windows File Explorer to copy the 2 EnCase files to the "cases" folder on the SIFTWorkstation in the SANS workgroup but you could also download it directly to the SIFT using the SIFT Firefox browser.

C.Read-only Mount the Encase image such that we can see them from the Ubuntu OS
Tthis blog describes how to do it (more or less):
http://stephenventer.blogspot.com/2009/02/mount-ewf-e01-on-linux.html

The SIFT 2.1 VM has most of the software/tools mentioned in the blog already installed / configured.
And pp 20-22 of "Digital Forensics with Open Source Tools" (Altheide & Carvey) details a similar process.
But there is one complication - the SIFT VM doesn't seem to recognise the HPFS (High Performance File System) / NTFS filesystem of the given EnCase files. The blog example doesn't mention this as a problem but I couldn't follow the blog/book procedures without getting errors.
I ended up using the Ubuntu Synaptic Manager (from the System, Control Center, Synaptic Package Manager menu) to install the "ntfs-config" package/software and Ubuntu then recognised/mounted the image. Not 100% sure why, but it seems to work ...
The Synaptic Package Manager is a GUI for installing Ubuntu software packages. Its kinda like the App store for iPhones. However, unlike iPhones you can also download source code seperately and compile/build it on your Ubuntu system. eg if its not available in a package.

So here's the full procedure I ended up performing:
  1. Boot up SIFT VM and login as sansforensics (password is "forensics" ... shhh! )
  2. At a terminal window, use the command "sudo su -" to login as root so we can issue commands with the appropriate privileges i.e. make data accessible/mount stuff.
  3. Use the command "mount_ewf.py /cases/nps-2008-jean.E* /mnt/ewf/" to combine the two evidence files into a single Unix style image file called "/mnt/ewf/nps-2008-jean" (note: we use the "nps-2008-jean.E*" argument so it picks up all EnCase parts). Afterwards, there will also be a text file containing the MD5 hash as originally calculated by EnCase. You can then use the command "md5sum /mnt/ewf/nps-2008-jean" to calculate a local MD5 hash for comparison with EnCase but it took a few minutes on my VM.
  4. Install the "ntfs-config" package using the Synaptic Manager.
  5. Use "losetup -o32256 -r /dev/loop0 /mnt/ewf/nps-2008-jean" to map the image file to a loop device (ensuring you specify the offset 32256 so the loop device is mapped to the Filesystem and not the beginning of the disk image. Blog/book has more info).
  6. Use "mkdir /mnt/m57jean" to create a mountpoint directory that we can use later.
  7. Use "mount /dev/loop0 /mnt/m57jean/ -o loop,ro" so we can map the loop device to a read only directory.
  8. As a test, use "ls -al /mnt/m57jean" to list the contents of the filesystem. You should see your typical Windows XP folder structure eg Documents and Settings, Program Files etc.
So to summarise, we've combined the 2 EnCase image files into one large image file and then mapped it to a read only directory called "/mnt/m57jean".

This article also has more information on read-only mounts for SIFT:
http://computer-forensics.sans.org/blog/2009/02/19/digital-forensic-sifting-how-to-perform-a-read-only-mount-of-evidence/

Some other potentially useful information:
Between steps 4-7 above, you can also use "fdisk -lu /mnt/ewf/nps-2008-jean" to show the filesystem type info (ie HPFS / NTFS).
If you need to unmount a directory, use "umount /mnt/m57jean" for example.
If you need to reset the loopback device, you can use the "losetup -d /dev/loop0" command.
If you restart the SIFT, it will lose all the mounting stuff and you'll have to do it all over. Can be helpful if you make a mistake and can't figure out how to recover.

You can also load up FTK Imager to preview the .E01 file directly from "/cases" but while you can browse the files thru FTK Imager, the other SIFT tools won't be able to read the EnCase format.
You can also browse the "/mnt/m57jean" folder using the Ubutu file explorer - just double click on one of the folders on the left hand side of the desktop and navigate to "/mnt/m57jean" (after completing steps 1-7).

I'll stop here and post my method(s) of investigation in the next post - just in case you want to figure out the next part yourself...

M57.biz Practice Investigation (Pt 3 - Final)

$
0
0
RESULTS AND LEARNING OUTCOMES


Welcome to the M57 entry where I present what I learnt during this investigation. Due to its ongoing use, I have removed my results/analysis section. I have also removed any comments mentioning any tools/strategies.

Learning Outcomes:

I spent several days on this - the briefing PDF mentions spending "until lunch" using EnCase (LOL!).
This investigation took a lot longer than I estimated - part of it was learning about/setting up the tools, part of it was discovering Windows places of interest (eg Registry artefacts), part of it was the snoopy factor ("What has this user been up to?") and part of it was just repeating commands so I could document the results more comprehensively. I am still not 100% sure that someone from the company was NOT involved with the bogus email but I can't seem to find anything to support it.

In the future, I should pay more attention to documenting my progress as I investigate. I was using a old fashioned notebook and pen - maybe I should be using a text file / word document? It would certainly make capturing the command lines / paths much easier.
By learning on the fly/diving in and not having a set process to follow, I don't think I was maximising my efficiency either. Still, I guess you have to walk before you run etc.
Also, all details from the client brief should be confirmed/verified before starting - I spent quite some time searching for a .xlsx file as stated in the PDF brief only to find it was a .xls file.



Postscript:
'Nother practice scenario which might interest y'all (see, I can speak like a Southerner too!) is:
http://www.cfreds.nist.gov/dfrws/Rhino_Hunt.html
In this scenario, possessing more than 9 Rhino pictures has been declared illegal in New Orleans (those dirty Rhinos!). You've been tasked to find as much evidence as you can from 3 tcpdumps and a 256 Mb USB key dd image. This is good for gaining experience using the WireShark network analyser (also included on SANS SIFT) and "foremost". And they have kindly supplied the answers too!

Don't Let This Happen To YOU !

$
0
0

Here is list of interview questions compiled by Libby - my Computer Forensics study partner. I've added a few more towards the end. They were sourced from questions posted on websites and questions asked in interviews. Feel free to add more questions and/or any tips for answering in the Comments section.
From my limited (entry-level) interview experience, it seems that character related questions are just as, if not more important as the technical ones. Having an encyclopaedic technical knowledge is probably less important than showing that you can work effectively with others (ie the interviewers). Showing a willingness/capability to learn independently and communicate ideas is also important. I also think that while you should be on your best behaviour (so-to-speak), you should also be YOURSELF. The interviewers will find out one way or another if you are acting. Speaking of which, it can't hurt to get some background on the interviewers (eg read their LinkedIn page, their company profile). If you have something in common, you might like to mention it during the interview (in a completely non-stalker way of course!) so as to build a rapport/be more memorable.

  • Describe the different file systems? FAT 12, FAT 16, FAT 32, NTFS
  • Describe the Windows operating systems?
  • What imaging tools and techniques are you familiar with?
  • What is the basic command line syntax for dd or dcfldd? What are the differences between the two?
  • Describe the steps to image a laptop with a bootable forensic cd?
  • What are some options to write block a drive before imaging or previewing?
  • What are two ways to do a network acquisition using Helix? List hardware and software required for each method.
  • What is the bare minimum equipment needed to image a desktop?
  • What is an MD5 checksum and how is it used in forensics?
  • What are some other hashing algorithms besides MD5?
  • What is a .ISO?
  • What is a bit level image and how is that different from an ISO?
  • What is the SAM file? Which operating system has it?
  • What is data carving?
  • What is live previewing of a system?
  • How would you image a hard drive on a system that cannot be shut down?
  • If a file is labeled .tar.gz what is it and why is it in .tar.gz format?
  • Describe the chain of custody in detail?
  • How would you be able to tell at the hex level that a file has been deleted in FAT 12?
  • How would you go about imaging a network without taking it down?
  • What is metadata? What is affected by it? What attributes does it represent?
  • Why is it important to sanitize your analysis media?
  • You have an IDE drive and it is not reading. Why is this?
  • Describe the difference between wiping and formatting drives?
  • How many timestamps are there in NTFS and what are they?
  • Does the registry have any timestamps?
  • What is the ntuser.dat file?
  • What do the MRU keys tell you in the registry?
  • What is a three way handshake in TCP/IP?
  • How does TCP differ from UDP?
  • What would I bring to the position?
  • What are the steps when taking a computer from the home?
  • What is the step by step procedure after receiving a hard drive which contains child pornography?
  • Someone willingly brings their computer in for some minor offense. After imaging, it is returned to the person. During the examination child pornography is found, what do you do?
  • What is slack space?
  • What is unallocated space?
  • What are bits, bytes, nibbles and clusters?
  • What is the hex value for a deleted file or directory in FAT systems?
  • What is the hex value for a directory?
  • How to calculate disk capacity?
  • What is volatile data?
  • What happens when a disk is formatted?
  • What is the numeric base system for hexadecimal, decimal, octal and binary?
  • What motivates you?
  • What are some challenges to computer forensics in the future?
  • Tell us about a time you faced a (technical) challenge and how you overcame it?
  • Give us an example of when you worked independently/within a team to meet a deadline?
  • Have you ever communicated technical concepts to a non-technical audience?
  • What brought you to this point in your career?
  • What do you know about our industry? Our organisation?
  • How can you help us? eg What skills do you have?
  • What are your career plans for the next 3 and 5 years?
  • What are your strengths/weaknesses?
  • Do you have any other interests/hobbies?

And here are some questions candidates might like to ask the interviewers ...

  • Where would I fit into the team? How big is the team? What is the experience level of the team?
  • What is the technical environment like? What tools/storage/hardware do you use?
  • What upcoming projects will I be involved in?
  • How is training organised?
  • What are the typical working hours/travel requirements?

And here are some websites chock full of forensicky advice goodness for the newbie ...

 "What makes a good forensicator? or how to get a job in Digital Forensics..."

(*GRATUITOUS NAMEDROP* Written by Mike Wilkinson - one of my previous Lecturers :)

Corey Harrell blogs about entry into Computer Forensics

Harlan Carvey blogs about entry into Computer Forensics

ForensicFocus Job Seeking Advice by Joe Alonzo

Magazine Article on Digital Forensics in Australia

Eric Huber Interviews Detective Cindy Murphy (Law Enforcement)


    Using SIFT to Crack a Windows (XP) Password from a Memory Dump

    $
    0
    0
    Introduction:

    Recently, I was thinking about writing a blog entry on Volatility but then found out that SketchyMoose has done an awesome job of covering it already (in a Windows environment). Thinking of my fellow SIFT-ians / SIFT-ers / SIFT-heads (what?!) - I figured I could still write an entry with a focus on using the SIFT VM to crack a Windows password *evil laugh*.
    To give an example of a DFIR scenario, FTK Imager can be used to capture a live Windows memory image and then the SIFT VM can be used to determine the Windows password(s). Or the responder could always nicely ask the owner for the password ;)
    For this scenario however, we will be using a Windows XP memory image supplied by NIST. It's not that I don't trust you all with the contents of my memory ... *sarcastic laugh*

    Here are the resources I used:

    - The SketchyMoose's Blog entry that inspired me (to copy it ;) :
    http://sketchymoose.blogspot.com/2011/10/cracking-passwords-with-volatility-and.html
    with some further demos:
    http://sketchymoose.blogspot.com/2011/11/using-volatility-suspicious-process.html

    - For the official Volatility Documentation (eg plugin usage with example outputs) see:
    https://code.google.com/p/volatility/wiki/CommandReference
    and for some brief notes about Volatility from the SANS Forensics 2009 - Memory Forensics and Registry Analysis Presentation by Brendan Dolan-Gavitt see:
    http://www.slideshare.net/mooyix/sans-forensics-2009-memory-forensics-and-registry-analysis

    - The official John The Ripper Documentation is available at:
    http://www.openwall.com/john/doc/
    with usage examples at:
    http://www.openwall.com/john/doc/EXAMPLES.shtml

    So what I'm now about to cover is specific to using Volatility (2.1a) and John The Ripper as provided on the SANS SIFT Virtual Machine V2.12.

    Volatility can be used to analyse a variety of Windows memory images. The general usage syntax is:
    vol.py plugin_namememory_image_name
    where plugin_name can be things such as pslist (list of running processes), pstree (hierachical view of running processes), connections (live network connections), connscan (live and previous network connection artifacts), hivelist (Windows hive virtual addresses), hashdump (extracts hashes of domain credentials). For more plugins refer to the Volatility Documentation Wiki link mentioned previously.

    Method:

    Here are the steps I followed:

    1. From the command prompt in the SIFT VM, type "sudo mkdir /cases/mem" to create a directory "/cases/mem"

    2. Copy/Download "memory-images.rar" (~500 Mb) to "/cases/mem/" from NIST's CFReDS Project at http://www.cfreds.nist.gov/mem/Basic_Memory_Images.html

    3. Type "sudo unrar e /cases/mem/memory-images.rar" to extract NIST images to "/cases/mem/"

    4. Type "vol.py imageinfo -f /cases/mem/xp-laptop-2005-07-04-1430.img"
    This should return an output something like:

    Volatile Systems Volatility Framework 2.1_alpha
    Determining profile based on KDBG search...

              Suggested Profile(s) : WinXPSP3x86, WinXPSP2x86 (Instantiated with WinXPSP2x86)
                         AS Layer1 : JKIA32PagedMemory (Kernel AS)
                         AS Layer2 : FileAddressSpace (/cases/mem/xp-laptop-2005-07-04-1430.img)
                          PAE type : No PAE
                               DTB : 0x39000
                              KDBG : 0x8054c060L
                              KPCR : 0xffdff000L
                 KUSER_SHARED_DATA : 0xffdf0000L
               Image date and time : 2005-07-04 18:30:32
         Image local date and time : 2005-07-04 18:30:32
              Number of Processors : 1
                        Image Type : Service Pack 2

                       
    5. We then use the "WINXPSP3x86" profile to search/parse thru the dump. Type "sudo vol.py --profile=WinXPSP3x86 hivelist -f /cases/mem/xp-laptop-2005-07-04-1430.img" so we can obtain the virtual addresses for the SAM and System hives. Note without the "sudo", I was getting some errors so I decided to play it safe. I also tried using it with "--profile=WinXPSP2x86" but got similar errors.
    The resulting output will look something like:

    Volatile Systems Volatility Framework 2.1_alpha
    Virtual     Physical    Name
    0xe2610b60  0x14a99b60  \Device\HarddiskVolume1\Documents and Settings\Sarah\Local Settings\Application Data\Microsoft\Windows\UsrClass.dat
    0xe25f0578  0x17141578  \Device\HarddiskVolume1\Documents and Settings\Sarah\NTUSER.DAT
    0xe1d33008  0x0f12c008  \Device\HarddiskVolume1\Documents and Settings\LocalService\Local Settings\Application Data\Microsoft\Windows\UsrClass.dat
    0xe1c73888  0x0efc5888  \Device\HarddiskVolume1\Documents and Settings\LocalService\NTUSER.DAT
    0xe1c04688  0x0e88e688  \Device\HarddiskVolume1\Documents and Settings\NetworkService\Local Settings\Application Data\Microsoft\Windows\UsrClass.dat
    0xe1b70b60  0x0dff5b60  \Device\HarddiskVolume1\Documents and Settings\NetworkService\NTUSER.DAT
    0xe1658b60  0x0c748b60  \Device\HarddiskVolume1\WINDOWS\system32\config\software
    0xe1a5a7e8  0x094bf7e8  \Device\HarddiskVolume1\WINDOWS\system32\config\default
    0xe165cb60  0x0c6ecb60  \Device\HarddiskVolume1\WINDOWS\system32\config\SAM
    0xe1a4f770  0x0948c770  \Device\HarddiskVolume1\WINDOWS\system32\config\SECURITY
    0xe1559b38  0x02d64b38  [no name]
    0xe1035b60  0x0283db60  \Device\HarddiskVolume1\WINDOWS\system32\config\system
    0xe102e008  0x02837008  [no name]
    0x8068d73c  0x0068d73c  [no name]


    6. Now we can extract the hashed password list to a file (in the current directory) called "xp-passwd" by typing "vol.py --profile=WinXPSP3x86 hashdump -y 0xe1035b60 -s 0xe165cb60 -f /cases/mem/xp-laptop-2005-07-04-1430.img  > xp-passwd"

    Note: 0xe1035b60 = system hive virtual address, 0xe165cb60 = SAM hive virtual address which we obtained previously in step 5.

    7. (Optional) If you type "cat xp-passwd" you should get something like:

    Administrator:500:08f3a52bdd35f179c81667e9d738c5d9:ed88cccbc08d1c18bcded317112555f4:::
    Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    HelpAssistant:1000:ddd4c9c883a8ecb2078f88d729ba2e67:e78d693bc40f92a534197dc1d3a6d34f:::
    SUPPORT_388945a0:1002:aad3b435b51404eeaad3b435b51404ee:8bfd47482583168a0ae5ab020e1186a9:::
    phoenix:1003:07b8418e83fad948aad3b435b51404ee:53905140b80b6d8cbe1ab5953f7c1c51:::
    ASPNET:1004:2b5f618079400df84f9346ce3e830467:aef73a8bb65a0f01d9470fadc55a411c:::
    Sarah:1006:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::


    8. For clarity, I am assuming that you will be running these commands from "/home/sansforensics". I had some initial problems launching John The Ripper from there - it was complaining that it couldn't open "john.ini" which contains the configuration info/rules. Consequently, I copied/renamed the John configuration file into "/home/sansforensics" using the command "cp /etc/john/john.conf ~/john.ini". This ensures that John's rules will be initialised properly when we proceed with the next step.

    9. Type "john xp-passwd" and depending on your CPU, wait a while ... you should get something like this:

    Loaded 10 password hashes with no different salts (LM DES [128/128 BS SSE2])
                     (Sarah)
                     (SUPPORT_388945a0)
                     (Guest)
    6                (Administrator:2)
    NEON96           (phoenix)
    guesses: 5  time: 0:00:01:27 (3)  c/s: 33842K  trying: FG#NNJG - FG#NNNI
    guesses: 5  time: 0:00:01:32 (3)  c/s: 33771K  trying: SWY1-4C - SWYEGAD
    guesses: 5  time: 0:00:01:38 (3)  c/s: 33981K  trying: 0INHM1 - 0INIEK
    guesses: 5  time: 0:00:03:35 (3)  c/s: 35750K  trying: KM51319 - KM5135E
    NEON199          (Administrator:1)
    guesses: 6  time: 0:00:16:35 (3)  c/s: 30877K  trying: 3S35/5# - 3S35/EA
    guesses: 6  time: 0:00:17:49 (3)  c/s: 30836K  trying: 06OZJYB - 06OZJ4U
    guesses: 6  time: 0:00:20:14 (3)  c/s: 30332K  trying: GM5BOM! - GM5BILI
    guesses: 6  time: 0:00:20:19 (3)  c/s: 30341K  trying: HMO-F37 - HMO-FM.
    guesses: 6  time: 0:00:40:15 (3)  c/s: 30880K  trying: EYGOMOA - EYGOP5U
    guesses: 6  time: 0:00:52:16 (3)  c/s: 30931K  trying: W8W24EI - W8W24N6
    JVYMGP1          (HelpAssistant:2)
    guesses: 7  time: 0:01:22:17 (3)  c/s: 28872K  trying: V4VBN69 - V4VBN8F
    guesses: 7  time: 0:01:23:37 (3)  c/s: 28802K  trying: UCBKWW0 - UCBKWG6
    guesses: 7  time: 0:01:28:01 (3)  c/s: 28419K  trying: SGVRGO6 - SGVRGUV
    guesses: 7  time: 0:01:38:31 (3)  c/s: 28008K  trying: #04CR3 - #04CM!
    guesses: 7  time: 0:01:47:08 (3)  c/s: 27758K  trying: UFE'ACB - UFE'ABN
    guesses: 7  time: 0:01:49:04 (3)  c/s: 27620K  trying: FXRG7D  - FXRBOVW
    guesses: 7  time: 0:02:02:48 (3)  c/s: 27110K  trying: DYCIAQD - DYCIIHK
    guesses: 7  time: 0:06:31:50 (3)  c/s: 24555K  trying: )K6T-. - )K6T_F
    guesses: 7  time: 0:06:32:15 (3)  c/s: 24557K  trying: )^Y3G_ - )^Y3TT
    Session aborted


    You can see where I ran out of patience with my single core Athlon64 CPU and aborted the session after approx 6.5 hours (by pressing CTRL-C).  Your mileage will vary methinks - so feel free to let it run to completion. Whilst John is running, whenever the operator presses a key, a timestamped statistics message is printed to screen.

    So from the output above, we now know the "Administrator" password is "NEON1996". John displays passwords in groups of 7 letters so we append the results of Administrator:2 (ie "6") to Administrator:1 (ie "NEON199"). In contrast, "phoenix" has the password "NEON96" - there is no second half to append / there is no numbered index associated. Also, "Sarah" / "SUPPORT_388945a0" / "Guest" do not appear to have a password set.

    Also from the output above, we can theorise that "HelpAssistant" and "ASPNET" have passwords greater than 7 characters long (ie they each use 2 password hashes). John reports 10 loaded password hashes = 1 hash each for "Sarah" / "SUPPORT_388945a0" / "Guest "/ "phoenix" + 2 hashes for "Administrator" which implies 4 password hashes left between "HelpAssistant" and "ASPNET".

    10. If we type "john -show xp-passwd" we will get a summary of the findings so far:

    Administrator:NEON1996:500:ed88cccbc08d1c18bcded317112555f4:::
    Guest::501:31d6cfe0d16ae931b73c59d7e0c089c0:::
    HelpAssistant:???????JVYMGP1:1000:e78d693bc40f92a534197dc1d3a6d34f:::
    SUPPORT_388945a0::1002:8bfd47482583168a0ae5ab020e1186a9:::
    phoenix:NEON96:1003:53905140b80b6d8cbe1ab5953f7c1c51:::
    Sarah::1006:31d6cfe0d16ae931b73c59d7e0c089c0:::


    7 password hashes cracked, 3 left

    Note: The second field is the password field eg for "Administrator", the password is "NEON1996". There are no passwords set for "Guest", "SUPPORT_388945a0" and/or "Sarah". I stopped John before it could calculate the passwords for "HelpAssistant" and "ASPNET".

    The John results are stored in a file called "john.pot" and events are logged to "john.log". Both of these files are located in the directory where "john" was launched from (eg "/home/sansforensics"). So if we want to restart a cracking attempt from scratch, you can use "rm -f john.pot" before re-launching "john". Should "john" crash/be CTRL-C'd, there will be a "john.rec" recovery file generated so "john" can restart from its last calculation point (as opposed to from the beginning).

    So thats about all I have to show you for now ... if you decide to try it out, I'd be interested to hear you comment on how long your processing time took. Go bananas !

    Using SIFT to Crack a Windows (XP) Password from a Forensic Image

    $
    0
    0
    In the previous post, we focused on retrieving Windows login passwords from a memory dump using Volatility.

    But what happens if you don't have a memory dump / only have a forensic image of the hard drive?

    Well, Rob Lee has kindly provided the tools in the SANS SIFT (V2.12) workstation and Irongeek has previously posted a how-to-guide. Additional information is also available in "Windows Registry Forensics" by Harlan Carvey (p 95) which describes other tools that can be used to crack Windows passwords (eg pwdump7, Cain, ophcrack).

    For this exercise, we will be using the M57 Jean image (mounted as before) and seeing if we can extract any Windows passwords.
    Windows (XP) uses a "bootkey" to encrypt the SAM password hashes so we need to determine this (using bkhive) first. We can then retrieve the unencrypted password hashes (using samdump2) and crack them using John The Ripper.

    Note: With this knowledge comes great responsibility - seriously, please don't abuse it.

    At a terminal command prompt:
    1. Type "bkhive /mnt/m57jean/WINDOWS/system32/config/system saved-system-key.txt"

    which should give the following output:

    bkhive 1.1.1 by Objectif Securite
    http://www.objectif-securite.ch
    original author: ncuomo@studenti.unina.it

    Root Key : $$$PROTO.HIV
    Default ControlSet: 001
    Bootkey: 02d709efb8514a2fc7474b28a30e0180


    The "saved-system-key.txt" file now contains the bootkey

    2. Type "samdump2 /mnt/m57jean/WINDOWS/system32/config/SAM saved-system-key.txt > jean-passwords.txt" to extract the hashes and store them in "jean-passwords.txt".

    The screen output looks something like:

    samdump2 1.1.1 by Objectif Securite
    http://www.objectif-securite.ch
    original author: ncuomo@studenti.unina.it

    Root Key : SAM


    And we can view the contents of "jean-passwords.txt" by typing "more jean-passwords.txt":

    Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    HelpAssistant:1000:c3bdfc311d5a1fc504f78d8f541b1278:ec90e2f6d084b8da1fd45605f51770a6:::
    SUPPORT_388945a0:1002:aad3b435b51404eeaad3b435b51404ee:b4bc4c178aa19d6a32960f64e16b6944:::
    Kim:1003:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Jean:1004:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Addison:1005:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Abijah:1006:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Devon:1007:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Sacha:1008:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::


    Note: looking at the first hash group ("aad3b435b51404eeaad3b435b51404ee")  for each login suggests that they all have the same password except for "HelpAssistant".

    3. Type "john jean-passwords.txt" to brute force the password hashes. You might need to copy the "john.conf" to the local directory if you haven't already done this (see the previous post exercise's step 8).

    The output should be something similar to:

    Loaded 2 password hashes with no different salts (LM DES [128/128 BS SSE2])
    guesses: 0  time: 0:00:00:35 (3)  c/s: 9522K  trying: JD43877 - JD43804
    guesses: 0  time: 0:00:01:36 (3)  c/s: 12533K  trying: MDLIDL - MDLA39
    guesses: 0  time: 0:00:01:48 (3)  c/s: 12610K  trying: H2OUB1$ - H2OUGY!
    guesses: 0  time: 0:00:13:20 (3)  c/s: 15198K  trying: EL3CFR9 - EL3CFSU
    guesses: 0  time: 0:00:19:48 (3)  c/s: 15325K  trying: VWATIBN - VWATLA.
    guesses: 0  time: 0:00:27:03 (3)  c/s: 15364K  trying: 4VA1RWW - 4VA1TA4
    guesses: 0  time: 0:00:27:09 (3)  c/s: 15367K  trying: R318IP8 - R318I2T
    guesses: 0  time: 0:00:37:19 (3)  c/s: 15617K  trying: 3LP7VNZ - 3LP7V40
    2KPLRCM          (HelpAssistant:2)
    guesses: 1  time: 0:00:39:55 (3)  c/s: 15300K  trying: KMX1MP1 - KMX1MCS
    guesses: 1  time: 0:00:48:17 (3)  c/s: 14007K  trying: GMEL-1D - GMEN315
    guesses: 1  time: 0:01:00:39 (3)  c/s: 12784K  trying: IEH;G F - IEHKIQN
    guesses: 1  time: 0:01:07:02 (3)  c/s: 12274K  trying: HX0RW8F - HX0RJE0
    guesses: 1  time: 0:01:16:48 (3)  c/s: 11733K  trying: J SJF5Y - J SJFP5
    guesses: 1  time: 0:01:26:37 (3)  c/s: 11303K  trying: LL*MKH0 - LL*MKT2
    guesses: 1  time: 0:01:30:49 (3)  c/s: 11166K  trying: MKGU97X - MKGU90L
    guesses: 1  time: 0:02:03:45 (3)  c/s: 10335K  trying: LT8HFGI - LT8HFMG
    guesses: 1  time: 0:02:21:02 (3)  c/s: 10011K  trying: K_)LILG - K_)LLS&
    guesses: 1  time: 0:02:22:42 (3)  c/s: 9970K  trying: ZW6RCD@ - ZW6RB5Z


    and if you keep waiting .... eventually (several hours later on my VM)

    LL@1WI8          (HelpAssistant:1)

    4. Typing "john -show jean-passwords.txt" will show the results in full:

    Administrator::500:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Guest::501:31d6cfe0d16ae931b73c59d7e0c089c0:::
    HelpAssistant:LL@1WI82KPLRCM:1000:ec90e2f6d084b8da1fd45605f51770a6:::
    SUPPORT_388945a0::1002:b4bc4c178aa19d6a32960f64e16b6944:::
    Kim::1003:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Jean::1004:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Addison::1005:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Abijah::1006:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Devon::1007:31d6cfe0d16ae931b73c59d7e0c089c0:::
    Sacha::1008:31d6cfe0d16ae931b73c59d7e0c089c0:::

    11 password hashes cracked, 0 left


    So we can conclude that there was only one set password ("LL@1WI82KPLRCM" for "HelpAssistant"). It appears that all other logins did not use a password - Oh The Horror!
    We can then infer that access to the Windows system is/was effectively uncontrolled and anyone could have access. Thus planting some seeds of doubt when trying to attribute a user's activities.

    A quicker password cracking method would be to use ophcrack (also provided on SIFT) and download the XP rainbow table(s). The rainbow table contains pre-calculated results to compare the hashes to so the process should run much quicker.
    Looking at the ophcrack tables info page shows that we would need to use the XP Special (7.5 Gb) table to handle the special "@" character in the "HelpAssistant" password.
    This table is not free so thats where I'll choose to end this exercise (cheap b@stard!). The smaller free tables only handle upper and lower case letters and numbers - no special characters. Just for completeness, I'll probably do a future post about ophcrack using the hashed SAM passwords from the Volatility post - none of those passwords use special characters.

    Using SIFT and ophcrack to Crack a Windows (XP) Password

    $
    0
    0
    First, A Note on Windows Passwords ...


    Thought I should include some relevant theory rather than dive striaght in as I have been doing ...

    Jesper M. Johansson has written an excellent PowerPoint presentation on "Windows Passwords: Everything You Need To Know"here.
    I'm not sure when it was written, but he also wrote a similar MS Technet article in 2005 here.

    In both, he describes how Windows stores/uses passwords. There are 2 types of password hashes stored in Windows - the LanManager (LM) password hash and the more recent NT password hash.
    The LM password is a holdover from the past and is still included for backward compatibility. It relies on padding, capitalising and splitting a password into 2 seperate 7 character parts. These parts are then used as  (DES) encryption keys to encrypt a known constant. The two resultant encrypted outputs are then joined together to form the LM "hash". There are only 142 possible useable characters that can be entered by the user (of which only 68 appear on English keyboards) and the maximum number of password combinations is 6.8 x 10^12.

    In contrast, the NT password hash uses the MD4 hash function on a Unicode (65 535 symbols) based password. If we limit ourselves to use the same character set/password length as LM, there are 4.6 x 10^25 combinations. Which is a LOT more combinations than LM! And once we allow ourselves to use anything from the full symbol list, the number of 14 character length password combinations increases to 2.7 x 10^67. Clearly, NT hashes are a lot more secure than LM hashes.

    By using a password longer than 14 characters, Windows will not store the LM password hash (only the NT hash). You can also create/set a NoLMHash Registry value to stop LM hash storage.

    I suspect if you want to login to an XP system on your desk, you will need the NT password. But if all you have is the LM password, you could start capitalising various combinations until you get a valid login. eg given a LM password of "NEON96" try Neon96, nEon96 etc.

    Using SIFT ophcrack

    So as promised, here's how to crack a Windows password using ophcrack on the SIFT Workstation.

    1. Go here and download the XP Free Fast (703 Mb) zip file to SIFT (eg save to "/home/sansforensics").

    2. Launch a new command terminal window and type "mkdir ~/ophcrack_tables" followed by "mkdir ~/ophcrack_tables/tables_xp_free_fast". Note: The "~/" is shorthand for your home directory ie "/home/sansforensics/". In this step, we are creating a directory structure to store our rainbow tables.

    3. Type "mv ~/tables_xp_free_fast.zip ~/ophcrack_tables/tables_xp_free_fast/" to move the downloaded zip file to our new directory structure. This step assumes that you downloaded the zip file to "/home/sansforensics/".

    4. Type "cd ~/ophcrack_tables/tables_xp_free_fast" followed by "unzip tables_xp_free_fast.zip" to extract the zip file to the "~/ophcrack_tables/tables_xp_free_fast/" directory.

    5. Assuming you've already done step 6 from the previous Volatility post and have obtained the XP hash password file (in "~/xp-passwd"), type "ophcrack -d ~/ophcrack_tables/ -t tables_xp_free_fast,0,1,2,3 -n 4 -f ~/xp-passwd -l ophcrack-vol-op.txt" to load/use all 4 tables to crack the "xp-passwd" hash file and then store the results in the "ophcrack-vol-op.txt" file in the current directory.

    The output at the command line will eventually look like:

    0h  4m 11s; search (98%); tables: total 4, done 3, using 1; pwd found 4/7.

    6. By typing "more ophcrack-vol-op.txt" we can see the actual results:

    15 hashes have been found in /home/sansforensics/xp-passwd.
    Opened 4 table(s) from /home/sansforensics/ophcrack_tables//tables_xp_free_fast,0,1,2,3.
    0h  0m  0s; Found empty password for user Guest (NT hash #1)
    0h  0m  0s; Found empty password for 2nd LM hash #4
    0h  0m  0s; Found empty password for user Sarah (NT hash #6)
    0h  0m  1s; Found password 6 for 2nd LM hash #0
    0h  0m 53s; Found password NEON199 for 1st LM hash #0in table XP free fast #2 at column 4645.
    0h  0m 53s; Found password Neon1996 for user Administrator (NT hash #0)
    0h  0m 59s; Found password NEON96 for 1st LM hash #4in table XP free fast #1 at column 4368.
    0h  0m 59s; Found password Neon96 for user phoenix (NT hash #4)
    0h  1m  6s; Found password JVYMGP1 for 2nd LM hash #2in table XP free fast #0 at column 4037.


    Results:

    username / hash                  LM password    NT password
    Administrator                    NEON1996       Neon1996
    Guest                            *** empty ***  *** empty ***
    HelpAssistant                    .......JVYMGP1 .......
    SUPPORT_388945a0                 *** empty ***  .......
    phoenix                          NEON96         Neon96
    ASPNET                           .............. .......
    Sarah                            *** empty ***  *** empty ***


    It looks like the NIST "HelpAssistant" password cracking attempt has failed - it might have special characters in it. You can also see it took A LOT less time than using John The Ripper (minutes vs hours) and that ophcrack provides the case-sensitive version of the passwords under "NT password" rather than the "LanManager" all caps version.
    Just FYI, ophcrack also has a nice Windows GUI available for download from sourceforge but it will only be as good as the rainbow tables you give it.

    So I think that about covers Windows Password cracking for now. Please feel free to suggest a new forensicatory area to investigate next.

    Writing a CCleaner RegRipper Plugin Part 1

    $
    0
    0
    Introduction

    Hello again!

    I thought I would do another multi-part post - this time we will use SysInternals ProcMon (v 2.96) monitoring software to investigate the CCleaner (v 3.14.1616) Windows Cleaner program. As this program gets mentioned quite often on the LifeHacker website, I thought I'd try it out. It cleans Temporary (Internet and Windows) files, Browser History (downloads and forms), Cookies, Recycle Bin, the Recent Documents list, old log files and old/unused Registry entries. Since testing completed, an update has been released (3.14.1643).
    Once we have finished our (very) basic analysis, we shall then write a RegRipper plugin (in Perl) and test it using the SIFT v2.12 VM. Hopefully, the CCleaner installer will leave some artifacts in the registry which we can then parse using our RegRipper plugin to prove that CCleaner was installed on the PC/see what user settings were set.

    You can get your mits on the ProcMon executable here.
    The CCleaner installer can be found here.

    About CCleaner

    CCleaner "Cleaner" Menu
    CCleaner "Registry" Menu

    After installing (for all users) and launching, there are 4 main menu buttons on the left hand side of the app window:
     - Cleaner (for selecting which Windows/Application log files to clean),
     - Registry (for selecting the Registry cleaning settings),
     - Tools (running Uninstallers, selecting Windows Startup programs, selecting Internet Explorer startup plugins, removing System Restore Points, setting the (free/whole drive) Drive Wiper settings) and
     - Options (Secure Delete and Wipe Free Space settings, Cookies to delete/keep, Folders to delete/exclude,  some further Advanced Settings).

    As the objective is to write a RegRipper plugin to detect CCleaner installation/settings, we won't be trying to recover any deleted files at this time. Nor will we be investigating the Registry clean of old/unused entries functionality.
    Just FYI - there is a "Secure Delete" overwrite setting (under Option-Settings) which can be used to overwrite deleted files from 1 to 35 times. This is not enabled by default but if it works as advertised, I suspect file recovery will be close to impossible.
    There is also another option to "Wipe MFT Free Space" but presumably the user must either call the inbuilt "Drive Wiper" tool explicitly OR check the "Wipe Free Space" Cleaner option (not checked by default). A warning does pop up when the check box is selected recommending it stay unchecked for normal use due to the extra time it takes.

    Further unchecked by default "Cleaner" Options include:
    - Autocomplete Form History
    - Saved Passwords
    - Network Passwords
    - DNS cache
    - Old Prefetch data (timeframe unclear)
    - tray notifications Cache
    - Window Size/Location cache
    - User Assist History

    Additionally, under Option-Advanced settings, the "Only delete files in Windows Temp folder older than 24 hours" option is checked by default.

    As you can see, there's a shedload of CCleaner functionality to investigate. Any takers?


    Using ProcMon to analyse CCleaner's Cleaner Function

    After launching the ProcMon.exe (does not require installation), the user is first shown a popup filter window. For now, we can "Cancel" it.
    In the remaining main window, the capture button (third button from the left) should have a magnifying glass with a red line through it meaning ProcMon is not currently capturing any events. If it doesn't have the red line, toggle the button to stop monitoring.

    Now we press the ProcMon capture button and launch the CCleaner program. In the lower part of the Cleaner option window, we press the "Analyze" button and when the analysis completes, we then press the "Run Cleaner" button.
    We then exit the CCleaner program and toggle off the capture button in ProcMon.
    In  ProcMon's File menu, we can save the capture for later review. I chose to save "All Events" in the native PML format.

    If you now look at what you've captured (or open the .PML logfile), you can filter out everything except CCleaner events by going to the Filter...filter sub menu  (or the Filter pop up window) and then set the drop boxes to read "Process Name" "is" "CCleaner.exe" (probably have to type process name directly) then "Include". Now hit the "Add" button and then the "Apply" and "OK" buttons. The Filter Window generally looks something like this:

    ProcMon Filter Window

     In my capture file, I still had ~200 000 events - clearly, more filtering is necessary.
    However, in a rush of excitement / not-so youthful exuberance I started wading into this event list anyway to try to figure out what CCleaner actually does. Here's a brief (and probably incomplete) overview:
    - Opens/Reads the CCleaner.exe prefetch file
    - Reads the C:\, C:\Documents and Settings, C:\Program Files, C:\Windows directories
    - Opens/Reads HKCU\Software\Piriform\CCleaner registry key
    - mostly opens/queries but also sets HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\MountPoints2 subkeys
    - tries to open various browsers profiles.ini files (presumably so it can find their cache/history files)
    - searches thru HKCU\Software looking for applications to clean
    - reads C:\RECYCLER
    - reads the following directories looking for log (and .dmp)  files :
     C:\WINDOWS\Temp, C:\Documents and Settings, C:\WINDOWS\Minidump, C:\WINDOWS\system32\wbem\Logs, C:\Documents and Settings\All Users\Application Data\Microsoft\Dr Watson, C:\WINDOWS\Debug,C:\WINDOWS\security\logs, C:\WINDOWS\Logs, C:\WINDOWS\Microsoft.NET
    - queries C:\WINDOWS\Prefetch directory
    - queries browser profile directories
    - calls WriteFile on C:\RECYCLER, C:\Documents and Settings\USERNAME\Recent\*.lnk files, index.dat files, Desktop.ini files, *.log files and (in this case) the Firefox Profiles directory.

    Thats kinda where I finished up / lost the will to keep going ...

    Getting back to our capture file, we shall try to limit the filter to Registry events only.
    Setup/Add/Apply another filter for "Operation" is "RegQueryValue" then "include".
    I just chose "RegQuery" value because it sounds like what CCleaner might use to read a CCleaner setting from the registry. There are actually loads more function names to choose from - see the MSDN documentation for registry function calls and for file system function calls.

    Anyhow ... you should now see about 1500 events from keys such as HKLM and HKCU.
    Scrolling down reveals ... events for the HKCU\Software\Piriform\CCleaner subkey and the HKLM\SOFTWARE\Piriform\CCleaner subkey.
    Aha! If CCleaner saves its configuration info to the Registry, we can now create a RegRipper plugin to look for those registry values.
    To better view these registry values, we can disable the previous filter and setup another filter - "Path contains Piriform" (also see ProcMon Filter Window pic above). The results indicate that the HKCU subkey contains our CCleaner configuration settings (eg WINDOW_WIDTH, WipeFreeSpaceDrives).

    "Path contains Piriform" Filtered Results

    So now we can see that if we create a RegRipper plugin to look for a Software\Piriform\CCleaner subkey and then parse all the values, we can determine what the User's settings were. Stay tuned viewers ... we'll do this in the next exciting installment.

    Writing a CCleaner RegRipper Plugin Part 2

    $
    0
    0

    Welcome Back Viewers!
    We now continue with our scheduled programming ... heh-heh...

    About RegRipper (on SIFT V2.12)

    RegRipper is written in Perl and is included with the SIFT VM. There are 3 main components to it:
    • the rip.pl Perl script located in /usr/local/bin
    • the various plugin scripts located in /usr/local/src/regripper/plugins
    • the Parse::Win32Registry library which is already installed/run (from source code at /usr/local/share/perl/5.10.0/Parse/Win32Registry)
    Harlan Carvey has written a "how to write a plugin" section in Windows Registry Forensics (pp.73-78).

    For more information on the methods/functions available for the Parse::Win32Registry, you can type:
    "man parse::win32registry" at a SIFT VM terminal window.

    RegRipper can be called at the SIFT command line, using something like:
    "rip.pl -r /cases/NTUSER.DAT -p ccleaner"

    *assuming "ccleaner.pl" exists in the plugin directory

    Typing "rip -h" gives the full syntax/explanation.

    Most plugins follow a similar pattern:
    1. Create a new Parse::Win32Registry object (passing the hive name as a parameter eg "NTUSER.DAT")
    2. Call the Parse::Win32Registry's "get_root_key" method
    3. Use the root key to call the "get_subkey" method (passing the key name as a parameter eg "Software\Piriform\CCleaner")
    4. Use "get_list_of_values" method to obtain the values under the subkey.
    Some Perls of Wisdom?

    A few Perls of note before we get stuck into coding:
    • "#" denotes comments. Anything after a # sign on the line is ignored by the Perl interpreter.
    • Each statement line should be terminated with a ";". A statement can be a declaration / assignment / function call.
    • "{ }" can be used to logically group multiple lines.
    • "sub" denotes subroutines (aka methods/functions).
    • A method / function can be passed a parameter using "( )" eg "function(x)" or it can be called without and use a default input argument eg in subroutines, "shift" defaults to use "@_".
    • "my" denotes local variables (they do not exist outside the containing "{ }" braces).
    • "$" denote scalar values (can be strings, numbers) and its the most basic / common variable Perl type.
    • "@" denotes an array / list of scalars.
    • "( )" can also be used to denote arrays/lists (see examples below).
    • "[i]" can be used to index items in arrays/lists (see examples below).
    • "%" denotes an associated array / hashmap (where each "value" in a list has a corresponding "key id").
    • "::rptMsg" is an existing RegRipper function to print out messages to the command line.
    • "." can be used to join strings and variables together.

    Some array examples:
    @x = (10, 20, 30);# sets up an array of numbers and stores it in @x
    $y = $x[1];# sets scalar y to equal the second scalar item in x (array index starts at 0). $x[1] is used instead of @x because we are assigning / using a scalar value contained in x. ie y = 20

    %fruits("apple" => 9,
            "banana" => 23,
            "cherry" =>11);# sets up an associated array (%) with "apple" being the "key id" and 9 being the "key value". Similarly, "banana" has a value of 23 etc.

    $fruits{"orange"} = 1; # will add the variable "orange" and its value of "1" to the "fruits" associated array

    delete($fruits{"orange"}); # will remove "orange" and its value from the "fruits" array

    @fruit_keys = keys(%fruits);# copies the key ids to the "fruit_keys" array using the perl "keys" function. "keys" returns an array list so that's why we use "@fruit_keys" instead of $fruit_keys or %fruitkeys.

    @fruit_values = values(%fruits);# copies the key values to the "fruit_values" array using the perl "values" function

    Some helpful Perl references are:
    • the official documentation here
    • good old wikipedia
    • and Chapter 16, Unix Unleashed 3ed. by Robin Burk (SAMS 1998) - which has a good summary of Perl. That's where I got the array examples from.
    Writing the RegRipper Plugin

    So after all that, I can tell you're just rearing to go ... and for those of you that stay - here's the code I came up with. Its based on the "warcraft3" plugin already included with SIFT V2.12. Note: the formatting might be a little off because of the blog column width causing long lines (typically comments) to wrap.

    # Start of Code

     #-----------------------------------------------------------
    # ccleaner.pl
    #   Gets CCleaner User Settings
    #
    # Change history
    #   20120128 Initial Version based on warcraft3.pl plugin
    #
    # References
    #
    #
    #-----------------------------------------------------------
    package ccleaner;
    use strict;

    my %config = (hive          => "NTUSER\.DAT",
                  hasShortDescr => 1,
                  hasDescr      => 0,
                  hasRefs       => 0,
                  osmask        => 22,
                  version       => 20120128);

    sub getConfig{return %config}
    sub getShortDescr {
        return "Gets User's CCleaner Settings";  
    }
    sub getDescr{}
    sub getRefs {}
    sub getHive {return $config{hive};}
    sub getVersion {return $config{version};}

    my $VERSION = getVersion();

    sub pluginmain {
        my $class = shift; # pops the first element off @_ ie the parameter array passed in to pluginmain
        my $hive = shift; # 1st element in @_ is class/package name (ccleaner), 2nd is the hive name passed in from rip.pl
        ::logMsg("Launching ccleaner v.".$VERSION);
        ::rptMsg("ccleaner v.".$VERSION);
        ::rptMsg("(".getHive().") ".getShortDescr()."\n");
        my $reg = Parse::Win32Registry->new($hive); # creates a Win32Registry object
        my $root_key = $reg->get_root_key;
        my $key;
        my $key_path = "Software\\Piriform\\CCleaner";
        # If CCleaner key_path exists ... ie get_subkey returns a non-empty value
        if ($key = $root_key->get_subkey($key_path)) {
            # Print registry key name and last modified date
            ::rptMsg($key_path);
            ::rptMsg("LastWrite Time ".gmtime($key->get_timestamp())." (UTC)");
            ::rptMsg("");
            my %cckeys; # temporary associative array for storing name / value pairs eg ("UpdateCheck", 1)
            # Extract ccleaner key values into ccvals array
            # Note: ccvals becomes an array of "Parse::Win32Registry::WinNT::Value"
            # As this is implemented in an Object oriented manner, we cannot access the values directly -
            # we have to use the "get_name" and "get_value" subroutines
            my @ccvals = $key->get_list_of_values();
            # If ccvals has any "Values" in it, call "Value::get_name" and "Value::get_data" for each
            # and store the results in the %cckeys associative array using data returned by Value::get_name as the id/index
            # and Value::get_data for the actual key value
            if (scalar(@ccvals) > 0) {
                foreach my $val (@ccvals) {
                    $cckeys{$val->get_name()} = $val->get_data();
                }
                # Sorts keynames into a temp list and then prints each key name + value in list order
                # the values are retrieved from cckeys assoc. array which was populated in the previous foreach loop
                foreach my $keyval (sort keys %cckeys) {
                    ::rptMsg($keyval." -> ".$cckeys{$keyval});
                }
            }
            else {
                ::rptMsg($key_path." has no values.");
            }
        }
        else {
            ::rptMsg($key_path." does not exist.");
        }
        # Return obligatory new-line
        ::rptMsg("");
    }

    1;


    # End of Code

    I have also included some screenshots of the code with line numbers so we can walk through it. Fun, fun, fun eh?
    Code Screenshot #1

    I used the SIFT's Ubuntu gedit file editor (by typing "gedit" at the command line) which has automatic Perl syntax highlighting, line numbers and multiple tabs. You can see the file path "/usr/local/src/regripper/plugins/" of the current file being edited ("ccleaner.pl") in the title bar.
    Comments are in blue, variable names are green, Perl keywords are brownish-orange, Perl functions are aqua. You can probably customize it further.
    As mentioned previously, I followed the same structure as the warcraft3.pl plugin so the first few lines are kinda standard / common to all plugins.

    Lines 1 - 11 are just comments
    Line 12 declares the name of this package/module.
    Line 13 is a directive to ensure we follow the Perl syntax rules
    Lines 15-20 declares a "config" associative array e.g. "hive" key having a value of "NTUSER\.DAT" etc.
    The "\" is required so Perl doesn't interpret the "." as something else.
    These are standard RegRipper variables that can later be accessed using the subroutines declared on lines 22 - 31.


    Code Screenshot #2

    Every RegRipper plugin must declare a "pluginmain" subroutine. This particular one will be called whenever you specify the "-p ccleaner" option.
    Lines 34-35: We see two local (ie "my") variables declared. These variables don't exist outside of our "pluginmain". The "pluginmain" is called in / by "rip.pl" which also passes in the hive name parameter (eg NTUSER.DAT).
    The parameter is available via the inbuilt Perl array "@_". To get to the hive name we need to pop/shift the first item off the "@_" array (ie the "ccleaner" package name). I'm not really sure why the package name was added to the "@_" array when "rip.pl" only passes in the hive name. Anyway, we call "shift" again to get our hive value (NTUSER.DAT).
    Lines 36-38: Prints out "Launching ccleaner" and the version info. This seems to be the standard thing to do.
    Lines 39-40: Creates a new "Parse::Win32Registry" object using the hive we specified at the command line (via "-r /cases/NTUSER.DAT"). Then we get a root key object (storing it in "root_key") so we can then use it later to get to our CCleaner key.
    Lines 41-42: We declare a local variable "key" (to be used later) and also declare a "key_path" scalar containing the actual Software registry key we are interested in (ie Software\\Piriform\\CCleaner).
    Line 44: If our "root_key" get_subkey method returns a result (stored in "key") we can then parse through / print the values otherwise execution skips to line 73 and we print out "Software\Piriform\CCleaner does not exist"
    Lines 46-48: Prints out the "key_path" ("Software\Piriform\CCleaner") and the last write time.
    Lines 49 and 54: We create a new local associative array called "cckeys" and a new local array called "ccvals". We then use the returned "key" variable to call "get_list_of_values" and store the result in "ccvals". "ccvals" should now contain multiple variables of the "Parse:Win32Registry::WinNT::Value" type. We can't access the registry key names / values directly - we have to use the library supplied "get_name" and "get_data" subroutines.
    Lines 58-70: We check to see if we have any "Value" items in the "ccvals" array and if we don't, execution skips to line 69 and we print out "Software\Piriform\CCleaner has no values". If we do have items in the "ccvals" array, we iterate through each item and store the name and value (eg "UpdateCheck" and the value "1") in the "cckeys" associative array using the name as the index / id. Then on lines 64-65, we get a list of these key ids (eg "UpdateCheck") and sort the list using the inbuilt Perl "sort" function. We iterate through this sorted list for each key id and we print out the key id (eg "UpdateCheck") and then a " -> " followed by the key value (eg "1"). Execution then goes to line 76 where we print out an empty line and the package returns a value of "1" by convention.

    We can now copy / save this script into the plugin directory, type "chmod a+x /usr/local/src/regripper/plugins/ccleaner.pl" to make it executable and then launch RegRipper with our new ccleaner plugin using:
    "rip.pl -p ccleaner -r /cases/NTUSER.DAT". *Assuming the relevant NTUSER.DAT is in /cases/.

    Here is the output of "ccleaner.pl" when run against a NTUSER.DAT containing CCleaner keys:

    Launching ccleaner v.20120128
    ccleaner v.20120128
    (NTUSER.DAT) Gets User's CCleaner Settings


    Software\Piriform\CCleaner
    LastWrite Time Fri Jan  6 05:52:48 2012 (UTC)


    (App)Autocomplete Form History -> True
    (App)Custom Folders -> False
    (App)DNS Cache -> True
    (App)Desktop Shortcuts -> False
    (App)FTP Accounts -> True
    (App)Google Chrome - Saved Form Information -> True
    (App)Google Chrome - Saved Passwords -> True
    (App)Hotfix Uninstallers -> False
    (App)IIS Log Files -> False
    (App)Last Download Location -> True
    (App)MS Photo Editor -> True
    (App)Mozilla - Saved Form Information -> True
    (App)Mozilla - Saved Passwords -> True
    (App)Mozilla - Site Preferences -> True
    (App)Notepad++ -> False
    (App)Old Prefetch data -> True
    (App)Saved Passwords -> True
    (App)Start Menu Shortcuts -> False
    (App)Sun Java -> True
    (App)Tray Notifications Cache -> False
    (App)Wipe Free Space -> False
    BackupPrompt -> 0
    DefaultDetailedView -> 1
    DelayTemp -> 1
    FFDetailed -> 1
    IEDetailed -> 1
    Language -> 1033
    MSG_CONFIRMCLEAN -> False
    MSG_WARNMOZCACHE -> False
    SecureDeleteMethod -> 0
    SecureDeleteType -> 1
    UpdateCheck -> 1
    UpdateKey -> 01/06/2012 04:52:48 PM
    WINDOW_HEIGHT -> 679
    WINDOW_LEFT -> 329
    WINDOW_MAX -> 0
    WINDOW_TOP -> 23
    WINDOW_WIDTH -> 1202
    WipeFreeSpaceDrives -> C:\|F:\

    Here's the output when we run the ccleaner plugin against an NTUSER.DAT which doesn't have any CCleaner artifacts (eg "rip.pl -p ccleaner -r /mnt/m57jean/Documents\ and\ Settings/Jean/NTUSER.DAT"):

    Launching ccleaner v.20120128
    ccleaner v.20120128
    (NTUSER.DAT) Gets User's CCleaner Settings


    Software\Piriform\CCleaner does not exist.

    And so that concludes our little exercise ... it wasn't too complicated in the end eh? My main goal was to write a simple RegRipper plugin and I think I did that OK. However, I think further ProcMon analysis of CCleaner could prove interesting. Anyway that's all we have time for today folks. As always, let me know what you think in the Comments section. I won't hold my breath lol ...




    Diving in to Perl with GeoTags and GoogleMaps

    $
    0
    0

    Girl, Unallocated recently posted a guide to plotting geotag data using exiftool and Google Earthhere.

    GoogleMaps also has some info about how to plot lat / long coordinates along with an info box on a map here.
    The example URL given looks like:
    http://maps.google.com/maps?q=37.771008,+-122.41175+%28You+can+insert+your+text+here%29&iwloc=A&hl=en
    You can see that the latitude parameter comes first in floating point (eg 37.771008) followed by the longitude and then a text field ("You can insert your text here") to be printed in the information popup. The "iwloc" parameter shows the GoogleMaps point label ("A"). The "hl" parameter is the language to use (en = English).

    So ... I'd thought I'd build on (*cough RIPOFF *cough) those posts  and write a basic Perl script to automatically extract the EXIF location data from a photo and then formulate an URL the user can then paste into their Internet Browser. The SIFT VM already has the exiftool Perl module and Perl 5 installed so it should be simple eh?

    Method

    At first, I thought I would filter/convert the output text of "exiftool" but after hunting around at www.cpan.org (the Perl package repository), I discovered the Image::ExifTool::Location module which provides some easy to use functions to retrieve lat / long coordinates from EXIF data.

    In order to use this though, we first have to install it on SIFT. Here's the guide.
    Its pretty simple though, you just type:
    "sudo cpan Module::Name"
    to get the cpan installer to automagically retrieve and install the module you're after. I added in the "sudo" so the installations aren't hindered by file permission issues.

    Looking at the documentation for the Image::ExifTool::Location module shows that it requires/depends on the Image::ExifTool and the Geo::Coordinates::DecimalDegrees modules. The Image::ExifTool module is already installed on SIFT - so we will install the Geo::Coordinates::DecimalDegrees module before installing the Image::ExifTool::Location module just in case.

    At a terminal window type:
    "sudo cpan Geo::Coordinates::DecimalDegrees"
    followed by
    "sudo cpan Image::ExifTool::Location"

    At this point, I had a bit of of an epiphany / scope creep when I thought "Wouldn't it be cool if you could also output the URLs for multiple files as links in a single HTML file?". The user could then open the HTML file and click on the various links to see the corresponding Google Map. No cutting and pasting of URLs and if there's a lot of pics with geotag information, it could make things a little easier for our fearless forensicators.

    Fortunately, there's already a Perl module to create an HTML table - its called HTML::QuickTable. We can install it by typing "sudo cpan HTML::QuickTable" at the command line.

    After we've completed the initial setup, we can now start writing our script (finally!). I called it "exif2map.pl" and put it in "/usr/local/bin/". Note: Don't forget to to make the file executable by typing "chmod a+x /usr/local/bin/exif2map.pl".


    The Code

    Here's the code I mangled came up with:

    #START CODE
    #!/usr/bin/perl -w

    # Perl script to take the output of exiftool and conjure up a web link
    # to google maps if the image has stored GPS lat/long info.

    use strict;

    use Image::ExifTool;
    use Image::ExifTool::Location;
    use Getopt::Long;
    use HTML::QuickTable;

    my $help = '';
    my $htmloutput = '';
    my @filenames;
    my %file_listing;

    GetOptions('help|h' => \$help,
        'html' => \$htmloutput,
        'f=s@' => \@filenames);

    if ($help||@filenames == 0)
    {
        print("\nexif2map.pl v2012.02.16\n");
        print("Perl script to take the output of exiftool and conjure up a web link\n");
        print("to google maps if the image has stored GPS lat/long info.\n");

        print("\nUsage: exif2map.pl [-h|help] [-f filename] [-html]\n");
        print("-h|help ....... Help (print this information). Does not run anything else.\n");
        print("-f filename ... File(s) to extract lat/long from\n");
        print("-html ......... Also output results as a timestamped html file in current directory\n");

        print("\nExample: exif2map.pl -f /cases/galloping-gonzo.jpg");
        print("\nExample: exif2map.pl -f /cases/krazy-kermit.jpg -f/cases/flying-piggy.jpg -html\n\n");
        print("Note: Outputs results to command line and (if specified) to a timestamped html file\n");
        print("in the current directory (e.g. exif2map-output-TIMESTAMP.html)\n\n");
       
        exit;
    }


    # Main processing loop
    print("\nexif2map.pl v2012.02.16\n");
    foreach my $name (@filenames)
    {
        ProcessFilename($name);
    }

    # If html output required AND we have actually retrieved some data ...
    if ( ($htmloutput) && (keys(%file_listing) > 0) )
    {   
        #timestamped output filename
        my $htmloutputfile = "exif2map-output-".time.".html";

        open(my $html_output_file, ">".$htmloutputfile) || die("Unable to open $htmloutputfile for writing\n");

        my $htmltable = HTML::QuickTable->new(border => 1, labels => 1);

        # Added preceding "/" to "Filename" so that the HTML::QuickTable sorting doesn't result in
        # the column headings being re-ordered after / below a filename beginning with a "\".
        $file_listing{"/Filename"} = "GoogleMaps Link";

        print $html_output_file "<HTML>";
        print $html_output_file $htmltable->render(\%file_listing);
        print $html_output_file "<\/HTML>";

        close($htmloutputfile);
    }

    sub ProcessFilename
    {
        my $filename = shift;

        if (-e $filename) #file must exist
        {
            my $exif = Image::ExifTool->new();
            # Extract all info from existing image
            if ($exif->ExtractInfo($filename))
            {
                # Ensure all 4 GPS params are present
                # ie GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef
                # The Ref values indicate North/South and East/West
                if ($exif->HasLocation())
                {
                    my ($lat, $lon) = $exif->GetLocation();
                    print("\n$filename contains Lat: $lat, Long: $lon\n");
                    print("URL: http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\n");
                    if ($htmloutput) # save GoogleMaps URL to global hashmap indexed by filename
                    {
                        $file_listing{$filename} = "<A HREF = \"http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\"> http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en</A>";
                    }
                    return 1;
                }
                else
                {
                    print("\n$filename : No Location Info available!\n");
                    return 0;
                }
            }
            else
            {
                print("\n$filename : Cannot Extract Info!\n");
                return 0;
            }
        }
        else
        {
            print("\n$filename does not exist!\n");
            return 0;
        }
    }

    #END CODE

    A brief explanation? The first section is getting the command line args (GetOptions) and printing help if the user stuffs up. The next section is the main processing loop which takes the list of filenames and calls our ProcessFilename subroutine for each. The results will be printed AND stored in the %file_listing hash (contains the filename and the URL).
    The next section handles writing the HTML file (if required) and uses the HTML::QuickTable module to come up with the HTML for the table. We then print a preceding <HTML> tag,  the table HTML and the trailing </HTML> tag all to a timestamped file in the users current directory called "exif2map-output-TIME.html". Where TIME is the number of seconds since 1970.


    Testing

    Here's a sample geotagged image from wikipedia:
    I've saved it into "/cases/" along with a .jpg file with no EXIF geotag information (Cheeky4n6Monkey.jpg) and a mystery geotagged file (wheres-Cheeky4n6Monkey.jpg). I prepared this mystery file on the sly earlier (using a separate but similar Perl script). Note: Blogger seems to be stripping out the GPS data (maybe I missed setting some other parameters?) of the pic below but I did actually manage to set the GPS info on a local copy. Honest!


    Mystery file - "wheres-Cheeky4n6Monkey.jpg"
     
    Lets see what happens when we run our script ...

    sansforensics@SIFT-Workstation:~$ exif2map.pl -f /cases/wheres-Cheeky4n6Monkey.jpg -f /cases/GPS_location_stamped_with_GPStamper.jpg  -f /cases/Cheeky4n6Monkey.jpg -html

    exif2map.pl v2012.02.16

    /cases/wheres-Cheeky4n6Monkey.jpg contains Lat: 36.1147630001389, Long: -115.172811
    URL: http://maps.google.com/maps?q=36.1147630001389,+-115.172811(/cases/wheres-Cheeky4n6Monkey.jpg)&iwloc=A&hl=en

    /cases/GPS_location_stamped_with_GPStamper.jpg contains Lat: 41.888948, Long: -87.624494
    URL: http://maps.google.com/maps?q=41.888948,+-87.624494(/cases/GPS_location_stamped_with_GPStamper.jpg)&iwloc=A&hl=en

    /cases/Cheeky4n6Monkey.jpg : No Location Info available!
    sansforensics@SIFT-Workstation:~$


    You can see that the script found/printed GPS positions and URLs for our mystery file wheres-Cheeky4n6Monkey.jpg and GPS_location_stamped_with_GPStamper.jpg but nothing for Cheeky4n6Monkey.jpg.

    In Firefox, The outputted HTML file looks like:

    Output HTML Table

    Notice how we don't have a table entry for the file with no GPS info (Cheeky4n6Monkey.jpg)? I thought it wouldn't make sense as the whole purpose was to plot EXIF GPS data.
    Anyhow, when we follow the table link for "GPS_location_stamped_with_GPStamper.jpg" we get:

    "GPS_Location_stamped_withGPStamper.jpg" plotted on GoogleMaps

    Aha! Chicago eh? No surprise when you look at the picture. Go Cubbies !


     "wheres-Cheeky4n6Monkey.jpg" gives:

     "wheres-Cheeky4n6Monkey.jpg" plotted on GoogleMaps


     36.114763,-115.172811 = Vegas Baby!  Uh-oh, what have you been up to Cheek4n6Monkey?!

    Like any good forensicator we should validate our results. We can check our script's output against the "Exif Viewer" Firefox plugin (already installed on SIFT):

    "Exif Viewer" of "GPS_Location_stamped_withGPStamper.jpg"


    "Exif Viewer" of "wheres-Cheeky4n6Monkey.jpg"

    Hooray! Our script output matches Exif Viewer's results for Latitude and Longitude. In fact, I think "Exif Viewer" has rounded off a few digits of precision.

    So that's about it folks - all of this was not in vain. There might be some further formatting to be done to make it look prettier but the basic functionality has been achieved. Cool bananas eh? Let me know what your think - comments/suggestions/curses?


    Making "exif2map.pl" recursively search

    $
    0
    0

    Recently Doppiamunnezza commented that it might be helpful if we could point the exif2map.pl script at a folder and have it automagically search all files below that for EXIF geotag data.

    Being the code-monkey hack that I am, here's my quick/dirty solution ...

    Code Listing

    # START CODE


    #!/usr/bin/perl -w

    # Perl script to take the output of exiftool and conjure up a web link
    # to google maps if the image has stored GPS lat/long info.

    use strict;

    use Image::ExifTool;
    use Image::ExifTool::Location;
    use Getopt::Long;
    use HTML::QuickTable;
    use File::Find;

    # commented out for now - apparently File:Find can issue some weird warnings
    #no warnings 'File::Find';

    my $version = "exif2map.pl v2012.02.21";
    my $help = ''; # help flag
    my $htmloutput = ''; #html flag
    my @filenames; # input files from -f flag
    my @directories; # input directories from -dir flag (must use absolute paths)

    my %file_listing; # stored results

    GetOptions('help|h' => \$help,
        'html' => \$htmloutput,
        'f=s@' => \@filenames,
        'dir=s@' => \@directories);

    if ($help||(@filenames == 0 && @directories == 0))
    {
        print("\n$version\n");
        print("Perl script to take the output of exiftool and conjure up a web link\n");
        print("to google maps if the image has stored GPS lat/long info.\n");

        print("\nUsage: exif2map.pl [-h|help] [-f filename] [-html]\n");
        print("-h|help .......... Help (print this information). Does not run anything else.\n");
        print("-f filename ...... File(s) to extract lat/long from\n");
        print("-dir directory ... Absolute path to folder containing file(s) to extract lat/long from\n");
        print("-html ............ Also output results as a timestamped html file in current directory\n");

        print("\nExample: exif2map.pl -f /cases/galloping-gonzo.jpg");
        print("\nExample: exif2map.pl -f /cases/krazy-kermit.jpg -dir /cases/rockin-rowlf-pics/ -html\n\n");
        print("Note: Outputs results to command line and (if specified) to a timestamped html file\n");
        print("in the current directory (e.g. exif2map-output-TIMESTAMP.html)\n\n");
       
        exit;
    }

    # Main processing loop
    print("\n$version\n");

    # Process filenames specified using the -f flag first
    if (@filenames)
    {
        foreach my $name (@filenames)
        {
            ProcessFilename($name);
        }
    }

    # Process folders specified using the -dir flag
    # Note: Will NOT follow symbolic links to files
    if (@directories)
    {
        find(\&ProcessDir, @directories);
    }

    # If html output required AND we have actually retrieved some data ...
    if ( ($htmloutput) && (keys(%file_listing) > 0) )
    {   
        #timestamped output filename
        my $htmloutputfile = "exif2map-output-".time.".html";

        open(my $html_output_file, ">".$htmloutputfile) || die("Unable to open $htmloutputfile for writing\n");

        my $htmltable = HTML::QuickTable->new(border => 1, labels => 1);

        # Added preceeding "/" to "Filename" so that the HTML::QuickTable sorting doesn't result in
        # the column headings being re-ordered after / below a filename beginning with a "\".
        $file_listing{"/Filename"} = "GoogleMaps Link";

        print $html_output_file "<HTML>";
        print $html_output_file $htmltable->render(\%file_listing);
        print $html_output_file "<\/HTML>";

        close($htmloutputfile);
        print("\nPlease refer to \"$htmloutputfile\" for a clickable link output table\n\n");
    }

    sub ProcessFilename
    {
        my $filename = shift;

        if (-e $filename) #file must exist
        {
            my $exif = Image::ExifTool->new();
            # Extract all info from existing image
            if ($exif->ExtractInfo($filename))
            {
                # Ensure all 4 GPS params are present
                # ie GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef
                # The Ref values indicate North/South and East/West
                if ($exif->HasLocation())
                {
                    my ($lat, $lon) = $exif->GetLocation();
                    print("\n$filename contains Lat: $lat, Long: $lon\n");
                    print("URL: http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\n");
                    if ($htmloutput) # save GoogleMaps URL to global hashmap indexed by filename
                    {
                        $file_listing{$filename} = "<A HREF = \"http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\"> http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en</A>";
                    }
                    return 1;
                }
                else
                {
                    print("\n$filename : No Location Info available!\n");
                    return 0;
                }
            }
            else
            {
                print("\n$filename : Cannot Extract Info!\n");
                return 0;
            }
        }
        else
        {
            print("\n$filename does not exist!\n");
            return 0;
        }
    }

    sub ProcessDir
    {
        # $File::Find::dir is the current directory name,
        # $_ is the current filename within that directory
        # $File::Find::name is the complete pathname to the file.
        my $filename = $File::Find::name; # should contain absolute path eg /cases/pics/krazy-kermit.jpg

        if (-f $filename) # must be a file not a directory name ...
        {
            my $exif = Image::ExifTool->new();
            # Extract all info from existing image
            if ($exif->ExtractInfo($filename))
            {
                # Ensure all 4 GPS params are present
                # ie GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef
                # The Ref values indicate North/South and East/West
                if ($exif->HasLocation())
                {
                    my ($lat, $lon) = $exif->GetLocation();
                    print("\n$filename contains Lat: $lat, Long: $lon\n");
                    print("URL: http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\n");
                    if ($htmloutput) # save GoogleMaps URL to global hashmap indexed by filename
                    {
                        $file_listing{$filename} = "<A HREF = \"http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\"> http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en</A>";
                    }
                    return 1;
                }
                else
                {
                    print("\n$filename : No Location Info available!\n");
                    return 0;
                }
            }
            else
            {
                print("\n$filename : Cannot Extract Info!\n");
                return 0;
            }
        }
    }

    # END CODE


    Code Summary

    The code mostly works as before - I've just added some extra code to handle any user specified folders.
    It could probably be re-written so only one function was required but I reckon the code would become a bit harder to explain/understand. Plus, I can be a pretty lazy monkey ;)
    Anyhoo, I've added a function called "ProcessDir" which gets called for each file/directory under the user specified folder(s). It is essentially the same code as "ProcessFilename" except it derives the filenames from the File::Find module. The File::Find's "find" function (ie "find(\&ProcessDir, @directories);") will search the given directories array and then call "ProcessDir" for each directory/file found. Consequently, "ProcessDir" should only call our EXIF checks if it's looking at a file (ie that's what the "if (-f $filename)" condition is for). The good news is the "File::Find" module is already loaded on the SIFT VM so we don't need to explicitly install it.
    Apart from those changes, the command line output now also tells the user the output HTML filename (if the -html flag is set).
    One caveat I noticed during testing was that folder names MUST use ABSOLUTE paths (eg "/home/sansforensics/testpics"). Otherwise, the file test mentioned above fails.


    Testing:

    Here's the file/folder structure I set up for testing:

    /home/sansforensics/wheres-Cheeky4n6Monkey.jpg
    /home/sansforensics/testpics/case1/Vodafone710.jpg
    /home/sansforensics/testpics/case1/wheres-Cheeky4n6Monkey.jpg
    /home/sansforensics/testpics/case1/subpics/GPS_location_stamped_with_GPStamper.jpg
    /home/sansforensics/testpics/case2/wheres-Cheeky4n6Monkey.jpg

    The various "wheres-Cheeky4n6Monkey.jpg" copies have GPS Lat/Long info as does "GPS_location_stamped_with_GPStamper.jpg". I also added in "Vodafone710.jpg" which has no GPS Lat/Long data. Both of our tests will be run from "/home/sansforensics/".

    For the first test, we shall specify a single file from the current directory ("wheres-Cheeky4n6Monkey.jpg") and also the "case1" folder which contains some images ("Vodafone710.jpg" and another copy of "wheres-Cheeky4n6Monkey.jpg").  The "case1" folder also contains a sub folder containing another image ("case1/subpics/GPS_location_stamped_with_GPStamper.jpg").

    Here's the command line input/output ...

    sansforensics@SIFT-Workstation:~$ exif2map.pl -dir /home/sansforensics/testpics/case1/ -f wheres-Cheeky4n6Monkey.jpg -html


    exif2map.pl v2012.02.21


    wheres-Cheeky4n6Monkey.jpg contains Lat: 36.1147630001389, Long: -115.172811
    URL: http://maps.google.com/maps?q=36.1147630001389,+-115.172811(wheres-Cheeky4n6Monkey.jpg)&iwloc=A&hl=en


    /home/sansforensics/testpics/case1/Vodafone710.jpg : No Location Info available!


    /home/sansforensics/testpics/case1/wheres-Cheeky4n6Monkey.jpg contains Lat: 36.1147630001389, Long: -115.172811
    URL: http://maps.google.com/maps?q=36.1147630001389,+-115.172811(/home/sansforensics/testpics/case1/wheres-Cheeky4n6Monkey.jpg)&iwloc=A&hl=en


    /home/sansforensics/testpics/case1/subpics/GPS_location_stamped_with_GPStamper.jpg contains Lat: 41.888948, Long: -87.624494
    URL: http://maps.google.com/maps?q=41.888948,+-87.624494(/home/sansforensics/testpics/case1/subpics/GPS_location_stamped_with_GPStamper.jpg)&iwloc=A&hl=en


    Please refer to "exif2map-output-1329903003.html" for a clickable link output table


    sansforensics@SIFT-Workstation:~$


    The output file "exif2map-output-1329903003.html" looks like:

    Results for "case1" Folder + 1 x Local File

    For the second test, we now specify the parent "testpics" directory (plus the local "wheres-Cheeky4n6Monkey.jpg" file). The script should pick up both "case1" and "case2" directories.

    sansforensics@SIFT-Workstation:~$ exif2map.pl -dir /home/sansforensics/testpics/ -f wheres-Cheeky4n6Monkey.jpg -html


    exif2map.pl v2012.02.21


    wheres-Cheeky4n6Monkey.jpg contains Lat: 36.1147630001389, Long: -115.172811
    URL: http://maps.google.com/maps?q=36.1147630001389,+-115.172811(wheres-Cheeky4n6Monkey.jpg)&iwloc=A&hl=en


    /home/sansforensics/testpics/case1/Vodafone710.jpg : No Location Info available!


    /home/sansforensics/testpics/case1/wheres-Cheeky4n6Monkey.jpg contains Lat: 36.1147630001389, Long: -115.172811
    URL: http://maps.google.com/maps?q=36.1147630001389,+-115.172811(/home/sansforensics/testpics/case1/wheres-Cheeky4n6Monkey.jpg)&iwloc=A&hl=en


    /home/sansforensics/testpics/case1/subpics/GPS_location_stamped_with_GPStamper.jpg contains Lat: 41.888948, Long: -87.624494
    URL: http://maps.google.com/maps?q=41.888948,+-87.624494(/home/sansforensics/testpics/case1/subpics/GPS_location_stamped_with_GPStamper.jpg)&iwloc=A&hl=en


    /home/sansforensics/testpics/case2/wheres-Cheeky4n6Monkey.jpg contains Lat: 36.1147630001389, Long: -115.172811
    URL: http://maps.google.com/maps?q=36.1147630001389,+-115.172811(/home/sansforensics/testpics/case2/wheres-Cheeky4n6Monkey.jpg)&iwloc=A&hl=en


    Please refer to "exif2map-output-1329903236.html" for a clickable link output table


    sansforensics@SIFT-Workstation:~$

    This produces the following output HTML file:

    Results for "testpics" Parent Folder + 1 x Local File

    We can see that our newly modified "exif2map.pl" script can now also recursively search given folders for files with EXIF GPS Lat/Long info. Hooray!
    The good thing about Perl is that someone has probably already thought of what you might need and has already written a module for it (eg File::Find). I highly recommend searching CPAN  before starting any new project.

    (Monkey) Carvings of Unknown File Types with Scalpel / Foremost on SIFT

    $
    0
    0

    Thierry13 recently requested we look into file carving - specifically, how do we carve for a non-standard / unknown files. For the scalpel and foremost carving utilities (both on SIFT) it's monkey's play!

    FYI There's another file carving utility on SIFT called photorec but this won't handle new unknown files only certain image/movie/document/archived files.

    Anyhoo, in order to configure scalpel / foremost, you must first have an idea of:
    - the file extension (if required eg .doc),
    - the maximum size of a potential file,
    - the file header signature (and optional file footer signature).

    You enter these parameters into a "scalpel.conf" or "foremost.conf" file before running the respective executable.
    As scalpel was derived from foremost, the .conf files look very similar. You can usually cut/paste the same rule for both.
    By default the executable will look for its .conf file in the current working directory but you can also tell it which .conf file to use.

    Here's how I prepared for testing all of the above:
    I plugged in freshly formatted 512 Mb USB Memory stick (in Windows XP).
    I downloaded (for free):
    - WinHex 
    - FTK Imager

    We will now launch WinHex to create our mystery file (eg "cheeky-file.c4n6") and then copy it to the USB stick.

    1. In WinHex, go to "File" ... "New" ... and select 1024 bytes in the resultant popup. Press "OK".

    2. Press Ctrl-L to fill the file with data. At the subsequent popup, use the default "Simple pseudo-random numbers" radio button and hit "OK".

    3. Add in our file header and footer data. You can simply click at the beginning of the file and type C4N6 like the screenshot below. Note: I clicked in the right most column of the corresponding hex digits to edit.
      Editing the Mystery File Header Signature

      4. Similarly, click 6 bytes/characters from the end of the file and type MONKEY.

      Editing the Mystery File Footer Signature

      5. Now go to "File" ... "Save" and call the file something like "cheeky-file.c4n6". Save it to BOTH the USB stick AND the local hard disk.

      6. Using Windows File Explorer, we delete the mystery file from the USB stick (eg Shift-Delete).

      7. Now we take a physical image of the USB stick using FTK Imager.
      In FTK Imager go to "File" ... "Create Disk Image". Select "Physical Drive" then "Next". Select the USB drive in the drop-menu and click "Finish".
      Click the "Add" button then choose the Raw(dd) Destination Image Type. Press "Next". Press "Next" again to skip entering the case details.
      Enter in a save location/filename (eg "usb512") for the image and press "Finish". Now click "Start".
      It was at this point I got a couple of failures. After I unplugged another USB device (curse you Madden Game Controller!), I was then able to save the whole image.
      There should now be a file (eg "usb512.001") with a corresponding audit log ("usb512.001.txt") containing the MD5 hash(es).

      8. Now launch the SANS SIFT VM (v2.12).

      9. Using Windows File Explorer, copy the image (eg "usb512.001") to the SIFT VM "/cases/" directory.
      Also copy the new mystery file (eg "cheeky-file.c4n6") to "/cases/" so we can MD5 hash compare it with any subsequently carved results.

      10. In a new SIFT terminal window, we should check the MD5 hash of the USB image by typing:
       "md5sum /cases/usb512.001".
      The terminal window should look something like:

      sansforensics@SIFT-Workstation:~$ md5sum /cases/usb512.001
      85cc5e5ef0b44c314da7dfc9954236f6  /cases/usb512.001
      sansforensics@SIFT-Workstation:~$

      We can then compare this to the "usb512.001.txt" audit file generated previously by FTK Imager (i.e.
      "MD5 checksum:    85cc5e5ef0b44c314da7dfc9954236f6")

      Cool bananas! Our image has not changed after being copied over.

      11. Now we set up local editable copies of the "scalpel.conf" / "foremost.conf" files. Assuming the current directory is "/home/sansforensics/", type "cp /usr/local/etc/foremost.conf ." and "cp /usr/local/etc/scalpel.conf ."
      Note: I found these existing .conf files on SIFT by typing "sudo find / -name scalpel.conf -print". There were two entries returned, so I picked the largest and most latest one. Ditto for the "foremost.conf" file. By default both .conf files have all their rules commented out. Which brings us to ...

      12. Using gedit (eg "gedit foremost.conf &" to launch gedit in the background), add the following lines to the "foremost.conf" files (screenshot):

      #Cheeky4n6Monkey Test file
          c4n6    y    2048    C4N6    MONKEY


      This line is saying "If you find a header equal to C4N6 (case sensitive) and a footer equal to MONKEY (case sensitive) within 2048 bytes, retrieve the data and label it with the .c4n6 extension"
      Note: The # sign means that line is a comment. Also note, there's a TAB between columns.


      Editing "foremost.conf"

      13. Similarly, edit the "scalpel.conf" file.

      Editing "scalpel.conf"

      14. Once we save the two .conf files, we can now run the carvers. For foremost the command line will look like:

      sansforensics@SIFT-Workstation:~$ foremost -c foremost.conf -o usb512-foremost -i /cases/usb512.001
      Processing: /cases/usb512.001
      |*****|
      sansforensics@SIFT-Workstation:~$

      We are telling foremost to use the "foremost.conf" file in the current directory (this option is not strictly required) to carve the "/cases/usb512.001" file and store the results in the current directory under the "usb512-foremost" sub-directory .

      Looking in the output directory yields:

      sansforensics@SIFT-Workstation:~$ ls usb512-foremost/
      audit.txt  c4n6
      sansforensics@SIFT-Workstation:~$ ls usb512-foremost/c4n6/
      00000640.c4n6

      Hooray! It looks like foremost found our mystery file and stored it in its own file-type specific directory ("c4n6").
      To verify this, lets compare MD5 hashes of the foremost recovered file and the file we copied over earlier ("/cases/cheeky-file.c4n6").

      sansforensics@SIFT-Workstation:~$ md5sum usb512-foremost/c4n6/00000640.c4n6
      94b4265826825763fbf8c661fa04ac1c  usb512-foremost/c4n6/00000640.c4n6
      sansforensics@SIFT-Workstation:~$
      sansforensics@SIFT-Workstation:~$ md5sum /cases/cheeky-file.c4n6
      94b4265826825763fbf8c661fa04ac1c  /cases/cheeky-file.c4n6
      sansforensics@SIFT-Workstation:~$

      And we have a MATCH!

      Similarly, for scalpel, the command line will look something like:

      sansforensics@SIFT-Workstation:~$ scalpel -c scalpel.conf -o usb512-scalpel /cases/usb512.001
      Scalpel version 2.0
      Written by Golden G. Richard III and Lodovico Marziale.
      Multi-core CPU threading model enabled.
      Initializing thread group data structures.
      Creating threads...
      Thread creation completed.


      Opening target "/cases/usb512.001"


      Image file pass 1/2.
      /cases/usb512.001: 100.0% |*****************************|  490.0 MB    00:00 ETAAllocating work queues...
      Work queues allocation complete. Building work queues...
      Work queues built.  Workload:
      c4n6 with header "C4N6" and footer "MONKEY" --> 1 files
      Carving files from image.
      Image file pass 2/2.
      /cases/usb512.001: 100.0% |*****************************|  490.0 MB    00:00 ETAProcessing of image file complete. Cleaning up...
      Done.
      Scalpel is done, files carved = 1, elapsed  = 7 secs.
      sansforensics@SIFT-Workstation:~$

      Note: The scalpel arguments take a slightly different format - there's no "-i" flag before the source file.

      Now looking in the output directory yields:

      sansforensics@SIFT-Workstation:~$ ls usb512-scalpel/
      audit.txt  c4n6-0-0/ 
      sansforensics@SIFT-Workstation:~$ ls usb512-scalpel/c4n6-0-0/
      00000000.c4n6

      And comparing MD5 hashes yields:

      sansforensics@SIFT-Workstation:~$ md5sum usb512-scalpel/c4n6-0-0/00000000.c4n6
      94b4265826825763fbf8c661fa04ac1c  usb512-scalpel/c4n6-0-0/00000000.c4n6
      sansforensics@SIFT-Workstation:~$
      sansforensics@SIFT-Workstation:~$ md5sum /cases/cheeky-file.c4n6
      94b4265826825763fbf8c661fa04ac1c  /cases/cheeky-file.c4n6
      sansforensics@SIFT-Workstation:~$

      Another Match!

      So that was pretty monkey proof eh? We have made up our own mystery file, deleted it and then recovered it using FTK Imager, foremost and scalpel. For more information on these file carvers you can type "man scalpel" and "man foremost" at the SIFT terminal window.

      Thierry13 also asked about how do we identify file headers and my copout answer would be use WinHex to look at a sample file first and tailor the .conf accordingly. If anyone has any alternative ideas, please leave a comment!
      Also FYI, Gary Kessler keeps a reference table of file header signatures here.

      Wow, this is my 20th post - with all this techno-babble, I have forgotten the humour component. Post number 21 will hopefully be less technical, more humour.

      Some Attempted Forensic Monkey Humour

      $
      0
      0

      I thought I would take a break from all the usual techno-babble and post some questionable (but safe for work) humour/entertainment.

      The first item is a GIF I made up for your exclusive viewing pleasure. I was inspired by various "They See Me Rollin, They Be Hatin" Internet Memes. For those that are interested, I used SIFT's GIMP editor - is there anything that SIFT cannot do?!
      I did think of an alternative punchline but "They See 4N6 Monkeys Rolling ... They Be Deleting / Unallocating / Obfuscating" just didn't have the same ring to it. Plus it might lose the already tenous link to the "They Be Hatin" Meme Theme. Feel free to suggest alternative punchlines in the comments section. BTW the monkey is *supposed* to be giving you "2 thumbs up". He is not giving you the bird nor is he representin' his crew by flashing gangsta signs ;)

      They Be Hatin Us Forensic Monkeys?


      And speaking of how monkeys roll, Amazon has this awesome rolling backpack - I'm not too sure how it would play in client meetings though ...

      Surely They Can't Be Hatin This?!


      Here's another Internet meme relating to those mysterious little balls of fluff that work their way inside your PC. I *think* I got it from the Lifehacker website a few months ago.

      Meet Mr Dust Bunny


      And now for the finale - the BEST EVER LOLCAT - as found on Dave Hull'sBlog:

      Whats The Matter? Cat Got Your Tongue?

      I was going to include a couple of Dilbert cartoons in this post and I did email them asking for permission but they haven't responded (in time).

      Thus ends this brief humourous/humour-less/semi-amusing post. If you enjoyed it, be sure to tip your waiter (leave a comment)  ...


      M57 Jean Investigation Oversight/Apology

      $
      0
      0


      It has come to my attention that the M57 Jean practice case is still being used as a teaching aid so consequently I will be removing my plan of attack post (#2) and heavily redacting my conclusions post (#3).
      I will try to leave the first post up so interested users can still mount the Encase files on SANS SIFT but I can't leave my strategy/answers.

      I apologise for this oversight and any inconvenience this may have caused to either yourself or Digital Corpora students/users.

      Banana-fully Yours,

      Cheeky4n6Monkey

      The (Wannabe) Dark Lord of the SIFT

      $
      0
      0

      Obi-Wan has taught you well?


      Recently, I deleted some posts relating to the M57.biz Jean scenario. However, I also think that there was some helpful (non M57 specific) information on using various SIFT tools for Windows investigations. Consequently, I have re-mixed some of the deleted posts into this non-case specific post.

      Before channelling the Dark Lord of the SIFT, I recommend reading "Digital Forensics With Open Source Tools" (Altheide & Carvey) and "Windows Forensic Analysis DVD Toolkit 2 Ed" (Carvey) or at least have them handy. Of particular interest are the following pages from DFWOST:
      pp 19-23 Working with Images on Linux
      pp 79-89 Windows Registry, Event Logs, Prefetch Files, Shortcut files
      pp 143-153 IE and Firefox Artifacts
      pp 161-164 Outlook PST
      WFADT also covers some of the above topics in greater detail - for example, Ch 4 Windows Registry (p 158) includes a section on finding USB artifacts (p 207).

      A Bit of Unix Background

      Most SIFT tools are located in the "/usr/local/bin" directory and can be run from a terminal window using the "sansforensics" login. If you know the command name (eg XYZ), you can type "which XYZ" and it will tell you where the exe is located. Because "/usr/local/bin" has been added to the PATH environment variable, you don't have to type something like "/usr/local/bin/XYZ" every time you want to launch the exe - you can just type "XYZ" and the shell will search/find it OK.
      You can also use the TAB key to auto-complete directory names / command names - perfect for the lazy monkey typist in all of us. eg typing "ls /ca" then TAB will autocomplete the command to "ls /cases/" (assuming there isn't another folder/directory starting with "ca").
      Also, all Ubuntu shell/terminal commands are saved to a history file and you can cycle through previous commands by using the UP / DOWN arrows on the keyboard.

      Redirection (">")  is another handy unix tool to know - eg "ls -al > sometextfile.txt" will store the output of the ls (list files) command to a file called "sometextfile.txt" in the current directory. You can then read the file using gedit (a GUI text editor) or the "more" command (eg "more sometextfile.txt").
      You can also use ">>" to append ie add to the end of a file. eg "ls -al >> sometextfile.txt" will add the file listing to whatever is already in "sometextfile.txt".

      Piping ("|") is also useful for chaining commands together - eg "ls -al | more" will print out a detailed file listing via the more command (which pauses a scrolling screen until a key is pressed). The pipe symbol is obtained by pressing SHIFT and the \ key (at least on my keyboard).

      The "find"command, can be used to find files by name eg "find . -iname *.txt -print" will print out a list of all .txt files (case-insensitive) under the current directory (.) - this will include any .txt files under a sub-directory.

      The "grep"command can be used to search through a file for keywords. eg "grep -in monkeys *.txt" will (case-insensitive) search all .txt files for the term "monkeys" and print the results to screen. A more comprehensive search can be obtained via "find . -type f -exec grep -in XYZ '{}' \;" this will search all files under the current directory for the XYZ search term.

      Calculating MD5 hashes can be done via "md5sum ABC" where ABC represents the file you wish to calculate the hash for.

      Help for the commands is usually available by typing "man XYZ" for unix system commands. Some of the SIFT tools are scripts and/or do not use the man help method. If they do have help, its usually in the form "XYZ -h" or "XYZ -help" (where XYZ is the script name/executable name). If in doubt, typing the exe/script name without arguments will usually bring up a usage hint.

      Selected List of SIFT Tools

      FYI On the SIFT desktop, there's a "Tool Descriptions for SIFT Workstation 2.12" PDF  and there's also the "SIFT Cheatsheet" PDF.

      In no particular order, here are some of the SIFT tools I have used and what they're used for:

      Ubuntu File Browser to er ... browse files (eg browse each mounted user's Desktop / Recent links). You can access it by double-clicking on the.SIFTWORKSTATION Desktop icon (or any of the other folder shortcuts on the desktop).

      "galleta" to analyse Internet Cookies eg "galleta cookie.txt > cookie-results.txt".

      "pasco" to analyse the Internet Explorer cache eg "pasco index.dat > index-dat-results.txt" or
      "find /mnt/caseX/Documents\ and\ Settings -iname index.dat -exec pasco '{}' \; > all-index-dat-results.txt"
      which will find all "index.dat" files and then run pasco against them and store the results in "all-index-dat-results.txt". Note the escape characters "\" before spaces in the path (eg "/Documents\ and\ Settings") which tells the terminal that a special character is coming up. You could also use TAB to auto-complete the various directory names instead of typing the whole thing.

      "rifiuti" can be used to see what was emptied from the Windows Recycle Bin via the RECYCLER INFO2 file. eg "rifiuti /mnt/caseX/RECYCLER/S-1-5-21-484763869-796845957-839522115-1004/INFO2"

      "exiftool" to extract metadata about a file eg "exiftool abc.doc > abc-doc-metadata.txt"

      "lp" to analyse file link metadata eg "lp shortcut.lnk > shortcut-metadata.txt"

      "pf" to analyse the Windows Prefetch files eg "ls Prefetch/*.pf | pf -m > prefetch-results.txt" will list all .pf files and then call "pf"for each and store the results.

      "readpst" to extract the contents of an MS outlook.pst file. eg "readpst -M- D outlook.pst" will extract all emails (including deleted ones) into the current directory under various sub-folders such as Inbox, Sent Items, Outbox. You can then open the emails up in a text editor and/or use grep to search for interesting terms.
      Update: A similar .pst extraction tool called "pffexport" is also installed on SIFT and it has the added advantage of extracting/decoding any email attachments.You can launch it using something like:
      "pffexport outlook.pst".

      The Regripper "rip.pl" script can be used to analyse the contents of the Windows Registry (eg Mounted devices, recentdocs, typed paths, timezone, environment paths, typed URLS, USB storage, computer name, SAM parsing for User/Group info etc.) eg "rip.pl -r /mnt/caseX/Documents\ and\ Settings/Administrator/NTUSER.DAT -p userassist" will display the Administrators User Assist Key contents using the userassist plugin. A more comprehensive plugin list is available from the "Tool Descriptions for SIFT Workstation 2.12" PDF mentioned earlier.

      "foremost" to carve out any deleted files based on file headers in unallocated space / file slack. See "SANS SIFT Cheat Sheet" PDF under the "Recovering data" section (p 20).
      Basically you use "blkls" (from TSK) twice - once to list deleted (unallocated) disk blocks and again for files in slack space. We capture both outputs to separate files and then run the "foremost" executable with those captured outputs. eg "blkls -o 63 /mnt/ewf/caseX-image >/home/sansforensics/unalloc-caseX.blkls" and "blkls -s -o 63 /mnt/ewf/caseX-image >/home/sansforensics/slack-caseX.blkls". Followed by "foremost -o unalloc-output-directory /home/sansforensics/unalloc-caseX.blkls" and "foremost -o slack-output-directory /home/sansforensics/slack-caseX.blkls". The respective "unalloc-output directory" / "slack-output-directory" folders will now contain a file listing of any recovered files (called "audit.txt") in addition to recovered versions of those files (without their original filenames though). BTW the 63 indicates the sector offset to the file system obtained via (TSK) "sudo mmls /mnt/ewf/caseX-image"

      "usp" to find USB storage device artifacts on an NTFS volume. eg "sudo usp -disk /mnt/ewf/caseX-image 32256". Note the 32256 represents the byte offset of the file system from the beginning of the image. The files system in this example starts at sector 63 so ...  512 byte sector X 63 sectors = 32256 bytes.

      "evtx_view" to view Windows Event (.Evt) Log files from "/mnt/caseX/WINDOWS/system32/config". This GUI can be accessed from the Applications, Forensics top menu on the SIFT VM.

      "SQLite Manager" Firefox plugin to view SQLite databases such as places, cookies, downloads, form history.


      Creating A Super Timeline

      Basically a huge CSV file containing a list of files ordered by time. Where applicable, it also lists entries for created/modified/accessed times for each of those files. It makes it easier to pinpoint what files were changed in a given time period. There's a nice demo presentation by Rob Lee for SANS 508 from 2009 on the SANS 508 course web page (you need to login to view it). The steps are also documented in the "SIFT Cheat Sheet" PDF (p. 19 "Creating Super Timelines" section).
      Warning: It took my VM over 1 hour 20 minutes (mostly to do the initial "timescanner" command) for a 3 GB image. Loading the resultant 24 MB CSV (using SIFT'sOpenOffice) took a while too.

      Decoding Email Attachments

      Say you run "readpst" and then come across an email with an attachment encoded in base64. To obtain a copy of the file attachment, open the original email containing the attachment using a GUI Text Editor (eg gedit). When there's an attachment in an email, it will be delineated by a boundary marker (something like "----boundary-LibPST-iamunique-1649760492_-_-"). Then there will be a bunch of other fields with the last one being the "filename=abc.doc" field (for example). We then copy-and-paste all those characters after the filename into a seperate new file and then save it temporarily (eg "tempabc").
      Now we have to decode the temporary ("tempabc") attachment from base64, strip out any garbage characters and store the result in a file with the email's attachment original filename (eg "abc.doc"). One handy command for this is: "base64 -di tempabc > abc.doc"
      A subsequent "md5sum abc.doc" can then be used to compare it with another source so we can show that the email attachment matches/does not match another file.

      There are heaps more SIFT tools but due to time/sanity constraints I think I'll stop here ... If you have a favourite SIFT tool, please be sure to share it in the comments section.
      I have heard some good things about the meld tool for comparing differences between files - has anyone else used it?

      Detecting Spoofed Emails with SIFT's pffexport and some Perl scripting

      $
      0
      0

      One likely issue facing today's forensicator is the sheer number of emails people keep in their Inboxes.
      These numbers can grow at a phenomenal rate especially if the user subscribes to multiple mailing lists.
      Thinking from an Incident Response perspective - given a bunch of emails in the Inbox, how can we perform a quick check for any spoofed emails (ie forged "From" fields)?

      Rob Lee (unsure if was SANSRob Lee :o) recently suggested using pffexport for one of my previous posts dealing with email analysis. Like readpst, pffexport is installed on SANS SIFT and can be used to extract emails from MS Outlook ".pst" files. Unlike readpst however, pffexport will also automatically extract any attachments - no more pesky base64 decoding!

      You can launch it using something like:
      "pffexport /mnt/caseX/Documents\ and\ Settings/UserX/Local\ Settings/Application\ Data/Microsoft/Outlook/outlook.pst -t userx"

      The results will be stored in the current directory under the "userx.export" sub-directory. The "-t userx" argument tells pffexport what first name to use for the export folder.
      If you don't specify the "-t userx" argument, pffexport will use the default "outlook.pst.export" folder name.

      Under "userx.export" we should now have a bunch of sub directories - for the retrieved emails go to the "userx.export/Top of Personal Folders" sub folder. There you will find separate folders for "Inbox", "Sent Items", "Deleted Items" etc. And under these folders you will find that each email message gets its own numbered folder. pffexport also separates out the mail header information, attachments and body text into their own respective files.
      For our purposes, under each "Inbox" / "Deleted Items" message folder we can find an "InternetHeaders.txt" file which captures that particular email's header info.
      For example, "outlook.pst.export/Top of Personal Folders/Inbox/Message00007/InternetHeaders.txt" might look something like:

      Return-Path: <badguy@badboys.com>
      X-Original-To: victim@spoofme.com
      Delivered-To: x2789967@spunkymail-mx5.g.dreamhost.com
      Received: from rv-out-0304.google.com (rv-out-0304.google.com [209.85.198.210])
          by spunkymail-mx5.g.dreamhost.com (Postfix) with ESMTP id 10B7A41CB9
          for <victim@spoofme.com>; Sun,  6 Jul 2008 00:58:25 -0700 (PDT)
      Received: by rv-out-0304.google.com with SMTP id b20so2037328rvf.23
              for <victim@spoofme.com>; Sun, 06 Jul 2008 00:58:24 -0700 (PDT)
      DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
              d=google.com; s=beta;
              h=domainkey-signature:received:message-id:date:from:to:subject
               :mime-version:content-type:content-transfer-encoding;
              bh=2JhxMj72b75tltAvNhxeDuk6ECfLUR1gUXtJ7Hx6b60=;
              b=TT6rYh6A2JkKPxXT3aJbHpCsGbUiWjLxfROkZe2kKhBzd+4bwR1QfAvakfjIj/YXt9
               oHGqQz1qaVe6n2gCVsVA==
      DomainKey-Signature: a=rsa-sha1; c=nofws;
              d=google.com; s=beta;
              h=message-id:date:from:to:subject:mime-version:content-type
               :content-transfer-encoding;
              b=r3HhXow0DdCIy9QnmfByeJ+D57+/rOQ7zAmtqb4HOclOdURK8AIOFOw1vOiyC1Hnso
               fi5Id7j4XDRIDMBLwHRg==
      Received: by 10.114.108.8 with SMTP id g8mr1855176wac.28.1215331104741;
              Sun, 06 Jul 2008 00:58:24 -0700 (PDT)
      Message-ID: <2638157.101571215331104745.JavaMail.ins-frontend@google.com>
      Date: Sun, 6 Jul 2008 00:58:24 -0700 (PDT)
      From: goodguy@goodguys.com
      To: victim@spoofme.com
      Subject: Google Email Verification
      MIME-Version: 1.0
      Content-Type: text/plain; charset=US-ASCII
      Content-Transfer-Encoding: 7bit


      FYI For this exercise I will be using a heavily modified copy of the pffexport extracted results from an M57 Jean outlook.pst. I have placed this modified copy in "/home/sansforensics/mod-outlook.pst.export".
      I have deleted a shed load of "Inbox" emails (for ease of testing) and then manually edited a couple of "InternetHeaders.txt" files to reflect a spoof attack. ie I have modified the "Return-Path" (badguy@badboys.com) so it does not match the "From" field (goodguy@goodguys.com).

      In real life, the "From" field can be manipulated by the submitting party when interfacing with the recipients email server. It is not usually verified.
      The "Return-Path" field is (usually) set by the recipients email server at the time of reception and consequently it should reflect the actual sending party.
      For more information on the "Return-Path" and "From" fields see this programmer's forum.

      Coding

      So knowing all this, it's now time to write a Perl script to parse all of the "InternetHeaders.txt" files under the "Inbox" and "Deleted Items" message folders and then check that the "From" fields match the "Return-Path" fields.
      If we get a mismatch, we will print out both a warning and the "From" / "Return-Path" address fields. We will also add the capability to exclude user specified email addresses (to reduce the resultant data set).
      After processing all relevant emails, we will also print out the total number of suspect emails.
      I've (inelegantly) named the script "pffexport-spoofchk.pl" and saved it under "/usr/local/bin/". I've also made it world executable by typing "chmod a+x /usr/local/bin/pffexport-spoofchk.pl".



      Here's the code:


      # START CODE

      #!/usr/bin/perl -w

      # Perl script to take the output of pffexport and check the "From" email address fields against the "Return-Path" in InternetHeaders.txt
      # from each Inbox/Deleted Items email

      use strict;

      use Getopt::Long;
      use File::Find;
      use Email::Address;

      my $version = "pffexport-spoofcheck.pl v2012.03.05";
      my $help = ''; # help flag
      my @directories; # input directories from -dir flag (must use absolute paths)
      my @exfilter; # exclude addresses in form user@domain
      my $mismatchcount = 0;

      GetOptions('help|h' => \$help,
          'x=s@' => \@exfilter,
          'dir=s@' => \@directories);

      if ($help||@directories == 0)
      {
          print("\n$version\n");
          print("Perl script to process the output of pffexport and check for spoofed emails.\n");
          print("\nUsage: pffexport-spoofcheck.pl [-h|help] [-dir dirname] [-x user\@domain]\n");
          print("-h|help ........... Help (print this information). Does not run anything else.\n");
          print("-dir dirname ...... Directory containing exported pffexport Inbox/Deleted Items files.\n");
          print("-x user\@domain .... Exclude this email address from processing.\n");
          print("\nExamples:\n");
          print("pffexport-spoofcheck.pl -dir /cases/outlook.pst.export/Top of Personal Folders/Inbox/ -x goodguy\@goodguys.com\n\n");
          print("pffexport-spoofcheck.pl -dir /cases/outlook.pst.export/Top of Personal Folders/Deleted Items/ -x goodguy\@goodguys.com\n\n");
          print("pffexport-spoofcheck.pl -dir /cases/outlook.pst.export/Top of Personal Folders/ -x goodguy\@goodguys.com\n\n");
          exit;
      }

      # Setup Email::Address exclusion filter array
      my @exEmailAddresses;
      my $idx = 0;
      if (@exfilter > 0)
      {
          foreach my $email (@exfilter)
          {
              $exEmailAddresses[$idx] = new Email::Address($email);
              $idx++;
          }
      }

      # Main processing loop
      print("\nRunning $version\n");

      # Recursively process folders specified using the -dir flag
      # Note: Will NOT follow symbolic links to files
      find(\&ProcessDir, @directories);

      print ("\nFound $mismatchcount mismatched emails\n");

      # Gets called for each file/folder under user specified dir
      sub ProcessDir
      {
          # $File::Find::dir is the current directory name,
          # $_ is the current filename within that directory
          # $File::Find::name is the complete pathname to the file.
          my $filename = $File::Find::name; # should contain absolute path eg /cases/outlook.pst.export/Top of Personal Folders/Inbox/
          my @retaddresses;
          my @fromaddresses;
          my $fromuser;
          my $fromhost;
          my $retuser;
          my $rethost;

          # InternetHeaders.txt files from "Inbox" or "Deleted Items"
          if ( ($filename =~ /InternetHeaders.txt/) && (($filename =~ /Inbox/)||($filename =~ /Deleted Items/)) )
          {
              # Open the file for reading
              if (open(IHFILE, "<", $filename))
              {
                  # Read each line of the file
                  while (<IHFILE>) # Assigns each line in turn to $_
                  {
                      if ($_ =~ /Return-Path/)
                      {           
                          # If it contains a "Return-Path" string try to extract email address
                          @retaddresses = Email::Address->parse($_); 
                      }
                      elsif ($_ =~ /From/)
                      {           
                          # If it contains a From string try to extract email address
                          @fromaddresses = Email::Address->parse($_);
                      }
                  }
                  if (@retaddresses > 0)
                  {
                      $retuser = $retaddresses[0]->user;
                      $rethost = $retaddresses[0]->host;
                  }
                  else
                  {
                      close IHFILE;
                      return; # Bail out - there should be at least 1 retaddress field
                  }
                  if (@fromaddresses > 0)
                  {
                      $fromuser = $fromaddresses[0]->user;
                      $fromhost = $fromaddresses[0]->host;
                  }
                  else
                  {
                      close IHFILE;
                      return; # Bail out - there should be at least 1 fromaddress field
                  }

                  # Check if retaddresses or fromaddresses are supposed to be filtered out/ignored
                  if (@exEmailAddresses > 0)
                  {
                      foreach my $email (@exEmailAddresses)
                      {
                          if ( (($email =~ $retuser)&&($email =~ $rethost))
                              || (($email =~ $fromuser)&&($email =~ $fromhost)) )
                          {
                              close IHFILE;
                              return; # don't print anything or spoofcheck, this email is being filtered out
                          }
                      }
                  }   

                  # Normal case (no filtering) - Compare the Return-Path to the From path and print out if there's a discrepancy
                  if (($retuser ne $fromuser)||($rethost ne $fromhost))
                  {
                      print ("\n*** Mismatched From and Return-Path addresses in $filename ***\n");
                      print ("From user = $fromuser, From host = $fromhost\n");
                      print ("Return-Path user = $retuser, Return-path host = $rethost\n");
                      $mismatchcount++;
                  }
                  close IHFILE;
              }
              else
              {
                  print("Unable to open $filename for analysis\n");
              }
          }
      }

      # END CODE


      Code Summary

      The first few sections are similar to those in "exif2map.pl" - there's a GetOptions to handle user specified arguments and a Help screen printout check. One thing to note is the inclusion of the "useEmail::Address" Perl module. This pre-existing  module makes it easier to look for/handle email addresses. We can install it by typing:
       "sudo cpan Email::Address".
      After the help section, we set up an array list (exEmailAddresses) of any user specified email addresses which are to be excluded for processing.
      Next we call the File::find function to call the ProcessDir subroutine for each file/directory it finds under the user specified directory.
      After all directories have been processed, we then print the "mismatchcount" and exit.
      The ProcessDir subroutine first checks if the filename path (eg "/cases/outlook.pst.export/Top of Personal Folders/Inbox/Message00001/InternetHeaders.txt") contains both "InternetHeaders.txt" AND either "Inbox" or "Deleted Items". If it does, it opens the "InternetHeaders.txt" file, reads each line and tries to extract "Return-Path" and "From" email addresses (using the Email::Address::parse function). If its a valid "InternetHeaders.txt" file, there should be at least 1 of each email address. If none are found, we bail out of the ProcessDir function (close the "InternetHeaders.txt" file and call return) and the next file/directory will have ProcessDir called against it.
      Next, we check to see if the user specified any exclusion filters. If so, we compare the extracted "From" and "Return-Path" email addresses and bail out of ProcessDir if there's a match.
      The last major part occurs if we have a "From" and "Return-Path" and they are not being filtered out.
      We compare the "From" user and the "From" domain against the "Return-Path" user and domain. If there's a mismatch, we print out our message plus the various email fields and increment our "mismatchcount" counter.

      Testing

      For this testing scenario, I have conjured up 2 spoofed emails in the "Inbox" and left a bunch of googlealerts (and other mailing list emails) in both the "Inbox" and "Deleted Items".
      We will now verify that we can detect these 2 spoofed emails and also reduce the data set with our filter parameter (-x).
      Additionally, we will point the script to the "Inbox", "Deleted Items" and finally the "Top of Folders" directories and see that it processes all relevant emails.

      Processing the "Inbox" without filters

      Typing "pffexport-spoofcheck.pl  -dir /home/sansforensics/mod-outlook.pst.export/Top\ of\ Personal\ Folders/Inbox/" results in the following output:

      Running pffexport-spoofcheck.pl v2012.03.05

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00045/InternetHeaders.txt ***
      From user = googlealerts-noreply, From host = google.com
      Return-Path user = 3M25zSBQKBF0BJJBG95G9MON-IJM9KGTBJJBG9.7JHE95IHac.6DU, Return-path host = alerts.bounces.google.com
      .
      .
      .
      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00044/InternetHeaders.txt ***
      From user = googlealerts-noreply, From host = google.com
      Return-Path user = 3BGRzSBQKBCQGOOGLEALERTS-NOREPLYGOOGLE.COMJEANMfh.BIZ, Return-path host = alerts.bounces.google.com

      Found 37 mismatched emails
      sansforensics@SIFT-Workstation:~$


      Note: I have edited out a bunch of output entries to save space. As you can see there's a lot of mismatched emails besides the 2 we created. This is because mailing lists typically have different "Return-Path" and "From" fields. The "Return-Path" fields are usually set to bounce any replies. To reduce these output results we will set a filter in the next test.

      Processing the "Inbox" with filter on "googlealerts-noreply@google.com"

      Typing "pffexport-spoofcheck.pl  -dir /home/sansforensics/mod-outlook.pst.export/Top\ of\ Personal\ Folders/Inbox/ -x googlealerts-noreply@google.com" results in the following output:

      Running pffexport-spoofcheck.pl v2012.03.05

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00007/InternetHeaders.txt ***
      From user = goodguy, From host = goodguys.com
      Return-Path user = badguy, Return-path host = badboys.com

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00026/InternetHeaders.txt ***
      From user = admin, From host = associatedcontent.com
      Return-Path user = webadmin, Return-path host = associatedcontent.com

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00037/InternetHeaders.txt ***
      From user = allsongs, From host = n.npr.org
      Return-Path user = newsletters, Return-path host = n.npr.org

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00008/InternetHeaders.txt ***
      From user = accounts, From host = goodguys.com
      Return-Path user = badguy, Return-path host = badguys.com

      Found 4 mismatched emails
      sansforensics@SIFT-Workstation:~$


      Much better! We can actually see our 2 x "badguy" spoof attempts but we can also filter out "admin@associatedcontent.com" and "allsongs@n.npr.org".

      Processing the "Inbox" with multiple filters

      Typing "pffexport-spoofcheck.pl  -dir /home/sansforensics/mod-outlook.pst.export/Top\ of\ Personal\ Folders/Inbox/ -x googlealerts-noreply@google.com -x admin@associatedcontent.com -x allsongs@n.npr.org" results in the following output:

      Running pffexport-spoofcheck.pl v2012.03.05

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00007/InternetHeaders.txt ***
      From user = goodguy, From host = goodguys.com
      Return-Path user = badguy, Return-path host = badboys.com

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00008/InternetHeaders.txt ***
      From user = accounts, From host = goodguys.com
      Return-Path user = badguy, Return-path host = badguys.com

      Found 2 mismatched emails
      sansforensics@SIFT-Workstation:~$


      Aha! We have narrowed down our 50 odd emails to our 2 suspected spoof emails!
      Now lets try processing the "Deleted Items" (there shouldn't be any spoofs)...



      Processing the "Deleted Items" without filters

      Typing "pffexport-spoofcheck.pl  -dir /home/sansforensics/mod-outlook.pst.export/Top\ of\ Personal\ Folders/Deleted\ Items/" results in the following output:

      Running pffexport-spoofcheck.pl v2012.03.05

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Deleted Items/Message00001/InternetHeaders.txt ***
      From user = googlealerts-noreply, From host = google.com
      Return-Path user = 3HER-SBQKBCcJRRJOHDOHUWV-QRUHSObJRRJOH.FRPMHDQPik.ELc, Return-path host = alerts.bounces.google.com

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Deleted Items/Message00002/InternetHeaders.txt ***
      From user = googlealerts-noreply, From host = google.com
      Return-Path user = 3zwuASBQKBKMJRRJOHDOHUWV-QRUHSObJRRJOH.FRPMHDQPik.ELc, Return-path host = alerts.bounces.google.com

      Found 2 mismatched emails


      So we can see that there are just 2 mismatched google alert emails in the "Deleted Items".
      Now we will try specifying the "Top of Folders" directory (unfiltered) and see if it picks up the 37 mismatched "Inbox" emails PLUS the 2 "Deleted Item" mismatches = 39 total.

      Processing the "Top of Personal Folders" without filters


      Typing "pffexport-spoofcheck.pl  -dir /home/sansforensics/mod-outlook.pst.export/Top\ of\ Personal\ Folders/" results in the following output:

      Running pffexport-spoofcheck.pl v2012.03.05

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00045/InternetHeaders.txt ***
      From user = googlealerts-noreply, From host = google.com
      Return-Path user = 3M25zSBQKBF0BJJBG95G9MON-IJM9KGTBJJBG9.7JHE95IHac.6DU, Return-path host = alerts.bounces.google.com
      .
      .
      .
      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Deleted Items/Message00002/InternetHeaders.txt ***
      From user = googlealerts-noreply, From host = google.com
      Return-Path user = 3zwuASBQKBKMJRRJOHDOHUWV-QRUHSObJRRJOH.FRPMHDQPik.ELc, Return-path host = alerts.bounces.google.com

      Found 39 mismatched emails

      OK we found all 39 mismatches from specifying the "Top of Personal Folders". Now we will filter out the googlealerts by typing:

      "pffexport-spoofcheck.pl  -dir /home/sansforensics/mod-outlook.pst.export/Top\ of\ Personal\ Folders/ -x googlealerts-noreply@google.com" which results in the following output:

      Running pffexport-spoofcheck.pl v2012.03.05

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00007/InternetHeaders.txt ***
      From user = goodguy, From host = goodguys.com
      Return-Path user = badguy, Return-path host = badboys.com

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00026/InternetHeaders.txt ***
      From user = admin, From host = associatedcontent.com
      Return-Path user = webadmin, Return-path host = associatedcontent.com

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00037/InternetHeaders.txt ***
      From user = allsongs, From host = n.npr.org
      Return-Path user = newsletters, Return-path host = n.npr.org

      *** Mismatched From and Return-Path addresses in /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00008/InternetHeaders.txt ***
      From user = accounts, From host = goodguys.com
      Return-Path user = badguy, Return-path host = badguys.com

      Found 4 mismatched emails
      sansforensics@SIFT-Workstation:~$


      Which shows that we can point "pffexport-spoofchk.pl" at the "Top of Personal Folders" and it will process (and filter) both "Inbox" and "Deleted Items" emails. WHEW!

      Miscellaneous Note
      By typing something like:
       
      grep -in "goodguy\@goodguys\.com" /home/sansforensics/mod-outlook.pst.export/Top\ of\ Personal\ Folders/Inbox/Message*/InternetHeaders.txt
      we can search a bunch of emails for a particular email address. The results will look something like:
      /home/sansforensics/mod-outlook.pst.export/Top of Personal Folders/Inbox/Message00007/InternetHeaders.txt:26:From: goodguy@goodguys.com

      So that's all for today folks.
      I'm not sure how useful the script will be in real life but as a Perl programming exercise I think it was interesting enough (for me anyway). Once again, any comments/suggestions will be appreciated.

      Inspecting Registry key differences on SIFT with "regdump.pl" and Meld

      $
      0
      0
      Recently, I read some favourable reviews (on the Ubuntu forum) about a open source diff program called meld. Commonly used in programming, diff programs are used to compare 2 separate files.
      There is an existing Unix command line diff program called "diff" however, I think a GUI diff editor makes it much easier/quicker to visualise lots of little differences.

      Note: This post isn't going to be anything special but it might introduce you to handy tool which you can add to your SIFT kit.

      So, lets take meld for a spin on SIFT V2.12 shall we?

      To install meld, at a command line terminal type:
      "sudo apt-get install meld"

      Already installed on the SIFT VM is the "regdump.pl" Perl script.
      All you have to do is give it the Registry hive (eg "NTUSER.DAT") and the key (eg "Software\\Microsoft\\winmine" which is the Minesweeper Registry entries) plus some arguments (-r for recursively listing and v to print the values). Note: When listing the key, ensure you use "\\" to separate subkey names.

      I have used FTK Imager to grab 2 copies of my "NTUSER.DAT" files. One with my impressive Minesweeper records ("NTUSER.DAT")and the second one taken after I reset those records (NTUSER-2.DAT). Oh, the sacrifices this Minesweeping Monkey makes!

      We start our exploration by typing:

      "regdump.pl /cases/NTUSER.DAT -rv Software\\Microsoft\\winmine > ntuser1-winmine.txt"

      and

      "regdump.pl /cases/NTUSER-2.DAT -rv Software\\Microsoft\\winmine > ntuser2-winmine.txt"

      Note: These commands store the outputs of "regdump.pl" in the "ntuser1-winmine.txt" and "ntuser2-winmine.txt" files in the current directory ("/home/sansforensics/").

      So now that we have our Registry listings, let's compare them.

      First, let's use the existing Unix command line "diff" with the "-y" argument to list the 2 files side by side:

      "diff -y ntuser1-winmine.txt ntuser2-winmine.txt"

      which outputs something like:

      $$$PROTO.HIV\Software\Microsoft\winmine [2011-08-02T09:52:57Z |    $$$PROTO.HIV\Software\Microsoft\winmine [2012-03-09T06:32:01Z
      Difficulty (REG_DWORD) = 0x00000002 (2)                Difficulty (REG_DWORD) = 0x00000002 (2)
      Height (REG_DWORD) = 0x00000010 (16)                Height (REG_DWORD) = 0x00000010 (16)
      Width (REG_DWORD) = 0x0000001e (30)                Width (REG_DWORD) = 0x0000001e (30)
      Mines (REG_DWORD) = 0x00000063 (99)                Mines (REG_DWORD) = 0x00000063 (99)
      Mark (REG_DWORD) = 0x00000001 (1)                Mark (REG_DWORD) = 0x00000001 (1)
      AlreadyPlayed (REG_DWORD) = 0x00000001 (1)            AlreadyPlayed (REG_DWORD) = 0x00000001 (1)
      Color (REG_DWORD) = 0x00000001 (1)                Color (REG_DWORD) = 0x00000001 (1)
      Sound (REG_DWORD) = 0x00000000 (0)                Sound (REG_DWORD) = 0x00000000 (0)
      Xpos (REG_DWORD) = 0x000002a8 (680)                Xpos (REG_DWORD) = 0x000002a8 (680)
      Ypos (REG_DWORD) = 0x0000016c (364)                Ypos (REG_DWORD) = 0x0000016c (364)
      Time1 (REG_DWORD) = 0x00000010 (16)                  |    Time1 (REG_DWORD) = 0x000003e7 (999)
      Time2 (REG_DWORD) = 0x00000065 (101)                  |    Time2 (REG_DWORD) = 0x000003e7 (999)
      Time3 (REG_DWORD) = 0x000003e7 (999)                Time3 (REG_DWORD) = 0x000003e7 (999)
      Name1 (REG_SZ) = A                          |    Name1 (REG_SZ) = Anonymous
      Name2 (REG_SZ) = A                          |    Name2 (REG_SZ) = Anonymous
      Name3 (REG_SZ) = Anonymous                    Name3 (REG_SZ) = Anonymous

      sansforensics@SIFT-Workstation:~$


      You can also call "diff" without the side-by-side formatting and so it only shows the differences (leaving out the common lines):

      "diff ntuser1-winmine.txt ntuser2-winmine.txt"

      which outputs something like:

      1c1
      < $$$PROTO.HIV\Software\Microsoft\winmine [2011-08-02T09:52:57Z]
      ---
      > $$$PROTO.HIV\Software\Microsoft\winmine [2012-03-09T06:32:01Z]
      12,13c12,13
      < Time1 (REG_DWORD) = 0x00000010 (16)
      < Time2 (REG_DWORD) = 0x00000065 (101)
      ---
      > Time1 (REG_DWORD) = 0x000003e7 (999)
      > Time2 (REG_DWORD) = 0x000003e7 (999)
      15,16c15,16
      < Name1 (REG_SZ) = A
      < Name2 (REG_SZ) = A
      ---
      > Name1 (REG_SZ) = Anonymous
      > Name2 (REG_SZ) = Anonymous
      sansforensics@SIFT-Workstation:~$


      Now lets compare "diff" with the meld GUI.

      To launch meld:
      Go to the Ubuntu menu at the top of the screen and select:
      Applications ... Programming .... Meld Diff Viewer

      Or at the command line terminal type:
      "meld &"

      Go to File ... New and then in the resultant pop up (under the "File Comparison" tab), we "Browse" the "Original" dropbox to "/home/sansforensics/ntuser1-winmine.txt". Similarly, we set the "Mine" dropbox to "/home/sansforensics/ntuser2-winmine.txt" and then press "OK".

      Here's what you should now see:


      Whats the Diff?

      As you can see, for our purposes a GUI diff makes it a LOT easier to see what has changed.
      We can easily see that not only have my glorious record times/names been changed, but the so has the last access time for the key.

      I don't recommend using meld over more than one key eg don't "regdump.pl" the top level "Software" key and then try to compare everything - it's too confusing.
      For example, if the 2 hives have different programs installed, the "regdump.pl" output line numbers won't match up and meld will mark whole blocks as different.

      One interesting feature of meld is the ability to do a three-way (Oh Grow Up!) compare . Interesting, but I'm not sure how useful it would be in a real investigation.

      Thus ends our brief test drive. If you know of any other good (Unix/Windows) "diff" tools/hints, please leave a comment!

      UPDATE:
      I have since discovered SIFT's "regcompare.pl" GUI (located in "/usr/local/bin"). This program uses the Parse::Win32Registry Perl module (same as RegRipper) to graphically compare 2 Registry hives. You can see below that any changed values are also displayed. Users can also search for changes and bookmark Registry entries. It can be launched from the command line (in the background) by typing:
      "regcompare.pl &"

      Using "regcompare.pl" to compare MineSweeper Registry Entries


      Other SIFT GUI Registry Viewers (located in "/usr/local/bin") ...
      The "regview.pl" GUI is also based on the Parse::Win32Registry Perl module. It lets users (read-only) browse a Registry hive. Users can also search for and bookmark Registry entries. It can be launched from the command line (in the background) by typing:
      "regview.pl &"

      There is another Registry editor called "yaru" (which funnily stands for "yet another registry viewer"). This viewer can:
      - display allocated Registry data
      - display unallocated Registry data,
      - search for data,
      - save a copy of the hive to another file and
      - display a Report on certain Registry entries.

      Users can launch "yaru" via the Applications ... Forensics ... "YARU Registry Editor" SIFT menu or via the command line by typing:
      "yaru &"


      Quick Tutorial On Re-using My Perl Scripts

      $
      0
      0
      Hi All,

      What a busy week for this little monkey!
      A fellow monkey recently contacted me about some problems they had getting my "exif2map.pl" script to work on SIFT. Specifically, they were getting "command not found" errors whenever they launched their version.

      I thought I'd write a quick guide just in case anyone else was having issues. This procedure applies to all of my Perl programs I have posted on this blog.

      I'll use my latest "exif2map.pl" as an example:

      # START CODE


      #!/usr/bin/perl -w

      # Perl script to take the output of exiftool and conjure up a web link
      # to google maps if the image has stored GPS lat/long info.

      use strict;

      use Image::ExifTool;
      use Image::ExifTool::Location;
      use Getopt::Long;
      use HTML::QuickTable;
      use File::Find;

      # commented out for now - apparently File:Find can issue some weird warnings
      #no warnings 'File::Find';

      my $version = "exif2map.pl v2012.02.21";
      my $help = ''; # help flag
      my $htmloutput = ''; #html flag
      my @filenames; # input files from -f flag
      my @directories; # input directories from -dir flag (must use absolute paths)

      my %file_listing; # stored results

      GetOptions('help|h' => \$help,
          'html' => \$htmloutput,
          'f=s@' => \@filenames,
          'dir=s@' => \@directories);

      if ($help||(@filenames == 0 && @directories == 0))
      {
          print("\n$version\n");
          print("Perl script to take the output of exiftool and conjure up a web link\n");
          print("to google maps if the image has stored GPS lat/long info.\n");

          print("\nUsage: exif2map.pl [-h|help] [-f filename] [-html]\n");
          print("-h|help .......... Help (print this information). Does not run anything else.\n");
          print("-f filename ...... File(s) to extract lat/long from\n");
          print("-dir directory ... Absolute path to folder containing file(s) to extract lat/long from\n");
          print("-html ............ Also output results as a timestamped html file in current directory\n");

          print("\nExample: exif2map.pl -f /cases/galloping-gonzo.jpg");
          print("\nExample: exif2map.pl -f /cases/krazy-kermit.jpg -dir /cases/rockin-rowlf-pics/ -html\n\n");
          print("Note: Outputs results to command line and (if specified) to a timestamped html file\n");
          print("in the current directory (e.g. exif2map-output-TIMESTAMP.html)\n\n");
         
          exit;
      }

      # Main processing loop
      print("\n$version\n");

      # Process filenames specified using the -f flag first
      if (@filenames)
      {
          foreach my $name (@filenames)
          {
              ProcessFilename($name);
          }
      }

      # Process folders specified using the -dir flag
      # Note: Will NOT follow symbolic links to files
      if (@directories)
      {
          find(\&ProcessDir, @directories);
      }

      # If html output required AND we have actually retrieved some data ...
      if ( ($htmloutput) && (keys(%file_listing) > 0) )
      {   
          #timestamped output filename
          my $htmloutputfile = "exif2map-output-".time.".html";

          open(my $html_output_file, ">".$htmloutputfile) || die("Unable to open $htmloutputfile for writing\n");

          my $htmltable = HTML::QuickTable->new(border => 1, labels => 1);

          # Added preceeding "/" to "Filename" so that the HTML::QuickTable sorting doesn't result in
          # the column headings being re-ordered after / below a filename beginning with a "\".
          $file_listing{"/Filename"} = "GoogleMaps Link";

          print $html_output_file "<HTML>";
          print $html_output_file $htmltable->render(\%file_listing);
          print $html_output_file "<\/HTML>";

          close($htmloutputfile);
          print("\nPlease refer to \"$htmloutputfile\" for a clickable link output table\n\n");
      }

      sub ProcessFilename
      {
          my $filename = shift;

          if (-e $filename) #file must exist
          {
              my $exif = Image::ExifTool->new();
              # Extract all info from existing image
              if ($exif->ExtractInfo($filename))
              {
                  # Ensure all 4 GPS params are present
                  # ie GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef
                  # The Ref values indicate North/South and East/West
                  if ($exif->HasLocation())
                  {
                      my ($lat, $lon) = $exif->GetLocation();
                      print("\n$filename contains Lat: $lat, Long: $lon\n");
                      print("URL: http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\n");
                      if ($htmloutput) # save GoogleMaps URL to global hashmap indexed by filename
                      {
                          $file_listing{$filename} = "<A HREF = \"http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\"> http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en</A>";
                      }
                      return 1;
                  }
                  else
                  {
                      print("\n$filename : No Location Info available!\n");
                      return 0;
                  }
              }
              else
              {
                  print("\n$filename : Cannot Extract Info!\n");
                  return 0;
              }
          }
          else
          {
              print("\n$filename does not exist!\n");
              return 0;
          }
      }

      sub ProcessDir
      {
          # $File::Find::dir is the current directory name,
          # $_ is the current filename within that directory
          # $File::Find::name is the complete pathname to the file.
          my $filename = $File::Find::name; # should contain absolute path eg /cases/pics/krazy-kermit.jpg

          if (-f $filename) # must be a file not a directory name ...
          {
              my $exif = Image::ExifTool->new();
              # Extract all info from existing image
              if ($exif->ExtractInfo($filename))
              {
                  # Ensure all 4 GPS params are present
                  # ie GPSLatitude, GPSLatitudeRef, GPSLongitude, GPSLongitudeRef
                  # The Ref values indicate North/South and East/West
                  if ($exif->HasLocation())
                  {
                      my ($lat, $lon) = $exif->GetLocation();
                      print("\n$filename contains Lat: $lat, Long: $lon\n");
                      print("URL: http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\n");
                      if ($htmloutput) # save GoogleMaps URL to global hashmap indexed by filename
                      {
                          $file_listing{$filename} = "<A HREF = \"http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en\"> http://maps.google.com/maps?q=$lat,+$lon($filename)&iwloc=A&hl=en</A>";
                      }
                      return 1;
                  }
                  else
                  {
                      print("\n$filename : No Location Info available!\n");
                      return 0;
                  }
              }
              else
              {
                  print("\n$filename : Cannot Extract Info!\n");
                  return 0;
              }
          }
      }

      # END CODE


      1. OK, what I should have mentioned was that the very first line in your "exif2map.pl" script is the line containing " #!/usr/bin/perl -w".
      This line tells Unix, "Hey make sure you use the Perl interpreter on this script". It's GOT to be the first thing Unix sees in the script. Otherwise, you will probably get a bunch of "command not found" errors. The "#START CODE" and "#END CODE" comments aren't supposed to be in the script file.

      2. Copy/paste everything from that first line down to just BEFORE the "# END CODE".

      3. According to this article it might be best if you don't try saving it into a Windows text editor if you intend to eventually run it on SIFT. You might be better off, using Firefox / gedit on SIFT. The reason being, the Windows text editor may save it using an incorrect character set / add extra control characters. Probably unlikely to happen but documented here just in case.

      4. After saving the script (preferably in "/usr/local/bin/"), don't forget to make it executable using "chmod a+x /usr/local/bin/exif2map.pl".

      5. For some reason, when I used CPAN to install Image::ExifTool::Location, the module seemed to download/install OK but then I got some testing failures. I am not sure why this is happening - maybe the version of CPAN on SIFT is not compatible with Image::ExifTool::Location's tests? Maybe the module is expecting a particular environment path? I don't know. The weird thing was that none of the other CPAN modules I have subsequently installed had any testing issues *shrug*.
      Anyway, like the true hack I am - I decided to code it anyway and see if it worked afterwards :) Subsequently,  I couldn't see any problems after testing the "exif2map.pl" script against the ExifTool Firefox plugin. Popular Programmers Paradigm - if it works, don't touch it !

      So that's the end of this "quick" little post. Hope this helps!

      Using Perl to View an SQLite Database

      $
      0
      0
       
      Warning: This is one of my longer rambles posts and there's not many pics either.

      According to the SQLite website, SQLite is an open source, cross-platform database library that is used in software applications (eg DropBox and Skype clients), gadgets (eg Nokia phones, MP3 players, PDAs) and website databases. As SQLite becomes more prevalent, it seems prudent for forensicators to be able to retrieve/view the contents of these databases. Thus, Enter the Perl Monkey!
      Special mention/unashamed name drop must go to Detective Cindy Murphy who mentioned the increasing prevalence of SQLite in the mobile phone arena which in turn, inspired this post.

      In brief, SQLite files are divided into equal sized pages. The minimum file size is one page (the minimum page size is 512 bytes), the maximum file size is ~140 TB. While page types can vary, each page size within a single database file is constant. Page sizes can/will differ between different database files. For those interested, the SQLite file format is defined here. 
      Richard Drinkwater'sblog has some excellent information on carving SQLite databases. Andrew Hoog has also released a set of carving slides here.

      Firefox and SQLite

      One of the more popular applications to use SQLite is Firefox. Firefox 3.6.17 is installed on SIFT V2.12 - which will probably be the last of the 3.6 branch. Firefox 11 is the latest "main" branch release. Both versions feature "Private Browsing" - the ability to automatically clear a User's History, Downloads and Cookies.
      In 2011, Alex blogged about Firefox 4 forensics here and Tsurany also wrote about Firefox's Private Browsing Mode here.
      For more information on Firefox and SQLite, I highly recommend "Digital Forensics With Open Source Tools" by Altheide and Carvey (p 147 onwards).

      In 2009, Kristinn Gudjonsson wrote an awesome Firefox History Perl script which he described in further detail here.

      To improve my overall understanding (of both Perl and SQLite), I have written a stripped down version of Kristinn's script. It's kinda re-inventing the wheel but I think I will improve my understanding/retention by doing rather than just reading. And given the stripped down nature of my script, it will also take less time for me to explain.

      But before monkeying around with the code, we need to find out about the relevant Firefox database schema (eg table names, keys, fields). Here is the schema for "places.sqlite" as defined by ForensicsWiki and also via my own observations:

      Firefox's "places.sqlite"schema


      Note: One side effect of having "moz_bookmarks" linked with "moz_places" is that "places.sqlite" will always contain any bookmarked site information regardless of CCleaner use or "Private Browsing".

      Here is the schema for "downloads.sqlite":

      Firefox's "downloads.sqlite" schema

      According to ForensicsWiki, Firefox stores its SQLite files in the following places:
      - Windows XP: C:\Documents and Settings\<username>\Application Data\Mozilla\Firefox\Profiles\<profile folder>\places.sqlite
      - Windows Vista: C:\Users\<user>\AppData\Roaming\Mozilla\Firefox\Profiles\<profile folder>\places.sqlite
      - Linux: /home/<user>/.mozilla/firefox/<profile folder>/places.sqlite
      - MacOS: /Users/<user>/Library/Application Support/Firefox/Profiles/default.lov/places.sqlite

      While each of these ".sqlite" files contains a SQLite database, some of them (eg "places.sqlite") contain multiple tables (eg moz_places, moz_historyvisits, moz_bookmarks). When running, Firefox also creates temporary ".sqlite" files with the extensions ".sqlite-shm" for shared memory (eg "places.sqlite-shm") and also ".sqlite-wal" for write ahead logs (eg "places.sqlite-wal"). As you can observe these at run time with Windows Explorer, theres a good chance they are carvable/recoverable.

      Firefox 11 has the following files: addons.sqlite, chromeappsstore.sqlite, content-prefs.sqlite, cookies.sqlite, downloads.sqlite, extensions.sqlite, formhistory.sqlite, permissions.sqlite, places.sqlite, search.sqlite, signons.sqlite, webappsstore.sqlite.

      Firefox 3.6.17 has the following sqlite files:
      content-prefs.sqlite, cookies.sqlite, downloads.sqlite, formhistory.sqlite, permissions.sqlite, places.sqlite, search.sqlite, signons.sqlite,  webappsstore.sqlite, urlclassifier3.sqlite.

      Note: The bold entries denote the differing files between Firefox versions. Its also easy to miss files when switching between browsing modes / using CCleaner so take this list with a grain of salt.

      For our script, we will be printing the Downloads, Bookmarks, and History information only. Unlike Kristinn's script we won't offer html output nor detect time-skewing.
      As we saw in the schema diagram above, the Bookmarks and History information is stored in the "places.sqlite" file. This file contains the "moz_places", "moz_bookmarks" and "moz_historyvisits" tables.
      The Downloads information is stored in the "moz_downloads" table from the "downloads.sqlite" file.

      Viewing SQLite Tables

      There are several SQLite Database browsers around but not all of them will be able to open the later releases of SQLite (the latest is 3.7.10).
      The easiest one to use is probably the Firefox Add-on GUI called SQLite Manager. I have used it to view the contents of Firefox 11 (uses SQLite V3.7.7) ".sqlite"  files. The SQLite Manager installed on SIFT appears to use SQLite V3.7.4.
      On SIFT, there is also an sqlite3 command line exe installed (in "/usr/local/bin/sqlite3"). Unfortunately, it seems that the current installed version (V3.6.16) cannot read Firefox 11 ".sqlite" files. It complains that the file is encrypted or not a database file. It will read SIFT's own Firefox 3.5.17 ".sqlite" files however.
      Consequently, I have downloaded the latest sqlite3 exe (V3.7.10) from the official SQLite website and then used it to successfully read Firefox 11 ".sqlite" files. For more information on using the sqlite3 command line interface I highly recommend "Digital Forensics With Open Source Tools" (Altheide and Carvey) p.150.

      For Perl scripts, we need to ensure that our SIFT's DBI (Database Interface?) package is up to date by typing: "sudo cpan DBI" (Warning: it takes a while to update!).
      Incidentally, there are other Perl CPAN packages available (ie DBD::SQLite and SQLite::DB) which are supposed to make it easier to interact with SQLite databases (eg make it more Object Oriented) but whilst I found they were able to read Firefox 3.5.17 ".sqlite" files, they were not able to read Firefox 11 ".sqlite" files.
      So that's when I decided to re-write my initial script using both Kristinn's script and this article by Mark-Jason Dominus as guides.

      Code

      OK so here's my script code:

      # CODE BEGINS AFTER THIS LINE
      #!/usr/bin/perl -w

      # Perl script to parse Firefox places.sqlite and downloads.sqlite
      # Based on Kristinn Gudjonsson's "ff3histview" Perl script (http://blog.kiddaland.net/dw/ff3histview) and
      # Mark-Jason Dominus's "A Short Guide to DBI" article (http://www.perl.com/pub/1999/10/DBI.html)
      # Works with SIFT's Firefox V3.6.17 and WinXP's Firefox V11.0
      # Be sure to run "sudo cpan DBI" to update the DBI Perl package before running!

      use strict;

      use Getopt::Long;
      use DBI;

      my $version = "ffparser.pl v2012-03-19";
      my $help = 0;
      my $bk = 0;
      my $dload = 0;
      my $hist = 0;
      my $path = "";

      GetOptions('help|h' => \$help,
          'bk' => \$bk,
          'dload' => \$dload,
          'hist' => \$hist,
          'path=s' => \$path);

      if ($help || $path eq "" || (!$bk and !$dload and !$hist))
      {
          print("\nHelp for $version\n\n");
          print("Perl script to parse Firefox places.sqlite and downloads.sqlite\n");
          print("\nUsage: ffparser.pl [-h|help] [-path pathname] [-bk] [-dload] [-hist]\n");
          print("-h|help .......... Help (print this information). Does not run anything else.\n");
          print("-path pathname ... Path to folder containing places.sqlite and downloads.sqlite.\n");
          print("-bk .............. Parse for Bookmarks (Date Added, Title, URL, Count).\n");
          print("-dload ........... Parse for Downloaded items (Download Ended, Source, Target, Current No. Bytes).\n");
          print("-hist ............ Parse for History (Date Visited, Title, URL, Count).\n");
          print("\nExample: ffparser.pl -path /cases/firefox/ -bk -dload -hist");
          print("\nNote: Trailing / at end of path\n");
          exit;
      }

      # For now, ass-ume downloads.sqlite, places.sqlite are in the path provided
      # Also, ass-ume that the path has a trailing "/" eg TAB autocompletion used
      print "Running $version\n";

      # Try read-only opening "places.sqlite" to extract the Big Endian 4 byte SQLite Version number at bytes 96-100
      # The version number will be in the form (X*1000000 + Y*1000 + Z)
      # where X is the major version number (3 for SQLite3), Y is the minor version number and Z is the release number
      # eg 3007004 for 3.7.4
      my $placesver=0;
      open(my $placesfile, "<".$path."places.sqlite") || die("Unable to open places.sqlite for version retrieval\n");
      #binmode($placesfile);
      seek ($placesfile, 96, 0);
      sysread ($placesfile, $placesver, 4)|| die("Unable to read places.sqlite for version retrieval\n");;
      # Treat the 4 bytes as a Big Endian Integer
      my $placesversion = unpack("N", $placesver);
      print("\nplaces.sqlite SQLite Version is: $placesversion\n");
      close($placesfile);

      # Extract/Print the SQLite version number for downloads.sqlite as well
      my $dloadsever=0;
      open(my $dloadsfile, "<".$path."downloads.sqlite") || die("Unable to open downloads.sqlite for version retrieval\n");
      #binmode($dloadsfile);
      seek ($dloadsfile, 96, 0);
      sysread ($dloadsfile, $dloadsever, 4)|| die("Unable to read downloads.sqlite for version retrieval\n");;
      # Treat the 4 bytes as a Big Endian Integer
      my $dloadsversion = unpack("N", $dloadsever);
      print("downloads.sqlite SQLite Version is: $dloadsversion\n");
      close($dloadsfile);

      # Open the places.sqlite database file first
      if ($bk or $hist)
      {
          my $db = DBI->connect("dbi:SQLite:dbname=$path"."places.sqlite","","") || die( "Unable to connect to database\n" );
         
          # Checks if this is a valid Firefox places.sqlite
          $db->prepare("SELECT id FROM moz_places LIMIT 1") || die("The database is not a correct Firefox database".$db->errstr);

          if ($bk)
          {
              print "\nNow Retrieving Bookmarks ...\n";

              my $sth =  $db->prepare("SELECT datetime(moz_bookmarks.dateAdded/1000000, 'unixepoch') AS \'Date Added\', moz_bookmarks.title AS Title, moz_places.url AS URL, moz_places.visit_count AS Count FROM moz_bookmarks, moz_places WHERE moz_places.id = moz_bookmarks.fk ORDER BY moz_bookmarks.dateAdded ASC");
         
              $sth->execute();

              print $sth->{NUM_OF_FIELDS}." fields will be returned\n";
              PrintHeadings($sth);
              PrintResults($sth);

              # We print out the no. rows now because apparently $sth->rows isn't set until AFTER
              #  $sth->fetchrow_array() has completed in PrintResults
              if ($sth->rows == 0)
              {
                  print "No Bookmarks found!\n\n";
              }
              else
              {   
                  print $sth->rows." Rows returned\n";
              }
              $sth->finish;
          }

          if ($hist)
          {
              print "\nNow Retrieving History ...\n";

              my $sth =  $db->prepare("SELECT datetime(moz_historyvisits.visit_date/1000000, 'unixepoch') AS \'Date Visited\', moz_places.title AS Title, moz_places.url AS URL, moz_places.visit_count AS Count FROM moz_historyvisits, moz_places WHERE moz_historyvisits.place_id = moz_places.id ORDER BY moz_historyvisits.visit_date ASC");
         
              $sth->execute();

              print $sth->{NUM_OF_FIELDS}." fields will be returned\n";
              PrintHeadings($sth);
              PrintResults($sth);

              if ($sth->rows == 0)
              {
                  print "No History found!\n\n";
              }
              else
              {   
                  print $sth->rows." Rows returned\n";
              }

              $sth->finish;
          }

          $db->disconnect;
      }

      if ($dload)
      {
          # Now we open the downloads.sqlite database file
          print "\nNow Retrieving Downloads ...\n";

          my $db = DBI->connect("dbi:SQLite:dbname=$path"."downloads.sqlite","","") || die( "Unable to connect to database\n" );

          # No further checks, we go straight into our query because it IS possible to have an empty moz_downloads table.
          my $sth =  $db->prepare("SELECT datetime(endTime/1000000, 'unixepoch') AS \'Download Ended\', source AS Source, target AS Target,  currBytes as \'Current No. Bytes\' FROM moz_downloads ORDER BY moz_downloads.endTime ASC");
         
          $sth->execute();

          print $sth->{NUM_OF_FIELDS}." fields will be returned\n";
          PrintHeadings($sth);
          PrintResults($sth);

          if ($sth->rows == 0)
          {
              print "No Downloads found!\n\n";
          }
          else
          {   
              print $sth->rows." Rows returned\n";
          }

          $sth->finish;

          $db->disconnect;
      }

      # end main

      sub PrintHeadings
      {
          my $sth = shift;

          # Print field headings
          for (my $i = 0; $i <= $sth->{NUM_OF_FIELDS}-1; $i++)
          {
              if ($i == $sth->{NUM_OF_FIELDS} - 1)
              {
                  print $sth->{NAME}->[$i]."\n"; #last item adds a newline char
              }
              else
              {   
                  print $sth->{NAME}->[$i]." | ";
              }
          }
      }

      sub PrintResults
      {
          my $sth = shift;
          my @rowarray;

          # Prints row by row / field by field
          while (@rowarray = $sth->fetchrow_array() )
          {
              for (my $i = 0; $i <= $sth->{NUM_OF_FIELDS}-1; $i++)
              {
                  if ($i == $sth->{NUM_OF_FIELDS} - 1 )
                  {
                      print $rowarray[$i]."\n"; #last field in row adds newline
                  }
                  else
                  {
                      if ($rowarray[$i])
                      {
                          print $rowarray[$i]." | ";
                      }
                      else
                      {
                          print " | "; # field returned could be UNDEFINED, just print separator
                      }
                  }
              }
          }
      }

      #END CODE


      Coding Summary

      So after starting off with the usual GetOptions command line parsing and the Help message sections, we try to extract the SQLite version number from "places.sqlite" file in the user nominated path. After I had trouble using sqlite3 (V3.6.16) to read Firefox 11 ".sqlite" files, I decided to print out the SQLite version in this script. It can't hurt to know the version of the SQLite database file you're investigating eh?
      According to the file format described here, this version number will be at bytes 96 to 100 inclusive. It will be in the form X00Y00Z as described here eg 3007004 represents 3.7.4.
      So we "open" the file for reading only (using the "<" argument), we "seek" to byte 96, then call "sysread" to store the 4 bytes in local variable called "$placesver". Our extraction isn't quite complete because Perl isn't sure how to interpret "$placesver". So we need to call "unpack" with the parameter "N" to tell Perl to interpret this variable as a 32 bit (or 4 byte) Big Endian Integer. See the section marked "How to Eat an Egg on a Net" here for further details. Once we have unpacked our number into a separate local variable ("$placesversion"), we print it to the command line and close the "places.sqlite" file.

      We repeat this process for the "downloads.sqlite" file.

      The next 3 sections are based on the following theme:
      - Call "DBI->connect" to open/connect to the relevant ".sqlite" file
      - Call "prepare" to prepare an SQL SELECT statement
      - Call "execute" for that statement
      - Print out the number of fields returned by the statement
      - Print out the Title Headers for each field/column (via our "PrintHeaders" function)
      - Print out each row of results returned by the statement (via our "PrintResults" function)
      - Call "finish" to free up any statement allocated resources
      - Call "disconnect" to hang up the connection with the ".sqlite" file

      The hearts of the whole program are the 3 SQL queries. The first 2 are run against the "places.sqlite" file.

      This is the Bookmark retrieval query:
      "SELECT datetime(moz_bookmarks.dateAdded/1000000, 'unixepoch') AS \'Date Added\', moz_bookmarks.title AS Title, moz_places.url AS URL, moz_places.visit_count AS Count FROM moz_bookmarks, moz_places WHERE moz_places.id = moz_bookmarks.fk ORDER BY moz_bookmarks.dateAdded ASC"

      Its basically saying: "Match up the table entries where the moz_places.id equals the moz_bookmarks.fk field and return a human readable version of the moz_bookmarks.dateAdded field, the moz_bookmarks.title field, the moz_places.url and moz_places.visit_count".
      The use of the keyword AS indicates that we wish to return result names under an alias. For example, instead of returning the name "moz.place.url" and its value, the query returns "URL" and the value.
      "Digital Forensics With Open Source Tools" (Altheide and Carvey) p. 150 has more details on how we determine the human readable time format.

      This is the History retrieval query:
      "SELECT datetime(moz_historyvisits.visit_date/1000000, 'unixepoch') AS \'Date Visited\', moz_places.title AS Title, moz_places.url AS URL, moz_places.visit_count AS Count FROM moz_historyvisits, moz_places WHERE moz_historyvisits.place_id = moz_places.id ORDER BY moz_historyvisits.visit_date ASC"

      This statement is *very* similar to the one explained in "Digital Forensics With Open Source Tools" (Altheide and Carvey) p.150 except it also returns the "moz_places.visit_count" field as well.

      Finally, the Downloads query is run against the "downloads.sqlite" file:
      "SELECT datetime(endTime/1000000, 'unixepoch') AS \'Download Ended\', source AS Source, target AS Target,  currBytes as \'Current No. Bytes\' FROM moz_downloads ORDER BY moz_downloads.endTime ASC"

      All 3 of these statements are run depending on the user's command line flags (eg -bk -hist -dload).

      Testing

      OK lets see how we went reading both Firefox 3.5.17 and 11 ".sqlite" files shall we?

      I've been running SIFT's Firefox 3.5.17 in non-"Private Browsing" mode for a while now. So I copied the ".sqlite" files into "/home/sansforensics/ffexamples/" and then ran our script against the copied examples.

      First we try analysing the Firefox 3.5.17 History:

      sansforensics@SIFT-Workstation:~$ ffparser.pl -path ffexamples/ -hist
      Running ffparser.pl v2012-03-19

      places.sqlite SQLite Version is: 3007004
      downloads.sqlite SQLite Version is: 3007004

      Now Retrieving History ...
      4 fields will be returned
      Date Visited | Title | URL | Count
      2011-11-16 20:37:31 | SANS Computer Forensics Training, Incident Response | http://computer-forensics.sans.org/ | 27
      2011-11-16 20:57:56 | SANS Computer Forensics Training, Incident Response | http://computer-forensics.sans.org/ | 27
      2011-11-16 21:02:08 | SANS Computer Forensics Training, Incident Response | http://computer-forensics.sans.org/ | 27
      2011-12-05 20:06:26 | SANS Computer Forensics Training, Incident Response | http://computer-forensics.sans.org/ | 27
      2011-12-05 20:06:37 | blog | http://computer-forensics.sans.org/blog | 4
      2011-12-11 05:56:16 | SANS Computer Forensics Training, Incident Response | http://computer-forensics.sans.org/ | 27
      ... (Edited out)
      2012-03-17 06:13:16 | Google | http://www.google.com.au/ | 75
      2012-03-17 06:13:22 | SQLite::DB - search.cpan.org | http://search.cpan.org/~vxx/SQLite-DB-0.04/lib/SQLite/DB.pm | 2
      2012-03-17 06:18:19 | ff3histview | http://blog.kiddaland.net/dw/ff3histview | 3
      1472 Rows returned
      sansforensics@SIFT-Workstation:~$


      Phew! Nothing too embarrassing there ;) 1472 rows though - I gotta clear that history more often!
      Hint: You can also run "ffparser.pl -path ffexamples/ -hist | more" so you can actually see the results before they scroll off the screen.
      Also note, the SQLite version info for Firefox 3.5.17 is 3.7.4.

      Next, we try looking at the Firefox 3.5.17 Bookmarks:

      sansforensics@SIFT-Workstation:~$ ffparser.pl -path ffexamples/ -bk
      Running ffparser.pl v2012-03-19

      places.sqlite SQLite Version is: 3007004
      downloads.sqlite SQLite Version is: 3007004

      Now Retrieving Bookmarks ...
      4 fields will be returned
      Date Added | Title | URL | Count
      2007-06-06 11:38:27 | Ubuntu | http://www.ubuntulinux.org/ | 0
      2007-06-06 11:38:27 | Ubuntu Wiki (community-edited website) | http://www.ubuntulinux.org/wiki/FrontPage | 1
      2007-06-06 11:38:27 | Make a Support Request to the Ubuntu Community | https://launchpad.net/distros/ubuntu/+addticket
      ... (Edited out)
      2012-02-02 01:48:44 | Perl - Wikipedia, the free encyclopedia | http://en.wikipedia.org/wiki/Perl | 15
      2012-02-14 06:26:14 | ExifTool by Phil Harvey | http://www.sno.phy.queensu.ca/~phil/exiftool/ | 4
      2012-02-14 11:10:51 | The Perl Programming Language - www.perl.org | http://www.perl.org/ | 18
      2012-02-15 12:59:30 | perlintro - perldoc.perl.org | http://perldoc.perl.org/perlintro.html | 11
      2012-02-15 23:23:41 | perlstyle - perldoc.perl.org | http://perldoc.perl.org/perlstyle.html | 2
      2012-02-29 05:06:19 | The CPAN Search Site - search.cpan.org | http://search.cpan.org/ | 18
      2012-03-04 09:05:41 | Ubuntu Forums | http://ubuntuforums.org/ | 1
      2012-03-16 11:37:26 | PerlMonks - The Monastery Gates | http://perlmonks.org/? | 1
      2012-03-16 11:40:23 | Stack Overflow | http://stackoverflow.com/ | 1
      33 Rows returned
      sansforensics@SIFT-Workstation:~$


      OK, now let's check the SIFT's Firefox 3.5.17 Downloads history.

      sansforensics@SIFT-Workstation:~$ ffparser.pl -path ffexamples/ -dload
      Running ffparser.pl v2012-03-19

      places.sqlite SQLite Version is: 3007004
      downloads.sqlite SQLite Version is: 3007004

      Now Retrieving Downloads ...
      4 fields will be returned
      Download Ended | Source | Target | Current No. Bytes
      2012-02-14 06:15:02 | http://blob.perl.org/books/impatient-perl/iperl.pdf | file:///root/Desktop/iperl.pdf | 480850
      2012-02-14 06:39:42 | http://owl.phy.queensu.ca/~phil/exiftool/Vodafone.tar.gz | file:///root/Desktop/Vodafone.tar.gz | 61366
      2012-02-14 06:40:43 | http://owl.phy.queensu.ca/~phil/exiftool/Vodafone.tar.gz | file:///root/Documents/Vodafone.tar.gz | 61366
      ... (Edited out)
      2012-03-16 12:06:29 | http://www.forensicswiki.org/w/images/d/d0/Firefox3_places_relationship_schema.png | file:///cases/Firefox3_places_relationship_schema.png | 24481
      21 Rows returned
      sansforensics@SIFT-Workstation:~$


      Everything looks cool so far eh? As a check, I used the SQLite Manager plugin to view these same files and saw that randomly selected entries matched the script.

      Now let's try using our script with Firefox 11 files in "Private Browsing" mode. For this part, I browsed using WinXP Firefox 11 and downloaded a copy of ProcMon. I then copied the ".sqlite" files over to SIFT's "/cases/firefox11-prv/" directory.

      First, lets see if we can find any Firefox 11 "Private Browsing" History info:

      sansforensics@SIFT-Workstation:~$ ffparser.pl -path /cases/firefox11-prv/ -hist
      Running ffparser.pl v2012-03-19

      places.sqlite SQLite Version is: 3007007
      downloads.sqlite SQLite Version is: 3007007

      Now Retrieving History ...
      4 fields will be returned
      Date Visited | Title | URL | Count
      No History found!

      sansforensics@SIFT-Workstation:~$


      No real surprise there - the history appears to be empty.
      Also note, the SQLite version info for Firefox 11 is 3.7.7 (ie a later version than Firefox 3.5.17's).

      Lets look at the Firefox 11 "Private Browsing" Bookmarks info:

      sansforensics@SIFT-Workstation:~$ ffparser.pl -path /cases/firefox11-prv/ -bk | more
      Running ffparser.pl v2012-03-19

      places.sqlite SQLite Version is: 3007007
      downloads.sqlite SQLite Version is: 3007007

      Now Retrieving Bookmarks ...
      4 fields will be returned
      Date Added | Title | URL | Count
      2009-03-08 16:26:34 | NFL.com - Official Site of the National Football League | http://www.nfl.com/ | 0
      2009-03-08 16:29:28 | The National Football Post | http://www.nationalfootballpost.com/ | 0
      2009-03-08 16:32:05 | Breaking news, real-time scores and daily analysis from Sports Illustrated – SI.com | http://sportsillustrated.cnn.com/ |
      0
      ... (edited)
      2012-02-13 18:09:28 | ExifTool by Phil Harvey | http://www.sno.phy.queensu.ca/~phil/exiftool/index.html | 0
      2012-02-20 09:29:53 | The Perl Programming Language - www.perl.org | http://www.perl.org/ | 0
      2012-02-20 09:30:10 | The CPAN Search Site - search.cpan.org | http://search.cpan.org/ | 0
      473 Rows returned
      sansforensics@SIFT-Workstation:~$


      As expected, the Bookmark information is retained regardless of browser mode.
      Now, lets see whats in the Firefox 11 "Private Browsing" Downloads info:

      sansforensics@SIFT-Workstation:~$ ffparser.pl -path /cases/firefox11-prv/ -dload
      Running ffparser.pl v2012-03-19

      places.sqlite SQLite Version is: 3007007
      downloads.sqlite SQLite Version is: 3007007

      Now Retrieving Downloads ...
      4 fields will be returned
      Download Ended | Source | Target | Current No. Bytes
      No Downloads found!

      sansforensics@SIFT-Workstation:~$


      No real surprises there either - the Downloads history also appears empty in "Private Browsing" mode.

      And finally(!!!), here's the results of our script with Firefox 11 in non-"Private Browsing" mode. For this part, I browsed using WinXP Firefox 11 and downloaded a copy of ProcMon. I then copied the ".sqlite" files over to SIFT's "/cases/firefox11/" directory.

      Here's the Firefox 11 in non-"Private Browsing" History info:

      sansforensics@SIFT-Workstation:~$ ffparser.pl -path /cases/firefox11/ -hist
      Running ffparser.pl v2012-03-19

      places.sqlite SQLite Version is: 3007007
      downloads.sqlite SQLite Version is: 3007007

      Now Retrieving History ...
      4 fields will be returned
      Date Visited | Title | URL | Count
      2012-03-19 11:54:15 | Google | http://www.google.com.au/ | 1
      2012-03-19 11:55:05 | Windows Sysinternals: Documentation, downloads and additional resources | http://technet.microsoft.com/en-us/sysinternals/default.aspx | 1
      2012-03-19 11:56:00 | Sysinternals Process Utilities | http://technet.microsoft.com/en-us/sysinternals/bb795533 | 1
      2012-03-19 11:56:33 | Process Monitor | http://technet.microsoft.com/en-us/sysinternals/bb896645 | 1
      2012-03-19 11:56:39 | ProcessMonitor.zip | http://download.sysinternals.com/files/ProcessMonitor.zip | 0
      5 Rows returned
      sansforensics@SIFT-Workstation:~$


      And the Firefox 11 in non-"Private Browsing" Bookmark info:

      sansforensics@SIFT-Workstation:~$ ffparser.pl -path /cases/firefox11/ -bk | more
      Running ffparser.pl v2012-03-19

      places.sqlite SQLite Version is: 3007007
      downloads.sqlite SQLite Version is: 3007007

      Now Retrieving Bookmarks ...
      4 fields will be returned
      Date Added | Title | URL | Count
      2009-03-08 16:26:34 | NFL.com - Official Site of the National Football League | http://www.nfl.com/ | 0
      2009-03-08 16:29:28 | The National Football Post | http://www.nationalfootballpost.com/ | 0
      2009-03-08 16:32:05 | Breaking news, real-time scores and daily analysis from Sports Illustrated – SI.com | http://sportsillustrated.cnn.com/ |
      0
      ... (edited)
      2012-02-13 18:09:28 | ExifTool by Phil Harvey | http://www.sno.phy.queensu.ca/~phil/exiftool/index.html | 0
      2012-02-20 09:29:53 | The Perl Programming Language - www.perl.org | http://www.perl.org/ | 0
      2012-02-20 09:30:10 | The CPAN Search Site - search.cpan.org | http://search.cpan.org/ | 0
      473 Rows returned
      sansforensics@SIFT-Workstation:~$


      Which unsurprisingly is the same results as for the "Private Browsing" Bookmarks section detailed above.

      And finally, the Firefox 11 in non-"Private Browsing" Downloads output:

      sansforensics@SIFT-Workstation:~$ ffparser.pl -path /cases/firefox11/ -dload
      Running ffparser.pl v2012-03-19

      places.sqlite SQLite Version is: 3007007
      downloads.sqlite SQLite Version is: 3007007

      Now Retrieving Downloads ...
      4 fields will be returned
      Download Ended | Source | Target | Current No. Bytes
      2012-03-19 11:56:57 | http://download.sysinternals.com/files/ProcessMonitor.zip | file:///C:/ProcessMonitor.zip | 1324628
      1 Rows returned
      sansforensics@SIFT-Workstation:~$


      Pretty cool - we can see where we went to the SysInternals website and downloaded ProcessMonitor.zip.

      Some Interesting(?) Side Notes:

      If you type:
      about:cache
      into the Firefox address bar (both 3.6 and 11), Firefox displays the current cache contents via clickable html-like interface. Note: "Private Browsing" automatically clears the cache when Firefox is closed.

      With Firefox 11 in "Private Browsing"/"Forget History" mode, CCleaner v3.14.1616 will detect and remove "downloads.sqlite", "content-prefs.sqlite", "formhistory.sqlite".
      With Firefox 11 in non-"Private Browsing"/"Remember History" mode, CCleaner v3.14.1616 will detect and remove the cookies (but not the "cookies.sqlite" file), the user profile cache and the following sqlite files - "downloads.sqlite", "content-prefs.sqlite", "formhistory.sqlite", "signons.sqlite". Depending on what sites are visited, some of the ".sqlite" files may not be created (eg signons.sqlite).

      If you (masochistically) want to view the Firefox source code, its available here.

      And we're done!
      If you're still reading, it's time to grab a beverage ;)
      Or if you skipped ahead to get the summary (you missed out on the fun!) - we have successfully used Perl::DBI to read Firefox 3.5.17 and 11 ".sqlite" files for history, bookmark and download information. Reading other software's ".sqlite" files will follow a similar process.
      Thanks must go to Kristinn Gudjonsson for providing his original "ff3histview" script upon which this particular house of cards is based.
      Hopefully, you've gotten something out of this uber long post - there is a lot of information out there and if in doubt, I  added it in here.

      Comments/Suggestions are welcome! I think I'll leave any SQLite carving attempts for now though - it seems a little daunting for this monkey.

      Perl Parsing an SQLite Database File Header

      $
      0
      0

      Previously on Cheeky4n6Monkey ... we used the Perl DBI package to retrieve the contents of Firefox's (v3.5.17 and v11.0) "places.sqlite" and "downloads.sqlite". A secondary discovery was that depending on the version of the sqlite3 command line exe, one might be able to use it to read Firefox 11 ".sqlite" files ... or not. Just to clarify, sqlite3 v3.6.16 cannot read Firefox 11 files but sqlite3 (v3.7.10) can.
      Anyway, rather than waste time trying different versions of sqlite3 on various ".sqlite" files, I thought a Perl script to parse the ".sqlite" file header might be handy.
      Even if it's not particularly unique/useful (there's already an SQLite Manager Firefox plugin), at least we get more practice with Perl. Also, as far as I know, SQLite Manager does not show the SQLite Version nor does it show a meaningful Schema Format string.

      The relevant SQLite file structure is detailed here. Rather than extract all of the fields, I thought I would initially concentrate on 6 fields:
      - SQLite Version eg 3007007
      - SQLite Schema Format eg SQLite v3.0.0
      - Page Size (in bytes)
      - Number of Pages
      - Number of Pages on Freelist
      - Text Encoding used eg UTF-8

      The last 4 fields can be verified/viewed using SQLite Manager and the first 2 fields can be verified/viewed using the SIFT'sBless Hex Editor.

      So lets get stuck into the coding!


      Code

      # CODE BEGINS AFTER THIS LINE
      #!/usr/bin/perl -w

      # Perl script to parse selected SQLite Database header fields
      # Based on the Database Header section of http://www.sqlite.org/fileformat2.html

      use strict;

      use Getopt::Long;
      use Encode;

      my $version = "sqlite-parser.pl v2012-03-21";
      my $help = 0;
      my $filename = "";

      GetOptions('help|h' => \$help,
          'file=s' => \$filename);

      if ($help || $filename eq "")
      {
          print("\nHelp for $version\n\n");
          print("Perl script to parse selected SQLite header fields\n");
          print("\nUsage: sqlite-parser.pl [-h|help] [-file filename]\n");
          print("-h|help .......... Help (print this information). Does not run anything else.\n");
          print("-file filename ... sqlite filename to be parsed.\n");
          print("\nExample: sqlite-parser.pl -file /cases/firefox/places.sqlite\n");
          exit;
      }

      print "\nRunning $version\n\n";

      # Try read-only opening the SQLite file to extract various header information
      my $rawsqliteheader;

      open(my $sqlitefile, "<".$filename) || die("Unable to open $filename for header parsing\n");
      #binmode($sqlitefile);
      sysread ($sqlitefile, $rawsqliteheader, 100) || die("Unable to read $filename for header parsing\n");;
      close($sqlitefile);

      # First check that we have a valid header 1st 16 bytes should read "SQLite format 3\000" in UTF8
      # or alternatively "53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00" in hex
      my $rawsqlitestring = substr($rawsqliteheader, 0, 16);
      my $sqlitestring = decode("UTF8", $rawsqlitestring);
      if ($sqlitestring eq "SQLite format 3\000")
      {
          print "SQLite String Parsed OK - Continuing Processing of $filename ...\n";
      }
      else
      {
          print "$filename does NOT have a Valid SQLite String in Header! Bailing out ...\n\n";
          exit;
      }


      # Extract the database page size in bytes
      # Should/Must be a power of two between 512 and 32768 inclusive, or the value 1 representing a page size of 65536
      my $rawdbpagesize = substr($rawsqliteheader, 16, 2);
      my $dbpagesize = unpack("n", $rawdbpagesize); # Use "n" for 2 byte int
      if ($dbpagesize eq 1)
      {
          print("Database Page Size (bytes) = 65536\n");
      }
      else
      {
          print("Database Page Size (bytes) = $dbpagesize\n");
      }


      # Extract the size of the database file in pages.
      my $rawnumpages = substr($rawsqliteheader, 28, 4);
      my $numpages = unpack("N", $rawnumpages); # use "N" for 4 byte int
      if ($numpages ne 0)
      {
          # Must check that changecounter = validversionfor
          # as validversionfor stores the current changecounter value after the SQLite version number was written
          # (eg at creation)
          my $rawchangecounter = substr($rawsqliteheader, 24, 4);
          my $changecounter = unpack("N", $rawchangecounter);

          my $rawvalidversionfor = substr($rawsqliteheader, 92, 4);
          my $validversionfor = unpack("N", $rawvalidversionfor);

      #    print "changecounter = $changecounter\n";
      #    print "validversionfor = $validversionfor\n";

          if ($changecounter eq $validversionfor)
          {
              print("Valid Number of Pages = $numpages\n");
          }
          else
          {
              print("Invalid Number of Pages! (mismatched changecounter value)\n");
          }
      }
      else
      {
          print("Invalid Number of Pages! (zero)\n");
      }


      # Extract the total number of freelist pages.
      my $rawnumfreelistpages = substr($rawsqliteheader, 36, 4);
      my $numfreelistpages = unpack("N", $rawnumfreelistpages); # use "N" for 4 byte int
      print("Total Number of Freelist Pages = $numfreelistpages\n");


      # Extract the schema format number. Supported schema formats are 1, 2, 3, and 4.
      my $rawschemaformatnum = substr($rawsqliteheader, 44, 4);
      my $schemaformatnum = unpack("N", $rawschemaformatnum); # use "N" for 4 byte int
      #print("Schema Format Number = $schemaformatnum\n");
      if ($schemaformatnum == 1)
      {
          print("Schema Format = SQLite v3.0.0\n");
      }
      elsif ($schemaformatnum == 2)
      {
          print("Schema Format = SQLite v3.1.3 (2005)\n");
      }
      elsif ($schemaformatnum == 3)
      {
          print("Schema Format = SQLite v3.1.4 (2005)\n");
      }
      elsif ($schemaformatnum == 4)
      {
          print("Schema Format = SQLite v3.3.0 (2006) or higher\n");
      }
      else
      {
          print("Invalid Schema Format!\n");
      }


      # Extract the database text encoding. A value of 1 means UTF-8. A value of 2 means UTF-16le. A value of 3 means UTF-16be.
      my $rawtextencode = substr($rawsqliteheader, 56, 4);
      my $textencode = unpack("N", $rawtextencode); # use "N" for 4 byte int

      #print("Text Encoding = $textencode\n");
      if ($textencode == 1)
      {
          print("Text Encoding = UTF-8\n");
      }
      elsif ($textencode == 2)
      {
          print("Text Encoding = UTF-16le\n");
      }
      elsif ($textencode == 3)
      {
          print("Text Encoding = UTF-16be\n");
      }
      else
      {
          print("Invalid Text Encoding!\n");
      }


      # Extract the SQLite Version number as a 4 byte Big Endian Integer at bytes 96-100
      # The version number will be in the form (X*1000000 + Y*1000 + Z)
      # where X is the major version number (3 for SQLite3), Y is the minor version number and Z is the release number
      # eg 3007004 for 3.7.4
      my $rawsqliteversion = substr($rawsqliteheader, 96, 4);
      my $sqlversion = unpack("N", $rawsqliteversion);
      print("SQLite Version is: $sqlversion\n\n");

      # CODE ENDS HERE


      Code Summary

      As ".sqlite" files contain a UTF-8 encoded header string we need to use the "decode" function from the Encode Perl package. So the first thing to note is the "use Encode" statement.
      Next, we have the familiar GetOptions and Help sections. Yawn!
      And now the journey really begins ...
      We call a read-only "open" on the user specified filename and then "sysread" in the first 100 bytes into the "$rawsqliteheader" variable.
      Now that we have a copy of what should be the file header, we can now "close" the user specified file.

      The first thing we do is look for the UTF-8 encoded "SQLite format 3\000" string. We use "substr" to copy the first 16 bytes into "$rawsqlitestring" from our 100 byte "$rawsqliteheader" buffer.
      Next we call "decode" to convert "$rawsqlitestring" into a Perl usable string format and store the result in "$sqlitestring". See here for more information on encoding/decoding with Perl.
      Finally, we test the value of "$sqlitestring" and "exit" if it does not match "SQLite format 3\000".
      Pretty straight forward eh?

      Similarly, for the Database Page Size field - we call "substr" to copy our bytes (in this case only 2) but instead of calling "decode" we use "unpack" for interpreting numbers. There's a bit of a hack going on with this particular field - if it's value is 1, the actual size used is supposed to be 65536 bytes. So we include an "if" statement to handle that.

      I could go on but the other parsing sections do pretty much the same thing (except they call "substr" to extract 4 bytes from various offsets).


      Testing

      Here's the output of our "sqlite-parse.pl" script with a bogus fabricated file we prepared for an earlier post:

      sansforensics@SIFT-Workstation:~$ sqlite-parser.pl -file /cases/cheeky-file.c4n6

      Running sqlite-parser.pl v2012-03-21

      /cases/cheeky-file.c4n6 does NOT have a Valid SQLite String in Header! Bailing out ...

      sansforensics@SIFT-Workstation:~$


      OK so that shows that our's is a discerning script ... we don't accept any old riff-raff claiming to be SQLite!
      Now lets try it with a Firefox 11.0 "places.sqlite" file:

      sansforensics@SIFT-Workstation:~$ sqlite-parser.pl -file /cases/firefox11/places.sqlite

      Running sqlite-parser.pl v2012-03-21

      SQLite String Parsed OK - Continuing Processing of /cases/firefox11/places.sqlite ...
      Database Page Size (bytes) = 4096
      Valid Number of Pages = 145
      Total Number of Freelist Pages = 5
      Schema Format = SQLite v3.1.3 (2005)
      Text Encoding = UTF-8
      SQLite Version is: 3007007

      sansforensics@SIFT-Workstation:~$


      That looks OK but we should validate our results using the SIFT's SQLite Manager Firefox plugin.

      "SQLite Manager" Firefox Plugin Validating Our Script Output

      As you can see, the SQLite Manager confirms our scripts "Database Page Size" (4096), "Valid Number of Pages" (145), "Total Number of Freelist Pages" (5) and "Text Encoding" (UTF-8) values.


      Now we will use the SIFT's Bless Hex Editor to validate our script's results for the "SQLite Version"  and "Schema Format". You can access Bless from the Applications ... Programming ... Bless Hex Editor menu.

      Bless Hex Editor Validating Our Script's outputted SQLite Version (3007007)

      Bless Hex Editor Validating Our Script's outputted Schema Format (2)

      OK, the Bless screenshots confirm our script's "SQLite Version" (3007007) and "Schema Format" (2, which corresponds to "SQLite v3.1.3"). Dare I say, we've been blessed? heheh

      For extra giggity-giggity-giggles, lets now use our script with a Firefox 3.5.17 "places.sqlite" file:

      sansforensics@SIFT-Workstation:~$ sqlite-parser.pl -file ffexamples/places.sqlite


      Running sqlite-parser.pl v2012-03-21


      SQLite String Parsed OK - Continuing Processing of ffexamples/places.sqlite ...
      Database Page Size (bytes) = 4096
      Valid Number of Pages = 203
      Total Number of Freelist Pages = 0
      Schema Format = SQLite v3.0.0
      Text Encoding = UTF-8
      SQLite Version is: 3007004


      sansforensics@SIFT-Workstation:~$

      We can see that Firefox 3.5.17 and Firefox 11.0 use different schemas and SQLite versions for their respective "places.sqlite" files. For Firefox 11.0, Schema Format = SQLite v3.1.3 (2005), SQLite Version is: 3007007. For Firefox 3.5.17, Schema Format = SQLite v3.0.0, SQLite Version is: 3007004.
      This information would have been handy before we started trying to open Firefox 11.0 database files with a superseded copy of sqlite3 eh?

      Anyway, so ends another post. Please let me know if you think/find it useful or if you would like any more header fields printed.Cheers!

      Using Perl to Copy AppID Data from HTML to an SQLite Database

      $
      0
      0

      Someday You'll Find It ... The Jumplist Connection!

      So all this talk of Windows 7 Jumplists (eg here and here) got me thinking - What if you needed to look up an AppID and didn't have access to the Internet? Also, Is there any way we can import a list of AppIDs so we can then access them from some future scripts (eg for processing Jumplists)?
      I then had my "Aha!" moment (no, nothing to do with that 80s band), and thought "SQLite!"
      SQLite also has the benefit of being cross-platform - so an AppID List generated on SIFT should work on a PC etc. By using a standard database, we can also ensure that our fearless forensicator lab uses a common set of (unchanging) source data.

      So how exactly can we do this (using the SIFT VM)?
      1. We create an empty SQLite Database.
      2. We parse the ForensicsWiki AppID HTML page for data.
      3. We populate the Database





      Creating The Empty Database

      Creating an (empty) SQLite AppID Database is pretty straight forward. I opted to do this part manually using SIFT's "sqlite3" command line tool.

      1. At a terminal command line, I typed:
      "sqlite3 WinAppIDs.sqlite"
      2. At the resultant "sqlite3" prompt, I typed (without the quotes):
      "CREATE TABLE IF NOT EXISTS WinAppIDs (AppID TEXT, AppDescrip TEXT, DateAdded NUMERIC, Source TEXT);" 
      3. At the next "sqlite3" prompt, I typed (without the quotes):
      "CREATE UNIQUE INDEX IF NOT EXISTS AppIdx ON WinAppIDs(AppID, AppDescrip);"
      4. Finally, at the "sqlite3" prompt, I typed (without the quotes):
      ".quit"

      There should now be a "WinAppIDs.sqlite" database file in the current directory (eg "/home/sansforensics"). This file contains a "WinAppIDs" table which has AppId, AppDescription, DateAdded and Source fields (just like the ForensicsWiki page tables).
      Additionally, I have hacked/cheated a bit and created a Unique Index called "AppIdx". The purpose of the index is to ensure that we cannot insert duplicate entries. That is, we can't insert an entry with the same AppID AND the same AppDescription as an existing one. You wouldn't think this would be a problem with the AppId tables right? Wrong! In the tables there are entries such as:

      Different AppIds, Same Descriptions
      b8c29862d9f95832    Microsoft Office InfoPath 2010 x86
      d64d36b238c843a3    Microsoft Office InfoPath 2010 x86

      Same AppIDs, Different Descriptions
      23646679aaccfae0    Adobe Reader 9.
      23646679aaccfae0    Adobe Reader 9 x64

      So rather than calling a SELECT query to check before every INSERT (there's 400+ AppIDs to insert), we'll just make the INSERT fail (via "AppIdx") when there's already an existing entry with the same AppID and AppDescription. For more on SQLite indexing read here. Specifically it says, "If the UNIQUE keyword appears between CREATE and INDEX then duplicate index entries are not allowed. Any attempt to insert a duplicate entry will result in an error."


      Parsing the ForensicsWiki AppID HTML page for data and Populating the Database

      Parsing/extracting the AppIDs from HTML is easy once you find out somebody's already written a CPAN package for that exact purpose! Its called "HTML::TableExtract" and you can read all about it here.
      Additionally, the folks at ForensicsWiki have also included a "Source" link for each AppID. To extract the link we can use "HTML::LinkExtor". You can read about it here.
      As we have seen in previous posts, we can use the Perl "DBI" package to interface with an SQLite Database. So knowing all this, we should now be able to insert our newly extracted data.
      To simplify this exercise, we will extract the data and then enter it into the database from one script ("4n6wiki-parser.pl").
      Incidentally, the StackOverflow website is a great searchable resource for Perl questions - this was where I not only heard about the previously mentioned packages but also saw example code of their use. Awesome!

      Finally(!), we should write a short query script ("4n6wiki-query.pl") so that we can search our SQLite database by either AppID or AppDescrip.

      Now let's take a look at what this monkey passes off for code ...

      "4n6wiki-parser.pl" Code

      #CODE BEGINS ON NEXT LINE
      #!/usr/bin/perl -w

      # Perl script to parse http://www.forensicswiki.org/wiki/List_of_Jump_List_IDs and import it into an existing SQLite Database

      use strict;

      use Getopt::Long;
      use HTML::TableExtract;
      use HTML::LinkExtor;
      use DBI;

      my $version = "4n6wiki-parser.pl v2012-03-27";
      my $help = 0;
      my $filename = "";
      my $database = "";

      GetOptions('help|h' => \$help,
          'file=s' => \$filename,
          'db=s' => \$database);

      if ($help || $filename eq "" || $database eq "")
      {
          print("\nHelp for $version\n\n");
          print("Perl script to parse http://www.forensicswiki.org/wiki/List_of_Jump_List_IDs and import it into an existing SQLite Database\n");
          print("\nUsage: 4n6wiki-parser.pl [-h|help] [-file filename] [-db database]\n");
          print("-h|help .......... Help (print this information). Does not run anything else.\n");
          print("-file filename ... Copy of HTML file to be parsed.\n");
          print("-db database ..... Target SQLite Database.\n");
          print("\nExample: 4n6wiki-parser.pl -file List_of_Jump_List_IDs.html -db WinAppIDs.sqlite\n");
          exit;
      }

      print "\nRunning $version\n\n";

      open(my $htmlfile, "<".$filename) || die("Unable to open $filename for parsing\n");
      my @lines = <$htmlfile>; # extract each line to a list element
      my $html_string = join(' ', @lines); # join list elements into one big happy scalar string
      close($htmlfile);

      my $tblextract = HTML::TableExtract->new(keep_html => 1, headers => [qw(AppID Application Date Source)] );
      $tblextract->parse($html_string);

      my $lnkextr = HTML::LinkExtor->new();

      my @bigarray; # will be a all table cells merged into one [row][cell] type array
      my $rowcount = 0; # running count of rows in all tables

      foreach my $tbl ($tblextract->tables)
      {
          my @rows = $tbl->rows;
          my @cols = $tbl->columns;
          print "Extracted Table Size = ".scalar(@rows)." x ".scalar(@cols)."\n";

          for my $rownum (0 .. (@rows - 1))
          {
              # Ass-ume always 4 columns, last one (3) contains link info, col 2 contains date eg 8/22/2011
              for my $colnum (0 .. (@cols - 1))
              {
                  if ($colnum == (@cols - 2) )
                  {
                      #reformat date into form yyyy-mm-dd
                      my $date = $tbl->cell($rownum, $colnum);
                      $date =~ /(\d+)\/(\d+)\/(\d+)/;
                      my $year = $3;
                      my $month = $1;
                      my $day = $2;
                      if ($day < 10)
                      {
                          $day = "0".$day;
                      }
                      if ($month < 10)
                      {
                          $month = "0".$month;
                      }
                      my $newdate = $year."-".$month."-".$day;
                      $bigarray[$rowcount][$colnum] = $newdate;
                  }
                  elsif ($colnum == (@cols - 1))
                  {       
                      # Extract a link entry eg "http://social.technet.microsoft.com/Forums/" for col 3
                      $lnkextr->parse($tbl->cell($rownum, $colnum));
                      for my $link_tag ( $lnkextr->links )
                      {
                          my ($tag, %links) = @$link_tag;
                          foreach my $key (keys %links)
                          {
                              $bigarray[$rowcount][$colnum] = $links{$key};
                              last; # only do it for one link then bail out
                          }
                      }
                  }
                  else
                  {
                      #Record each of the other column fields for this row ie col 0, 1, 2
                      $bigarray[$rowcount][$colnum] = $tbl->cell($rownum, $colnum);
                  }
              }
              $rowcount = $rowcount + 1;       
          }
      }

      print "Number of Rows Extracted from HTML = $rowcount\n";

      my $db = DBI->connect("dbi:SQLite:dbname=$database","","") || die( "Unable to connect to database\n" );
      my $sth;

      for my $currow (0 .. ($rowcount-1))
      {
          my $id = $bigarray[$currow][0];
          my $descrip = $bigarray[$currow][1];
          my $date = $bigarray[$currow][2];
          my $source = $bigarray[$currow][3];
          $sth =  $db->prepare_cached("INSERT INTO WinAppIDs (AppID, AppDescrip, DateAdded, Source) VALUES (?, ?, ?, ?)"); #or die "Couldn't prepare statement: ".$db->errstr;
          $sth->execute($id, $descrip, $date, $source); #or die "Couldn't execute statement: ".$sth->errstr;
      }
      $sth->finish;
      $db->disconnect;
      print "$database Update complete ...\n"

      # END CODE


      "4n6wiki-parser.pl" Code Summary

      Before you use this script, ensure that you have installed the following Perl packages: HTML::TableExtract, HTML::LinkExtor, DBI (eg type "sudo cpan HTML::TableExtract" etc.).

      The first section calls "GetOptions" to parse the user's input arguments and then the following section is the Help printout.

      We then read in the user specified HTML file into one long scalar variable ("$html_string").

      Next we create a new HTML::TableExtract object to parse the table data. We use the argument "keep_html => 1" so we can later parse any web links in the table (using HTML::LinkExtor). The other argument, "headers => [qw(AppID Application Date Source)]" tells HTML::TableExtract the column headings so it can figure out how to extract data.
      Note: I just used the first word of each column heading and it seemed to work OK. I don't think you can specify something like "Application Description" for a heading because the whitespace will confuse HTML::TableExtract. If they ever change the column headings to use the same first word (eg "AppID" and "AppID Description"), we're in trouble.
      Anyhow, next we call the "parse" function of HTML::TableExtract to do all the heavy lifting and extract the data.

      Now we create a new HTML::LinkExtor object to extract any HTML links in the table. We then proceed to loop through each row/column of each table that HTML::TableExtract has found (currently, there are 10 tables). We store the result in a huge array of arrays ("bigarray[rownum][colnum]").
      For half of the columns (ie "AppId" and "Application Description"), we will just copy the data straight into "bigarray". The other 2 columns ("Date Added" and "Source") require a bit of massaging before entry (heheh).
      ForensicsWiki lists the dates in the U.S. format of month/day/year (I DARE GU to say it's a better format! LOL) with no preceding zeros for single digits. SQLite uses the yyyy-mm-dd format in it's time calculations. So, just in case we want to sort later by date, our script has to re-arrange the date before storing it in "bigarray". We use a regular expression to extract the various parts of the date and then zero pad them if required before re-assembling them into the SQLite format. The regular expression code is:

      "$date =~ /(\d+)\/(\d+)\/(\d+)/;"

       In the line before this, "$date" was assigned the value from the date cell (eg "8/22/2011"). The "\d+"s indicate a field of multiple digits. The surrounding "( )"s indicate we are extracting the various values (in order) to $1, $2, $3. We also need to "\" the date separators "/" so they don't confuse the regular expression delimiters. More information on Perl Regular expressions is available here.

      I found the basic code for the Link Extraction here. However, we call the HTML::LinkExtor's "parse" function with the table cell content (not the whole row). I found it quite difficult to initially understand this snippet, so I will try to explain using our actual code below.

      $lnkextr->parse($tbl->cell($rownum, $colnum));
      for my $link_tag ( $lnkextr->links )
      {
          my ($tag, %links) = @$link_tag;
          foreach my $key (keys %links)
          {
              $bigarray[$rowcount][$colnum] = $links{$key};
              last; # only do it for one link then bail out
          }
      }


      To get the results of the HTML::LinkExtor's "parse" call, we have to call the "$lnkextr->links" method. According to the CPAN documentation, this returns data in the form:
      "[$tag, $attr => $url1, $attr2 => $url2,...]".
      The enclosing "[ ]"s denote an array containing the "$tag" scalar variable and what looks like a hash list ("$attr => $url1, $attr =>$url2").
      Theoretically, there should only be one "$link_tag" result for our case and therefore, only one for ("my $link_tag") loop iteration.

      In the loop, we now see: "my ($tag, %links) = @$link_tag;"
      The right hand hand side, "@$link_tag" is saying treat this "$link_tag" scalar as a reference to an array (hence the "@"). This array will be the one containing the "$tag" and the hash list mentioned previously.
      The left hand side assigns values for the various parts of the array. So we end up with "$tag" and a hash list array (now called "%links").
      The next step is to iterate through "%links" and for the first key/URL it finds, store that link in "bigarrray".
      For more information on Perl References see here.

      OK, now we should have all the data we need in "bigarray" and we also have an SQLite database file ("WinAppIDs.sqlite") containing an empty table ("WinAppIDs"). Time to feed the machine!
      As we've seen in previous posts, we first call DBI's "connect" to interface with the Database. If it doesn't work we quit and nothing gets inserted.
      Next we loop through each "row" of "bigarray" and store the column values in temporary scalars ($id, $descrip, $date, $source). We then prepare our SQLite INSERT statement:

      $sth =  $db->prepare_cached("INSERT INTO WinAppIDs (AppID, AppDescrip, DateAdded, Source) VALUES (?, ?, ?, ?)"); #or die "Couldn't prepare statement: ".$db->errstr;

      We're using "prepare_cached" because it apparently minimises resources for our repeated INSERTs (seriously?!) compared to a normal "prepare".
      The "?" arguments are placeholders for our temporary values which get assigned in the next line.

      $sth->execute($id, $descrip, $date, $source); #or die "Couldn't execute statement: ".$sth->errstr;

      Note: I've commented out the ("die") error handling parts for "prepare_cached" and "execute" because if we fail to INSERT, it could be due to a duplicate. We should keep looping though "bigarray" because there might be new non-duplicate information later in "bigarray" (eg we might be updating an existing database).

      After we've finished looping through "bigarray", we call "finish" and "close" and print out an "Update Complete" message.


      "4n6wiki-query.pl" Code

      # CODE BEGINS ON NEXT LINE
      #!/usr/bin/perl -w

      # Perl script to query an existing SQLite Database for Windows AppID info
      # Intended to be used in conjunction with the 4n6wiki-parser.pl script.

      use strict;

      use Getopt::Long;
      use DBI;

      my $version = "4n6wiki-query.pl v2012-03-27";
      my $help = 0;
      my $appid = "";
      my $descrip = "";
      my $database = "";

      GetOptions('help|h' => \$help,
          'appid=s' => \$appid,
          'descrip=s' => \$descrip,
          'db=s' => \$database);

      if ($help || $database eq "" || ($descrip eq "" && $appid eq "") || ($descrip ne "" && $appid ne "") )
      {
          print("\nHelp for $version\n\n");
          print("Perl script to query an existing SQLite Database for Windows AppID info\n");
          print("Intended to be used in conjunction with the 4n6wiki-parser.pl script.\n"); 
          print("\nUsage: 4n6wiki-query.pl [-h|help] [-appid APPID] [-descrip Description] [-db database]\n");
          print("-h|help .......... Help (print this information). Does not run anything else.\n");
          print("-appid APPID ..... Search Database for match(es) for this APPID.\n");
          print("-descrip Description ... Search Database for match(es) \"like\" this Description.\n");
          print("-db database ..... Target SQLite Database.\n");
          print("\nExamples: \n");
          print("4n6wiki-parser.pl -appid 3dc02b55e44d6697 -db WinAppIDs.sqlite\n");
          print("4n6wiki-parser.pl -descrip \"Adobe Flash\" -db WinAppIDs.sqlite\n");
          print("Note: Listing BOTH -appid and -descrip will not work / prints this message\n\n");
          exit;
      }

      print "\nRunning $version\n\n";

      my $db = DBI->connect("dbi:SQLite:dbname=$database","","") || die( "Unable to connect to database\n" );
      my $sth;
      if ($appid ne "")
      {
          $sth =  $db->prepare("SELECT AppID, AppDescrip FROM WinAppIDs WHERE AppID=?") or die "Couldn't prepare statement: ".$db->errstr;
          $sth->execute($appid) or die "Couldn't execute statement: ".$sth->errstr;
          PrintHeadings($sth);
          PrintResults($sth);
          if ($sth->rows == 0)
          {
              print "No Matching AppIDs found!\n";
          }
          else
          {   
              print $sth->rows." Matches returned\n";
          }
      }
      elsif ($descrip ne "")
      {
          my $likestr = "%".$descrip."%";
          $sth =  $db->prepare("SELECT AppID, AppDescrip FROM WinAppIDs WHERE AppDescrip LIKE ?") or die "Couldn't prepare statement: ".$db->errstr;
          $sth->execute($likestr) or die "Couldn't execute statement: ".$sth->errstr;
          PrintHeadings($sth);
          PrintResults($sth);
          if ($sth->rows == 0)
          {
              print "No Matching Descriptions found!\n";
          }
          else
          {   
              print $sth->rows." Matches returned\n";
          }
      }

      $sth->finish;
      $db->disconnect;

      # End Main

      sub PrintHeadings
      {
          my $sth = shift;

          # Print field headings
          for (my $i = 0; $i <= $sth->{NUM_OF_FIELDS}-1; $i++)
          {
              if ($i == $sth->{NUM_OF_FIELDS} - 1)
              {
                  print $sth->{NAME}->[$i]."\n"; #last item adds a newline char
              }
              else
              {   
                  print $sth->{NAME}->[$i]." | ";
              }
          }
      }

      sub PrintResults
      {
          my $sth = shift;
          my @rowarray;

          # Prints row by row / field by field
          while (@rowarray = $sth->fetchrow_array() )
          {
              for (my $i = 0; $i <= $sth->{NUM_OF_FIELDS}-1; $i++)
              {
                  if ($i == $sth->{NUM_OF_FIELDS} - 1 )
                  {
                      print $rowarray[$i]."\n"; #last field in row adds newline
                  }
                  else
                  {
                      if ($rowarray[$i])
                      {
                          print $rowarray[$i]." | ";
                      }
                      else
                      {
                          print " | "; # field returned could be UNDEFINED, just print separator
                      }
                  }
              }
          }
      }

      # CODE ENDS


      "4n6wiki-query.pl" Code Summary

      This script is very similar to our previous "ffparser.pl" script. It will use DBI to run SELECT queries against a user nominated SQLite Database. There will be 2 types of queries - a search by AppID and a search for LIKE terms in AppDescrip. We won't use an exact match for AppDescrip because the analyst might not know the exact description (eg they just know it's for CCleaner).
      As usual, it starts off with "GetOptions" and the Help printout sections.
      We then call DBI's "connect" and one of the versions of "prepare" (depending on the user's input arguments).
      Next we call "PrintHeadings" and "PrintResults" (we cut and pasted these functions from "ffparser.pl").
      Finally, we call "finish" and "disconnect".

      Testing

      To test our scripts we will save a copy of the current ForensicsWiki AppID page  as "List_of_Jump_List_IDs.html". We will then edit/remove the entry for the "Win32 cmd.exe" in the last row of the last table and re-save it as "List_of_Jump_List_IDs-withoutCMD.html".
      Now we can simulate an update by first running "4n6wiki-parser.pl" with "List_of_Jump_List_IDs-withoutCMD.html" and then again with "List_of_Jump_List_IDs.html". After our first run, our SQLite Database (created previously) should not contain our "Win32 cmd.exe" entry and then after the second run, it should contain our "Win32 cmd.exe" entry.

      OK, here we run "4n6wiki-parser.pl" with "List_of_Jump_List_IDs-withoutCMD.html". (ie no "Win32 cmd.exe" entry)

      sansforensics@SIFT-Workstation:~$ ./4n6wiki-parser.pl -file List_of_Jump_List_IDs-withoutCMD.html -db WinAppIDs.sqlite


      Running 4n6wiki-parser.pl v2012-03-27


      Extracted Table Size = 53 x 4
      Extracted Table Size = 31 x 4
      Extracted Table Size = 10 x 4
      Extracted Table Size = 87 x 4
      Extracted Table Size = 50 x 4
      Extracted Table Size = 16 x 4
      Extracted Table Size = 66 x 4
      Extracted Table Size = 30 x 4
      Extracted Table Size = 2 x 4
      Extracted Table Size = 55 x 4
      Extracted Table Size = 7 x 4
      Number of Rows Extracted from HTML = 407
      WinAppIDs.sqlite Update complete ...
      sansforensics@SIFT-Workstation:~$


      OK now we run a search for the "cmd" term in our "WinAppIDs.sqlite" database.

      sansforensics@SIFT-Workstation:~$ ./4n6wiki-query.pl -descrip "cmd" -db WinAppIDs.sqlite


      Running 4n6wiki-query.pl v2012-03-27


      AppID | AppDescrip
      6728dd69a3088f97 | Windows Command Processor - cmd.exe (64-bit)
      1 Matches returned
      sansforensics@SIFT-Workstation:~$

      As expected, it only returns the value for the 64bit cmd.exe.
      Now let's run "4n6wiki-parser.pl" with "List_of_Jump_List_IDs.html" (ie includes "Win32 cmd.exe" entry).

      sansforensics@SIFT-Workstation:~$ ./4n6wiki-parser.pl -file List_of_Jump_List_IDs.html -db WinAppIDs.sqlite


      Running 4n6wiki-parser.pl v2012-03-27


      Extracted Table Size = 53 x 4
      Extracted Table Size = 31 x 4
      Extracted Table Size = 10 x 4
      Extracted Table Size = 87 x 4
      Extracted Table Size = 50 x 4
      Extracted Table Size = 16 x 4
      Extracted Table Size = 66 x 4
      Extracted Table Size = 30 x 4
      Extracted Table Size = 2 x 4
      Extracted Table Size = 55 x 4
      Extracted Table Size = 8 x 4
      Number of Rows Extracted from HTML = 408
      DBD::SQLite::st execute failed: columns AppID, AppDescrip are not unique at ./4n6wiki-parser.pl line 114.
      DBD::SQLite::st execute failed: columns AppID, AppDescrip are not unique at ./4n6wiki-parser.pl line 114.
      DBD::SQLite::st execute failed: columns AppID, AppDescrip are not unique at ./4n6wiki-parser.pl line 114.
      [... Lines Edited Out]
      DBD::SQLite::st execute failed: columns AppID, AppDescrip are not unique at ./4n6wiki-parser.pl line 114.
      WinAppIDs.sqlite Update complete ...
      sansforensics@SIFT-Workstation:~$

      OK so we are getting some errors about duplicate entries but the last table seems to have an extra entry 8 x 4 (not 7 x 4). This looks promising!
      We'll run a query and check ...

      sansforensics@SIFT-Workstation:~$ ./4n6wiki-query.pl -descrip "cmd" -db WinAppIDs.sqlite


      Running 4n6wiki-query.pl v2012-03-27


      AppID | AppDescrip
      6728dd69a3088f97 | Windows Command Processor - cmd.exe (64-bit)
      bc0c37e84e063727 | Windows Command Processor - cmd.exe (32-bit)
      2 Matches returned
      sansforensics@SIFT-Workstation:~$


      YAY! The update worked!

      Let's run a quick query using the bc0c37e84e063727 AppId (for "Win32 cmd.exe") and see what results we get:

      sansforensics@SIFT-Workstation:~$ ./4n6wiki-query.pl -appid bc0c37e84e063727 -db WinAppIDs.sqlite

      Running 4n6wiki-query.pl v2012-03-27

      AppID | AppDescrip
      bc0c37e84e063727 | Windows Command Processor - cmd.exe (32-bit)
      1 Matches returned
      sansforensics@SIFT-Workstation:~$


      OK we found our latest entry.

      Just for fun, let's check how many entries we have via "sqlite3".

      sansforensics@SIFT-Workstation:~$ sqlite3 WinAppIDs.sqlite
      SQLite version 3.6.16
      Enter ".help" for instructions
      Enter SQL statements terminated with a ";"
      sqlite>
      sqlite> select count (AppId) from WinAppIDs;
      408
      sqlite>

      Now let's see if that corresponds with the SQLite Manager Firefox plugin.

      Opening our Database with the "SQLite Manager" Firefox Plugin

      Note(1): The HTML links have been extracted into the table Source column.
      Note(2): There are 408 records reported ie the same as what our previous "sqlite3" query reported. The latest record is our "Win32 cmd.exe" record.

      Our last check (with "sqlite3") is to see if  our database's DateAdded field is usable by SQLite (we'll only grab the first 3 records). So here's those records with our inputted date format:

      sqlite> select AppId, AppDescrip, DateAdded from WinAppIDs limit 3;
      65009083bfa6a094|(app launched via XPMode)|2011-08-22
      469e4a7982cea4d4|? (.job)|2011-08-22
      b0459de4674aab56|(.vmcx)|2011-08-22
      sqlite>

      And here's the same results with the date field transformed into "number of days since now":

      sqlite> select AppId, AppDescrip, julianday('now') - julianday(DateAdded) from WinAppIDs limit 3;
      65009083bfa6a094|(app launched via XPMode)|220.419525995385
      469e4a7982cea4d4|? (.job)|220.419525995385
      b0459de4674aab56|(.vmcx)|220.419525995385
      sqlite>

      So we can see from 22 August 2011 until (now) 29 March 2012 has been calculated as ~220 days.
      A manual check: 9 remaining days of August + 30 Sept days + 31 Oct days + 30 Nov days + 31 Dec days + 31 Jan days + 29 Feb days + 29 Mar days = 220
      Coolio! Our date field is usable by SQLite.

      Summary

      OK so everything seems to work. We have extracted various fields from the ForensicsWiki AppId web page and then entered that data into an SQLite Database. We have then successfully queried that database for some information.
      The code from "4n6wiki-query.pl" would be easy to cut and paste into existing scripts that currently only output the AppID thus possibly saving analysts some time.
      I'm not sure if this will be of use to any forensicators but it was interesting from a Perl programming perspective. If you think you might find it useful, please leave a comment.

      Viewing all 76 articles
      Browse latest View live