NSIS解包


学长让我把解包工具都下一遍,终于也是有用了。7z解包只能看到插件看不到nsi文件。

不过还是得自己手动跑一遍手解。

起码我知道了最后解包出来应该长什么样

一个7z直接调用接口解压,另一个纯文本去里面找。

不过看起来没那么简单,那就来看看吧。


前面固定套路就几句话带过好了,不记了,懒。

断点 LoadLibEx

静态查找函数入口

找到install作为dll被调用的唯一函数,调用完毕后直接freeLib,所以只看install就好了

结合之前解包出来的脚本查看。

使用的解压方法:RtlDecompressBuffer

关键解压函数:sub_69C2B110

用简单粗暴的方式得到了压缩buf在内存中的地址,回头再来分析这个地址是从哪里来的,this看得我头昏眼花。

0x00068922 内容正确。

5E B8 00 4D 5A 50 00 02 00 00 00 00 04 00 0F 00 FF FF 00 00 C6 B8 00 58 01 10 40 00 1A 04 48 18 18 00 01 00 00 BA 10 00 0E 1F 00 B4 09 CD 21 B8 01 4C CD 00 21 90 90 54 68

傻了,010都有内容查找,干嘛那么费劲。

解压


COMPRESSION_FORMAT_LZNT1

p

0x6000000

p1

0x2D30000 == 47382528

p2

call后结果:0x00FA9398 == 16,421,784

取文件偏移(错误走向)


以下内容是试错

快照是个好东西,回溯的时候没想到要回溯到dll刚加载的时候,层层引用,this不方便调试。

寻找 69C7F938 处的 值 何时被写入,写入的应当是一个常量表示结构体在程序中的偏移。

断点命中:69C07C4C

DLL基址:69C00000

直接拿了,内容是D8 E1 80 00

也不是一开始就保存的啊,难道我错过了写入?

难道是在DllMain里面。。。

69C7F938 78 14 7F 00

翻了一下最新的快照,发现这个值有所变动。

其实我再跑一个断点就变成这个值了,第一次的读不知道是什么情况。

障眼法,也可能是编译器自动。

断点命中:69C07AA4

DLL基址:0x69C00000

好大一坨,烦躁。

sub_69C09220 可能为初始化文件偏移位置函数。

再寻文件指针和偏移


嗯,往上翻了一下,好吧,错过了。

另外,下次再尝试新想到的方法好了。

回去最开始的地方再寻读入文件。

DLL基址:0x6a7c0000

6a7c55A0仍未命中。

6a7cC3C0,顺利命中,但先不分析

尝试断点createfileW和SetFilePointer,找到个有意思的调用

从文件末尾开始,寻找FFFFFFFFFFFFFFF8的位置?*(0019F4F4) == FFFFFFFF 这个有点坑人。

盲猜0x00000000 00000916,直接去下一个调用看看。

0xFFFFFFFF FFFFF6E2 ?这又是哪里来的?

抬头看了一眼,好家伙,这不是我没分析的那个6a7cC3C0吗

注释那个,写进buffer里面了,v5没啥用

然后就是,

0xFFFFFFFF FFFFF6E2 = - (0x8 + 0x916)

行了,这下清楚了。 010 goto <91E

6A7F258B产生一次Readfile命中,6A7F2624

再去Rtl那边碰碰运气,试试新方法。

奇怪,这次的压缩文件长度怎么变成了841E?

不管了。尝试内存断点到02D30000,发现内存未被创建。

跟踪发现是直接从readfile读进来的

找到目标。

sub_6A7EB110 嗯,又回来了,毫无收获。

回到sub_6A7F205A

ReadFile

pointer

pointer

pointer

read(调用者:sub_6A7EB200)(和B100区别)

pointer

read(02D3出现了)(这次是B100)

也就是说,只在sub_6A7F205A的地方read了一次结构体。

读进来了0x91E的数据,

一致的。打个内存断点试试,6A7F221F命中。

发现内存被移动到了:0019F408,或者准确的说是0019F3BC,有个v4作为偏移

关键的链表内存:0073FD80 (被访问了很多次,难以搜寻)

试试根据pointer查找:

找到两个,上面调用B200和B100的时候也说明是两个。

那么取之间的间距,盲猜结构体大小为0x70


文件读入到

00764018 0x1000空间

(void *)(this + 40) 是一个指向读入文件数据当前位置的 指针 ,DWORD形式,指针会移动。

*(_DWORD *)(this + 44) 是最末尾的指针。


6A7CAF90:外层解析

6A7CD0A1:内层解析

再寻6A7EB110

试试邪道:

已知buf长度是841E

0073FDB8 02C2F340 02C2F360 02D2FAF8 02D2FB20 02D2FB4C 02D2FB68 02D2FBE8 02D2FBFC

在读入json数据之前出现的841E不重要,但是此时已经读入了文件数据,可以看看这个东西在哪里出现。

嗯?和文件数据非常接近,那么就是说。。。

无话可说。找到了buf长度的获取来源,附近应该有文件指针。

但是这时候可以用断点看一下是谁在读这个841E

确认调用者:内层:6A7CDCE0

调用堆栈:

新的地址:0073FDB8。保存为快照《获取到读入buf size的call》

不如顺便看看buf头位置?

00333004 02C2F338——看起来是结构体内容,因为后面紧跟着841E

后面几个在栈上,不管。


函数:

6A7CC540:外层解析

6A7CAF90:内层解析

6A7CBA20:还有高手

数据:

00060504:FOA

0000041E:长度

02D30000:读入后的VA

搜02D3完全是把上面Readfile的部分忘光了。

00764808

00764878

结构体头部存在于 < (0x08 + DWORD最末尾的数据)

文件数据 偏移结构体头部:0x860 不可以如此计算,每个结构体不同,动态伸缩长度。

重大进展


看懂了

其中str以 FF FE FF 长度 为开头

Json以 长度 为开头

我们不需要这些数据其实可以直接跳过。

然后读入的内容存入在v6里面,远在天边近在眼前服了。

欲读入的数据保存在this(ecx指针)下偏移0x28的位置

的

IMPORTANT这个结构体里面有我们想要的数据,但是要拿到它需要经过层层解析,可以【考虑】直接从尾部读回来。

但是这样和原程序逻辑不符,慎重考虑。

时间回溯,看看important是怎么处理的数据。

先进行getWord获取循环次数为00 03

然后每次循环读入的结构体看起来包含4个字符串,最后的两个FFFEFF可能是结束标志?

我们需要的是第二个结构体(“第二”是如何判断的?暂不可知,还得往后看)

看看结构体赋值的解析:

和上面的内容对比,是6A7CBA20:还有高手

感觉距离完全解析已经很近了。

被我标记为FALSE的那个函数还是返回了FALSE,可能是安装和卸载的判断,然后我们这个是安装。可能如此。

也可能是协议版本号问题。感觉这个更合理。但是无论哪种我都不关心。

分析完成,来试试倒找思路是否可行。

最末尾八个字节指向文件位置,跳过

吃掉三个Dword,往前寻找。

吃掉三个Str,往前寻找。

省略推理步骤,总之这个样本能找

不过要小心碰到循环头,判断的方法是看看次数是否一致。

看看另一个样本能不能这样找。

可能-1: p = 0x073327 size = 0xF1F1FC 名字如下:

可能-2:p = 0x06AF09 size = 0x841E 名字如下:

可能-3:p=0x06AF09 size = 0x0000,名字不找了,看看p的位置:

好吧名字太好找了,还是找一下,注意名字前面的03,和我们找到的三个相符,因此倒找完成。

需要注意的是两个样本都有一个意料之外的末尾。也就是倒找找到的第一个。

我有点好奇这是不是释放的另一个文件(我没有注意过它是否释放了其他文件)

第一个样本SoftwareTool的这个意料之外的 p = 0x068922 size = 0xf50860

看到了熟悉的MZ头。

电子发票也一样。

获得了buf位置和buf长度,看看这两个地方在什么时候断点。不过既然是MZ,那估计也是释放了。

不如直接去看看会被释放到哪里。

断点未命中,估计根本不释放,直接内存中运行了,看到读入了一堆的dll。

我无话可说,但是不释放也好,这样我不用纠结它释放去哪里了

写脚本准备输出吧,给这个脚本画上句号。话说我什么时候开始解析它的来着?

嗯。。。至少也一个月了。毕竟有一条分支的快照是没保存下来的。

结束


等会,还有高手?


什么,还有高手?

少解了一个,但是文件名却能正常读取,可能是它少了某些字段的原因

看来倒找还是出事了。

但是倒找是真的香吧,试试正倒结合,反正文件名正常获取就说明它的字符串还在原本的位置。

从字符串开始正找就好了。

正找找到的内容和倒找完全一致。


收个尾

这六周真正做对的,是在 11-25 换掉了问题

不是那个倒找的技巧。

从 11-19 拿到解压函数开始,我一直在追"这个偏移值是什么时候被写进内存的"—— 追 69C7F938、追 DLL 基址、翻快照、怀疑 DllMain,最后只能归因成"障眼法,也可能是编译器自动"。那一段我自己在正文里标了错误走向,它确实是错的:我在追一个运行期的中间状态,而我要的是文件里的静态布局。

转折点就是断点从"内存写入"换成 CreateFileW + SetFilePointer。一旦改成问"它到底去文件的哪个位置、读了多少字节",0xFFFFFFFF FFFFF6E2 = -(0x8 + 0x916) 一算就出来了,010 goto <91E 立刻对上。

所以这条是可以搬走的:**分析卡住的时候,先检查是不是问题问错了,而不是继续往下追。**同一个目标,问"这个值哪来的"要穿过层层 this,问"它读了文件哪里"一步到位。

倒找为什么会漏,以及怎么补

倒找(跳末尾八个字节 → 吃三个 DWORD → 吃三个 Str → 往前找)之所以香,是因为这个格式把结构描述放在尾部,从后往前不需要知道总长度。

但它有个隐含前提:每个条目的字段数量一样。那个少解掉的文件就栽在这儿 —— 它少了某些字段,按固定节奏往前吃就错位了。而文件名还能正常读出来,恰好说明字符串还在原位,于是从字符串往回正找,就绕开了"字段数量固定"这个假设。

正倒结合不是折中,是让两个方向互相校验:倒找负责快,正找负责在结构不规则时兜住。两边结果完全一致,这个解析才算立住。

还有一条纪律值得留着:倒找时要看循环次数是否一致,来判断有没有撞到循环头,否则会一路吃进隔壁的结构里。

三条我改不掉的毛病

  • **快照没保存全。**有一条分支的快照丢了,那段只能重来。凡是"回溯可能要用"的现场,先存快照再动手。
  • 010 有内容查找,我却先花力气去内存里刨。「傻了,010都有内容查找,干嘛那么费劲」—— 工具能一步做到的事,先试工具。
  • **同一个坑踩了两次。**12-16 写下「搜 02D3 完全是把上面 Readfile 的部分忘光了」—— 前面已经确认 02D30000 是读入后的 VA,转头又去搜一遍。分析周期一长,自己的结论也会忘。

一句话

从 11-11 到 12-24,六周,其中大约一个月花在追一个问错了的问题。

代码与样本(签名加密容器):https://cnb.cool/t1mmoc/tspace/pwngore/h16nsis-unpack