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

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

因此可以采用工具尝试能不能直接脱壳,或者使用手动脱壳的办法进行脱壳。
因为这里没有进行更改,所以可以直接使用工具 https://github.com/upx/upx/releases/ 进行脱壳,然后发现文件的体积变大了。

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


分析
查看这里的 main 函数,发现提示很明确。这里首先通过 __isoc99_scanf 输入内容,利用 %22s 和后续 for 循环中的比较,也提示输入的 flag 长度是 22 字节。然后后面输入 s 和 key 经过了 RC4 的函数,然后把输入 s 和 data 进行比较。
直接搜索 RC4,查阅相关文章,可以得知它是一种流加密算法,它通过字节流的方式依次加密明文中的每一个字节,解密的时候也是依次对密文中的每一个字节进行解密。这里直接点击 RC4 函数,查看其内部是怎么实现的。
在下面可以看到 RC4 函数的实现,它实现调用 init_sbox 函数对于 a2,也就是上面从 main 函数传入的 key 进行一系列处理,然后获取 a1,也就是从 main 函数传入的输入 s,然后在 for 循环中,循环遍历 a1 的每一位,然后使用一个异或进行处理。

因此我们可以理清楚这个程序的逻辑,我们首先进行了输入,然后程序将我们的输入和内置的密钥及进行 RC4 加密,然后将加密的结果和内置的数据进行比较,如果一样的话,说明我们的输入是正确的。
因此分别点击 main 函数中的 key 和 data 变量,找到了内置的密钥和比对密文,之后就可以写脚本获取 flag 了。
点击 key 一直进行寻找发现了密钥 NewStar.

点击 data 发现这里没有值。

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

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

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

方法一
既然已经获取了密文和密钥,那么我们完全可以套用 RC4 的算法来解密。因为 RC4 是流密码,它的本质就是异或,而 a ^ b = c,c ^ b = a,由此可以直接把内置的密文作为输入,然后使用算法进行解密。
#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 也显示我们输入的数据。


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

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

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 进行修改。


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

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

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

方法三
上面可以知道 RC4 流密码的最后一步就是异或了,那么我们可以在动调的过程中,把这个异或的值拿出来,然后直接把密文和这个异或的数据进行异或即可。
这里先找到 RC4 的加密函数处,在这个最后一步的异或处按 Tab 转化为汇编窗格形式。

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


这里不知道两个参数 al 和 [rbp+var_9] 分别代表什么,因此我们可以先直接动调,断在这里的时候再去看看数值。这里动调需要注意,因为 RC4 根据输入的字符个数进行逐个加密的,所以我们需要输入和密文长度相等的字符长度,也就是 22 个字符才可以获得完整的异或值。
这里发现 al 存储的就是我们的输入数据(我这里输入 22 个字符 a,它的十六进制就是 0x61),然后由此可以知道 [rbp+var_9] 存储的就是需要异或的值。因为它经过了22次循环,所以每次异或的值都可能不一样,但是都只在一个函数中,rbp 的值应该不变,所以可以直接使用条件断点的方式,把 [rbp+var_9] 中的值取出来,或者也可以在下面的 mov [rdx], al 中下条件断点,把异或后的 al 值提取出来

对于在 [rbp+var_9] 下条件断点,首先寻找这个数据的地址。点击进去可以发现数据存储在栈上,当前的数据为 0xA2.
![[rbp+var_9] 的内容](/assets/upx_26.YgMC-cij.png)
然后回来,在断点处 右键 » Edit breakpoint,然后点击三个点就可调出 IDAPython 的窗口,然后我这里使用 Python 脚本,所以在下面选择语言为 Python.


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

import ida_bytes
# addr = 0x7FFE7DB58887 # 这里也是一样,需要自己动调找相应的地址
print(get_byte(addr), end=',')(可以忽略的一步)最后在下面 retn 中下断点,防止 for 循环结束后直接退出,这一步没有太大的必要,只是方便后续的数据查看。

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

然后直接把之前获得的密文和这个数据进行异或即可。
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 中下条件断点

import idc
al = idc.get_reg_value("rax")
print(al, end=',')然后得到了假数据经过异或后的数据,然后我们可以利用异或的特性获得 flag.
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 $? 来进行获取。

然后观察 status 就是循环的下标,因此可以知道它是单字节判断的,若是某个字节判断错了就直接退出,同时返回第几个字节判断错了。由此根据退出码,我们可以直接进行爆破处理。
这里就是进行爆破的代码,tqdm 只是为了直观显示,可以删去相关代码。
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}")然后爆破一分钟左右就跑出结果了。
