Hack.lu 2012 CTF Challenge #3 (450)

3 – Zombies like PPTP

Our intel shows us that the Zombies use a MS-PPTP like protocol and luckily we could intercept a challenge-response transmission of one of the Zombie outposts. The important thing for Zombies in this war is mass! Not only brain mass but their mass. So they built their PPTP protocol compatible to all older Zombie soldiers. Luckily our science team could extract the algorithm of the challenge-response system out of a captured Zombie brain … I spare you the details, let’s just say it was not a pretty sight. And here comes your part soldier: we need the password of this intercepted transmission. With this password we were finally able to turn this war to our favor. So move your ass soldier and good luck!


Summary: bruteforce LM response

There are three files here – pptp.py, pyDes.py, transmission.pcap.

Let’s start with pcap. It’s really simple, there’s only one session here:

Looks simple. Now pptp.py: it contains 4 methods: lm_hash, response_lm, newTechnologie_hash, response_newTechnologie.

response_lm uses lm_hash so we should consider only response_ methods. We can think about conversation as

200 41787c9f6ffde56919ca3cd8d8944590a9fff68468e2bcb6
incompatible # seems nt is not supported
200 78165eccbf53cdb11085e8e5e3626ba9bdefd5e9de62ce91

Now we have both responses. response_lm method uses DES as a hash function:

DES(key=password.upper(), msg="Trololol") for lm_hash and
DES(key=lm_hash, msg=challenge) for response_lm

Here’s the scheme:

We can bruteforce both parts of the password separately:

  • first 8-byte part can be checked against LM Response 1
  • second 6-byte part can be checked agains LM Response 2
  • and LM Response 3

We can bruteforce first the second part of the key, because it’s faster and we can obtain some information about the first part (possible charset for example). This way we don’t know LM hash 1 so we’ll bruteforce its two last bytes for each valid LM Response 3 and check against LM Response 2 (there will be collisions because LM response 3 depends only on two bytes of the lm_hash2).

Recall that password is uppercased first and that DES’s low bits are meaningless, we can narrow the charset. So:


#include <openssl/des.h>
#include "brute.h"
char input[] = "Trololol";
char chall[] = "\xde\xad\x23\x4a\x1f\x13\xbe\xef";
char lm_resp1[] = "\x78\x16\x5e\xcc\xbf\x53\xcd\xb1";
char lm_resp2[] = "\x10\x85\xe8\xe5\xe3\x62\x6b\xa9";
char lm_resp3[] = "\xbd\xef\xd5\xe9\xde\x62\xce\x91";
void checker_second(char *pw, int len) {
    DES_key_schedule k;    
    char buf_hash2[16];
    char buf_resp2[8];
    char buf_resp3[8];
    DES_set_key_unchecked(pw, &k);
    DES_ecb_encrypt(input, buf_hash2, &k, 1);
    bzero(&buf_hash2[8], 6);
    DES_set_key_unchecked(&buf_hash2[6], &k);
    DES_ecb_encrypt(chall, buf_resp3, &k, 1);
    if (memcmp(buf_resp3, lm_resp3, 8))
    char key_for_resp2[8];
    memcpy(&key_for_resp2[2], buf_hash2, 6);
    unsigned int low;
    for (low = 0; low < 65535; low++) {
        *(unsigned short*)key_for_resp2 = low;
        DES_set_key_unchecked((void*)key_for_resp2, &k);
        DES_ecb_encrypt(chall, buf_resp2, &k, 1);
        if (!memcmp(buf_resp2, lm_resp2, 8)) {
            printf("6-byte part: %s\n", pw);
int main(int argc, char *argv) {
    bruteforce_set_key("", 6);
    bruteforce_set_charset("ABDFHJLNPRTVXZ02468_-", 20);
    bruteforce(6, checker_second);

Launch it and get in 1 minute:

6-byte part: DNRDLR

Seems charset is A-Z (maybe digits too but we’ll try A-Z first) for first block:

void checker_first(char *pw, int len) {
    DES_key_schedule k;
    char buf_hash1[8];
    char buf_resp1[8];
    DES_set_key_unchecked(pw, &k);
    DES_ecb_encrypt(input, buf_hash1, &k, 1);
    DES_set_key_unchecked(buf_hash1, &k);
    DES_ecb_encrypt(chall, buf_resp1, &k, 1);
    if (!memcmp(buf_resp1, lm_resp1, 8)) {
        printf("8-byte part: %s\n", pw);
int main(int argc, char *argv) {
    bruteforce_set_key("", 8);
    bruteforce_set_charset("ABDFHJLNPRTVXZ", 14);
    bruteforce(6, checker_first);

In ~6 minutes (30 minutes with one thread):

8-byte part: ZNLBHDRA

Now we have passwords but we’ve lost two bits in each byte: case bit and the lowest bit. Let’s brute force them agains response_newTechnologie, which is similar to response_lm but uses sha1 as first hash function:

DES_key_schedule k;
char nt_resp1[] = "\x41\x78\x7c\x9f\x6f\xfd\xe5\x69";
char pw[] = "ZNLBHDRADNRDLR";
char res[40];
char resp1[8];
unsigned int bits, i, acc;
for (acc = 0; acc < 0x10000000; acc++) { // 2**28
    bits = acc;
    for (i = 0; i < 14; i++) {
        pw[i] = pw[i] & (0xff ^ 0x20 ^ 1);
        pw[i] |= bits & 1;
        pw[i] |= (bits & 2) << 4;
        bits >>= 2;
    SHA1(pw, 14, res);
    DES_set_key_unchecked(res, &k);
    DES_ecb_encrypt(chall, resp1, &k, 1);
    if (!memcmp(resp1, nt_resp1, 8)) {
        printf("PWNED: %s\n", pw);
$ time ./a.out 
real	1m0.745s
user	1m0.636s
sys	0m0.000s

The flag: ZomBIEsAdOReMS


Skip to comment form

    • Ben Stock on October 26, 2012 at 14:34
    • Reply

    Isn’t the ordering wrong? The first response is NT, the second is LM – also according to your code shown here.

    1. oh, really! Thanks :)

    • Ok Seon Kyo on October 27, 2012 at 15:37
    • Reply

    I’m so sorry but would you mind posting also #1 Zombie Talk? I was all in with my friend to that question but we couldn’t solve it. If you don’t mind, please send the solution to my e-mail uyw4687@naver.com Thank you!!

    1. Just convert to base36 all the numbers (you can use http://www.efunda.com/units/base_n.cfm?base_from=16 for example)

Leave a Reply

Your email address will not be published.