Skip to content

UPX

脱壳

使用 IDA 查看文件,发现主函数很复杂。

很复杂的主函数

这时根据文件的名称和提示,和 UPX 相关联。直接搜索 UPX,可以得知它是可执行程序文件压缩器,是一种压缩壳。在程序启动时先执行 UPX 的代码,把压缩后的原文件解压后,再把控制流转到原文件。然后这里可以使用 DIE 进行查看,特征也显示为 UPX.

DIE 的查看结果

因此可以采用工具尝试能不能直接脱壳,或者使用手动脱壳的办法进行脱壳。

因为这里没有进行更改,所以可以直接使用工具 https://github.com/upx/upx/releases/ 进行脱壳,然后发现文件的体积变大了。

UPX 命令行程序脱壳的结果

再次使用 IDA 打开脱壳后的程序,发现主函数逻辑很清晰,反汇编的结果也很明确了。

脱壳后的 IDA View脱壳后的反编译结果

分析

查看这里的 main 函数,发现提示很明确。这里首先通过 __isoc99_scanf 输入内容,利用 %22s 和后续 for 循环中的比较,也提示输入的 flag 长度是 22 字节。然后后面输入 skey 经过了 RC4 的函数,然后把输入 sdata 进行比较。

直接搜索 RC4,查阅相关文章,可以得知它是一种流加密算法,它通过字节流的方式依次加密明文中的每一个字节,解密的时候也是依次对密文中的每一个字节进行解密。这里直接点击 RC4 函数,查看其内部是怎么实现的。

在下面可以看到 RC4 函数的实现,它实现调用 init_sbox 函数对于 a2,也就是上面从 main 函数传入的 key 进行一系列处理,然后获取 a1,也就是从 main 函数传入的输入 s,然后在 for 循环中,循环遍历 a1 的每一位,然后使用一个异或进行处理。

RC4 函数的实现

因此我们可以理清楚这个程序的逻辑,我们首先进行了输入,然后程序将我们的输入和内置的密钥及进行 RC4 加密,然后将加密的结果和内置的数据进行比较,如果一样的话,说明我们的输入是正确的。

因此分别点击 main 函数中的 keydata 变量,找到了内置的密钥和比对密文,之后就可以写脚本获取 flag 了。

点击 key 一直进行寻找发现了密钥 NewStar.

key 变量

点击 data 发现这里没有值。

没有值的 data 变量

然后对 datax 进行交叉引用,发现存在另外的函数也用到了 data 这个数据。

data 的交叉引用

直接点击这个使用 data 数据的函数 before_main 中,可以发现这里对 data 进行了赋值操作。

before_main 函数里的赋值操作

这里再次交叉引用 before_main 函数,可以发现它在 .init_array 段被调用。这个段里存放着的是在 main 函数执行前执行的代码,可以搜索相关资料进一步掌握。这里只需要知道它是在 main 函数之前被调用的,那么它就是最后进行比对的密文,我们直接拿出来就行。

.init_array 段

方法一

既然已经获取了密文和密钥,那么我们完全可以套用 RC4 的算法来解密。因为 RC4 是流密码,它的本质就是异或,而 a ^ b = cc ^ b = a,由此可以直接把内置的密文作为输入,然后使用算法进行解密。

C
#include <stdio.h>
#include <string.h>

unsigned char sbox[256] = {0};
const unsigned char* key = (const unsigned char*)"NewStar";
unsigned char data[22] = {-60, 96,  -81, -71, -29, -1,  46,  -101, -11,  16,  86,
                          81,  110, -18, 95,  125, 125, 110, 43,   -100, 117, -75};

void swap(unsigned char* a, unsigned char* b) {
    unsigned char tmp = *a;
    *a = *b;
    *b = tmp;
}

void init_sbox(const unsigned char key[]) {
    for (unsigned int i = 0; i < 256; i++) sbox[i] = i;
    unsigned int keyLen = strlen((const char*)key);
    unsigned char Ttable[256] = {0};
    for (int i = 0; i < 256; i++) Ttable[i] = key[i % keyLen];
    for (int j = 0, i = 0; i < 256; i++) {
        j = (j + sbox[i] + Ttable[i]) % 256;
        swap(&sbox[i], &sbox[j]);
    }
}

void RC4(unsigned char* data, unsigned int dataLen, const unsigned char key[]) {
    unsigned char k, i = 0, j = 0, t;
    init_sbox(key);
    for (unsigned int h = 0; h < dataLen; h++) {
        i = (i + 1) % 256;
        j = (j + sbox[i]) % 256;
        swap(&sbox[i], &sbox[j]);
        t = (sbox[i] + sbox[j]) % 256;
        k = sbox[t];
        data[h] ^= k;
    }
}

int main(void) {
    unsigned int dataLen = sizeof(data) / sizeof(data[0]);
    RC4(data, dataLen, key);
    for (unsigned int i = 0; i < dataLen; i++) {
        printf("%c", data[i]);
    }
    return 0;
}

方法二

上面是写代码进行解密,还可以直接在动调中把输入替换为密文,也可以进行解密。

首先在输入后和比对时按 F2 下断点。

下断点

然后进行动调,首先随便输入数据,然后程序在断点处停下(这里颜色改变了,说明断在这里了),输入 s 也显示我们输入的数据。

我们的输入

IDA 中看到的 s 内的值

然后先点击 data,找到 data 数据。然后使用 sheift + e 直接取出 data 的相关数据。

data 数据

然后可以先找到输入 s 的起始地址(0x56201B813040 需要你自己动调时的地址),然后使用 shift + F2 调出 IDAPython脚本窗口,然后使用 python 脚本进行修改,最后点击 Run 进行执行。然后可以发现左侧的输入 s 数据改变了。

修改 s 的数据

python
from ida_bytes import *
# addr = 0x56201B813040  # 这里需要填写自己动调时得到的地址
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,
       0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,
       0x75, 0xB5]
for i in range(22):
    patch_byte(addr + i, enc[i])
print('Done')

或者可以手动 右键 » Patching » Change byte 进行修改。

手动修改 s 的数据 1

手动修改 s 的数据 2

修改完之后,使用快捷键 F9 让程序直接运行,然后它会断在我们之前下的第二个断点处。

断在第二个断点处

这时再看我们的输入 s,会发现它经过 RC4 的再次加密(其实是解密,因为 RC4 是流密码,加密解密的流程一摸一样),呈现出来了 flag.

解密后的 flag

这里按 a 就可以转化为字符串的形式,这个就是最后的 flag 了。

flag

方法三

上面可以知道 RC4 流密码的最后一步就是异或了,那么我们可以在动调的过程中,把这个异或的值拿出来,然后直接把密文和这个异或的数据进行异或即可。

这里先找到 RC4 的加密函数处,在这个最后一步的异或处按 Tab 转化为汇编窗格形式。

RC4 的加密函数

然后在这个汇编的异或处下断点。下面展示了两种汇编代码的视图方式,可以按空格进行相互转换。

汇编的视图方式 1

汇编的视图方式 2

这里不知道两个参数 al[rbp+var_9] 分别代表什么,因此我们可以先直接动调,断在这里的时候再去看看数值。这里动调需要注意,因为 RC4 根据输入的字符个数进行逐个加密的,所以我们需要输入和密文长度相等的字符长度,也就是 22 个字符才可以获得完整的异或值。

这里发现 al 存储的就是我们的输入数据(我这里输入 22 个字符 a,它的十六进制就是 0x61),然后由此可以知道 [rbp+var_9] 存储的就是需要异或的值。因为它经过了22次循环,所以每次异或的值都可能不一样,但是都只在一个函数中,rbp 的值应该不变,所以可以直接使用条件断点的方式,把 [rbp+var_9] 中的值取出来,或者也可以在下面的 mov [rdx], al 中下条件断点,把异或后的 al 值提取出来

动调查看 al

对于在 [rbp+var_9] 下条件断点,首先寻找这个数据的地址。点击进去可以发现数据存储在栈上,当前的数据为 0xA2.

[rbp+var_9] 的内容

然后回来,在断点处 右键 » Edit breakpoint,然后点击三个点就可调出 IDAPython 的窗口,然后我这里使用 Python 脚本,所以在下面选择语言为 Python.

调出 IDAPython 窗口 1

调出 IDAPython 窗口 2

然后在 Script 中写入 IDAPython 脚本,点击左右两边的 OK 即可。

IDAPython 脚本

python
import ida_bytes
# addr = 0x7FFE7DB58887 # 这里也是一样,需要自己动调找相应的地址
print(get_byte(addr), end=',')

(可以忽略的一步)最后在下面 retn 中下断点,防止 for 循环结束后直接退出,这一步没有太大的必要,只是方便后续的数据查看。

retn 下断点

然后最为关键的就是在下面 Output 栏中打印的数据了,可以看到打印出了每次异或的值,但是因为断点的时候第一数据已经运行到了,所以没有第一个数据,但是我们之前查看 [rbp+var_9] 的时候观察到了,所以自己把这个 0xA2 给加上即可。

异或的值

然后直接把之前获得的密文和这个数据进行异或即可。

python
xor_data = [0xa2, 12, 206, 222, 152, 187, 65, 196, 140,
            127, 35, 14, 5, 128, 48, 10, 34, 59, 123, 196, 74, 200]
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,
       0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,
       0x75, 0xB5]
for i in range(len(enc)):
    enc[i] ^= xor_data[i]
print(''.join(chr(e) for e in enc))

这里我们还可以在下一条语句 mov [rdx], al 中下条件断点

下条件断点

python
import idc
al = idc.get_reg_value("rax")
print(al, end=',')

然后得到了假数据经过异或后的数据,然后我们可以利用异或的特性获得 flag.

假数据

python
input_data = 'aaaaaaaaaaaaaaaaaaaaaa'
after_xor = [195, 109, 175, 191, 249, 218, 32, 165, 237, 30,
             66, 111, 100, 225, 81, 107, 67, 90, 26, 165, 43, 169]
enc = [0xC4, 0x60, 0xAF, 0xB9, 0xE3, 0xFF, 0x2E, 0x9B, 0xF5, 0x10,
       0x56, 0x51, 0x6E, 0xEE, 0x5F, 0x7D, 0x7D, 0x6E, 0x2B, 0x9C,
       0x75, 0xB5]
for i in range(len(enc)):
    enc[i] ^= after_xor[i] ^ ord(input_data[i])
print(''.join(chr(e) for e in enc))

方法四

这里观察主函数,发现存在 exit 函数,它的功能就是关闭所有文件,终止正在执行的进程,中间的status 参数就是退出码,可以通过 echo $? 来进行获取。

exit 函数相关代码

然后观察 status 就是循环的下标,因此可以知道它是单字节判断的,若是某个字节判断错了就直接退出,同时返回第几个字节判断错了。由此根据退出码,我们可以直接进行爆破处理。

这里就是进行爆破的代码,tqdm 只是为了直观显示,可以删去相关代码。

python
from pwn import *
from tqdm import tqdm
context(arch='amd64', os='linux', log_level="error")

str = string.printable
flag = b"a"*22
tmp = 0

for i in tqdm(range(len(flag))):
    for j in str.encode():
        p = process("./upx")
        new_flag = flag[:i] + chr(j).encode() + flag[i+1:]
        p.sendafter(b"input your flag:\n", new_flag)
        p.wait()
        exit_code = p.poll()
        p.close()
        # 判断退出码是否变化,最后 flag 正确是退出码为 0(return 0),所以需要另外处理
        if exit_code > tmp or (exit_code == 0 and tmp != 0):
            flag = new_flag
            tmp = exit_code
            break
print(f"[*] Successfully get the flag : {flag}")

然后爆破一分钟左右就跑出结果了。

爆破结果