Paul C's Blog

To be funny,to grow up!

0%

九龙拉棺

1691764840274

本题难点

1.这道题要给线程下断点,

2.而且还要跳过一个IsDebuggerPresent的反调试,

3.在进入线程后,输入阶段也有一个跳转需要修改。

静态反汇编,

一路跟踪跳转关系,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int Main_func()
{
sub_41141A(); // 给函数指针赋值
return sub_412A90(); // 得到flag
}
//sub_412A90() line 36

//int sub_412D60() line 9
int Key_func()
{
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, 0, 0, 0);
hHandle = (HANDLE)sub_41127B();
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)sub_411316, 0, 0, 0);
}
//sub_411316 line3
//定位到关键函数
__int64 __stdcall sub_411B80(int a1)
{
int v1; // edx
__int64 v2; // ST08_8
char v4; // [esp+0h] [ebp-13Ch]
size_t Size; // [esp+D0h] [ebp-6Ch]
char Buf2; // [esp+DCh] [ebp-60h]
int v7_32; // [esp+108h] [ebp-34h]
char flag; // [esp+114h] [ebp-28h]
int savedregs; // [esp+13Ch] [ebp+0h]

sub_41137F((int)&unk_41D0F4);
WaitForSingleObject(hObject, 0xFFFFFFFF);
Check_Run_Error(&v4 == &v4);
Print_str((int)"please input your flag:", v4);
Input_str("%32s", (unsigned int)&flag);
v7_32 = j_strlen(&flag);
Enc_str(16, (int)&flag, v7_32, (int)&Buf2);
Size = 32;
if ( !j_memcmp(&unk_41B018, &Buf2, 32u) )
Print_str((int)"you win!", v4);
else
Print_str((int)"you lose!", v4);
SetEvent(hObject);
Check_Run_Error(&v4 == &v4);
HIDWORD(v2) = v1;
LODWORD(v2) = 1;
sub_411217((int)&savedregs, (int)&dword_411C7C);
return v2;
}

​ 猜测memcmp比较的:unk_41B018是程序的密文,Buf2存储根据输入生成的密文。

跟入前面的处理函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
unsigned __int64 __cdecl sub_411DA0(int a1_16, int flag, int a3_32, int cipher)
{
size_t v4; // eax
__int64 v5; // rax
char v6; // STEB_1
unsigned __int8 v7; // STDF_1
unsigned __int64 v8; // ST04_8
char v10[264]; // [esp+E8h] [ebp-16Ch]
int v11; // [esp+1F0h] [ebp-64h]
int v12; // [esp+1F4h] [ebp-60h]
int v13; // [esp+1F8h] [ebp-5Ch]
int v14; // [esp+1FCh] [ebp-58h]
int v15; // [esp+200h] [ebp-54h]
int v16; // [esp+20Ch] [ebp-48h]
int j; // [esp+218h] [ebp-3Ch]
int i; // [esp+224h] [ebp-30h]
unsigned int v19; // [esp+230h] [ebp-24h]
char v20; // [esp+23Ch] [ebp-18h]
char v21; // [esp+23Dh] [ebp-17h]
char v22; // [esp+23Eh] [ebp-16h]
char v23[21]; // [esp+23Fh] [ebp-15h]
int savedregs; // [esp+254h] [ebp+0h]

sub_41137F((int)&unk_41D0F4);
v11 = 1;
v12 = 0x55;
v13 = 0x1C39;
v14 = 0x95EED;
v15 = 0x31C84B1;
v19 = 0;
for ( i = 0; ; i += 4 )
{
v4 = j_strlen(Dest);
if ( v19 >= v4 )
break;
v16 = 0;
for ( j = 0; j < 5; ++j )
{
if ( Dest[j + v19] == 0x7A )
*(&v20 + j + i) = 0;
else
v16 += *(&v11 + 4 - j) * (Dest[j + v19] - 33);
}
v23[i] = v16;
*(&v22 + i) = BYTE1(v16);
*(&v21 + i) = BYTE2(v16);
*(&v20 + i) = HIBYTE(v16);
v19 += 5;
}
v5 = sub_411127((int)&v20, a1_16, (int)v10);
v19 = 0;
i = 0;
for ( j = 0; j < a3_32; ++j )
{
v19 = (signed int)(v19 + 1) % 256;
i = (i + (unsigned __int8)v10[v19]) % 256;
v6 = v10[v19];
v10[v19] = v10[i];
v10[i] = v6;
v7 = v10[((unsigned __int8)v10[i] + (unsigned __int8)v10[v19]) % 256];
HIDWORD(v5) = v7;
*(_BYTE *)(j + cipher) = v7 ^ *(_BYTE *)(j + flag);// cipher[j]=v7^flag[j]
}
v8 = __PAIR__(HIDWORD(v5), j);
Check_Stack((int)&savedregs, (int)&dword_411FF8);
return v8;
}

可以看到前面根据一系列操作,生成了v7,然后用v7和flag异或得到了正确的cipher。

所以动态调试,

1.先搜索字符串,下好断点,一个是输入位置,一个是比较位置。

1691327074344

考点1

多线程下好断点。

1691326897356

考点2

2.一开始直接jmp F12E10

1691325095601

3.进入到F12A90。

F9运行,断到了线程函数执行前。

1691327641950

按几下F9,成功断到了关键函数处。

1691328283702

此时的线程状态:

1691328244877

考点3

跟入F11037,在F12860的跳转处,强制让其不跳转。

1691329006039

断在输入位置0x411BDE位置,

1691329286256

先随便输入32个1,根据对比函数里的栈地址,找到密文unk_41B018,

1691329776133

1
2
00F1B018  DE 1C 22 27 1D AE AD 65 AD EF 6E 41 4C 34 75 F1  ?"'enAL4u?
00F1B028 16 50 50 D4 48 69 6D 93 36 1C 86 3B BB D0 4C 91 PP訦im??恍L?

整理如下:

1
DE1C22271DAEAD65ADEF6E414C3475F1165050D448696D93361C863BBBD04C91

再次运行程序,先断到长度函数处。

1691330092818

修改这里的输入值,将前面的密文输入进去。

1691330196880

再断到加密函数位置处:

1691330953276

经过异或处理后,此时memcmp的Buf2里, 就是flag值。

flag

1691330848182

1691330936363

Run跟踪

  • 跟踪一个call运行了哪些代码,有递归所有call和只对一层call的方法,操作方式分别是CTRL+F11和CTRL+F12。

给线程下断的另一种方式

1691331179709

假设程序都是静态链接的,先从整体上把握程序的装载过程,下一章将把程序拆成模块来观察。

Linux下的分段故障Segmentation fault与Windows的“进程因非法操作需要关闭,很多时候是因为进程访问了未经允许的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**Linux操作系统
* |----------------------------------|
* | 操作系统空间 | 1G
* |----------------------------------| 0xC0000000
* | |
* | |
* | 用户进程空间 | 3G
* | |
* | |
* |__________________________________| 0x00000000
* WinXP默认操作系统占据2G内存,可以调整使其只占1G内存。
*/

PAE

(Physical Address Extension),是针对32位CPU内存不足的一种修补。类似的,针对16位CPU,借助段偏移寻址能够达到1MB,每次读块只能读64KB,使用XMS(一种中断处理技术)读取大于1MB内存的地方。

OS采用36位地址线\内存地址时,程序使用的最大虚拟地址空间仍旧不会超过4GB,但是它程序的虚拟地址空间可以映射到的物理内存的范围扩大到64GB。

通过这种页或块映射在Win下访存的操作方式叫地址窗口映射扩展AWE(Address Windowing Extension);在Linu下通过mmap()系统调用来实现。

装载的方式

  • overlay覆盖装入,适合内存受限场景比如木马或者嵌入式设备;

保证调用路径上的块都在内存;禁止跨树间调用。

编写程序时将程序分块,写一小段辅助代码,管理模块在内存的驻留和更替。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* |----------------------------------------------------|
* |----------------------------------------------------|
* | Overlay Manager (几十Bytes) |
* |----------------------------------------------------|
* | Main (1024 bytes) |
* | |
* |----------------------------------------------------|
* | A 模块 (512 bytes) | B 模块 (256 bytes) |
* | |----------------------- |
* |----------------------------------------------------|
*/

如上图所示,A和B模块互不依赖,都被Main模块调用时,则可以采用Overlay的方式,使得需要的内存空间节省256Bytes。

  • Paging页映射 和页替换算法 MMU的地址映射

从操作系统可执行文件的装载

进程的建立

  • 1.创建虚拟地址空间(只创建映射函数需要的数据结构,并不实际创建空间)
    • Linux i386中是分配一个页目录,不设置页映射关系,在后面程序发生页错误的时候才设置)
  • 2.读取可执行文件头,建立虚拟空间与可执行文件的映射关系。(装载)

(Linux中)

  • 3.令EIP=EOP of Executable File

    ​ 内核堆栈和用户堆栈的切换、CPU运行权限切换

1
2
3
/**
页面可执行文件的偏移<----(装载)---->虚拟存储空间<----------->物理内存
*/

Linux将进程虚拟空间中的一个段叫VMA(Virtual Mempry Area),包括

1
start address;size;Attributes(RWE);State;Type;

对于相同权限状态的节Section,把它们合并到一起作为一个段Segment映射。这样在进程虚拟空间中只有一个VMA而不是多个,可以减少页面内部碎片,节省内存空间。

页错误

程序执行时进程虚拟空间中发生Page Fault,控制权从进程转移到OS;

根据数据结构找到VMA,计算页面在磁盘文件中的偏移,

然后物理内存分配物理页,读取该磁盘块到物理页中,

将发生页错误的虚拟页与物理页之间建立映射关系。

控制权归还给进程,从发生页错误的位置继续执行。

ELF文件

从链接角度(Linking View)看,elf文件按照节section存储,描述它的结构叫做节表Section Headers;

从装载角度或者执行视图(Execution View)看,elf文件可以按照段segment划分,描述它的结构叫做程序头Program Header;

elf可执行文件和共享库文件处于装载的需要,比目标文件多一个程序头表(Program Header Table)。

1
2
3
4
5
6
7
8
9
10
11
12

// 程序头表
typedef struct {
Elf32_Word p_type; // 段的类型,如LOAD:1.DYNAMIC,INTERP
Elf32_Addr p_offset; // 段在文件中的偏移量
Elf32_Addr p_vaddr; // 段在内存中的虚拟地址
Elf32_Addr p_paddr; // 段在物理内存中的地址(对于嵌入式系统可能有用)
Elf32_Word p_filesz; // 段在文件中的大小,可能是0
Elf32_Word p_memsz; // 段在进程虚拟地址空间中占用的大小,可能是0
Elf32_Word p_flags; // 段的属性标志,如可读、可写、可执行等
Elf32_Word p_align; // 字节按照2^p_align次方对齐
} Elf32_Phdr;

对于LOAD类型的Segment,p_memsz一定>=p_filesz(bss被合并在数据类型的段里,存放那些未被初始化的数据)。

PE文件的装载

步骤

PE文件可以装载到任何内存位置。PE文件中选用RVA,因为RVA可以始终保持一致。

1.读取文件第一个页,获取到DOS头、PE头、段表。

2.选择装载地址。检查进程空间地址里,目标地址是否可用。若不可用,则另外选择装载地址。

3.段映射。使用段表将PE文件中的段,映射到进程内存空间地址。—>若装载地址≠目标地址,Rebasing。

4.装载所需要的dll文件—>解析PE文件中的导入符号——>

5.建立初始化栈和堆—>建立主线程并启动进程

数据结构

1
2
3
4
5
6
7
8
9
10
coffHeader{
optionalHeader{
...
u32 addressofEntryPoint;//装载后PE文件第一个指令的RVA,病毒感染PE文件后要修改入口点,篡改执行流程。
u32 SectionAlignment;//内存中段对齐粒度,默认是4K=4096字节
u32 FileAlignMent;//文件中段对齐粒度,默认是512字节
u32 baseofCode;
u32 baseofData;//数据段起始RVA
}
}

6.4 进程虚存空间分布

https://www.kanxue.com/chm.htm?id=13855&pid=node1000002

指令集

  • MIPS 长度固定的指令,是RISC,处理器通过硬连线实现

  • x86(IA-32),长度变化,用CISC的指令集,CISC的处理器需要用微指令配合运行。但是当下Intel实际处理器的结构都已经变成了risc结构了,risc的结构实现流水线等特性比较容易。

cisc的寄存器数量较少,指令能够实现一些比较特殊的功能,例如Win32汇编语言对应的8086的一些寄存器: AX-DX,CS、DS、ES、SS。

GNU编译的elf文件对应到RISC的寄存器,会有很多通用寄存器。

虚拟模式需要使用一些特殊的寄存器、为了支持分页需要使用页表寄存器等,为了加速内存的访问需要使用TLB,加速数据和指令的访问而使用data cache和instruction cache等

jmp $是为了让程序停在这一行,防止程序跑飞(跑飞的程序危害很大!有可能把数据当代码或者把代码当数据!)

仿真工具

用synplify综合的电路,然后用debussy+modelsim仿真

IA-32的指令格式

1690965899478

前缀部分可选,分为四个group。

操作数前缀用于设置 锁总线和重复前缀 、段重写和分支预测、操作数宽度、地址宽度。

Opcode

  1. x86的opcode最短是1个字节,最长是3个字节。

  2. x86对于源操作数和目的操作数是暗含在opcode里面的。

1
2
88 11 BL(011) CL(001)     88 D9 MOV CL, BL               MR   
8A 11 011 001 8A D9 MOV BL(011),CL(001) R<--M
  1. 二字节通用opcode是0fh+一字节的编码;但是二字节的SIMD opcode是三字节长度,即一个强制前缀+0fh+一字节的操作码。

  2. 同样的,三字节的通用opcode,是0fh+二字节的编码。SIMD opcode格式是强制前缀+0FH+二字节编码。

    5.个别的SIMD指令不需要强制前缀来引导,比如addps(0FH+58H)

addps指令中,ps代表”packed single-precision floating-point” 。

XMM (eXtended MultiMedia ) 寄存器是SSE(Streaming SIMD Extensions)指令集中的一种寄存器。

它可以同时存储4个单精度浮点数,每个数占用32位,总共128位。

对应的,MM寄存器是64位的。

addps指令将分别对应位置上的单精度浮点数相加,并将结果存储回目标XMM寄存器中。

1
0F 58 01  addps xmm0,xmmword ptr[ecx] //从内存位置ecx处取128位单精度浮点数值加到xmm0 寄存器中的128位单精度浮点数上

Mod R/M

切分为3个位域,233.

mod:提供寻址模式,11=寄存器寻址 其余都是内存寻址
reg/opcode:
两种作用,第一种是提供寄存器寻址;另一种为某些opcode提供补充说明。
R/M:
结合MOD位域,提供内存/寄存器寻址。

R/M=100& MOD≠11 作为SIB的转义码 。

例如,

1
2
3
4
编码mov eax(000),ebx(011)
89H 11 011 000 89 D8 MR
8BH 11 000 011 8B C3 RM
//x86把源和目的操作数隐藏在opcode里面

1690969519285

SIB

同modr/m类似,SIB字节也是采用233切分成三个位域,名字分别叫Scale、Index、Base。SIB的名字也来自这三个位域名字的首字母缩写。
SIB字节由 R/M=100& MOD≠11 引导出来。
1.SIB确定的寻址方式是[base+Index* Scale +disp],

esp作为index时候,index自动被忽略,即此时scale因子视为0 , 寻址计算方法是[base+disp]

2.disp意思是后面尾随的若干个displacement字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
mov eax,[0x1]可以编码为以下形式:
a)采用moffs32编码
opcode moffs32
A1 01 00 00 00
A1 40H
b)采用MODR/M引导的disp
opcode mod reg r/m displacement
8B 00 000 101 01 00 00 00H
8B 05H 01 00 00 00H
c)采用SIB引导的disp
opcode mod reg r/m scale index base displacement
8B 00 000 100 01 100 101 01 00 00 00H
8B 04H 65H 01 00 00 00H

1.数据不平衡问题

2.数据饥饿问题

3.静态检测时混淆加密手段的干扰

4.运行崩溃的解决

5.样本中特征不足导致矩阵全0

面向 APT 家族分析的攻击路径预测方法研究,信息学报,陈伟翔 1, 任怡彤 1, 肖岩军 2, 侯 锐 3, 田志宏 1

IDS需要关联 APT 生命周期内的数 个阶段, 以此提高检测攻击能力。

1.要解决的问题

基因库—可靠数据获取方法

HMM——预测的攻击路径的可解释性

2.概念

攻击行为概述

“寻找入口点”、“C2 通信”、“权限提升”、“资 产发现”、“数据过滤”、

APT 攻击路径的可见状态集

注册表&服务

CS(create-service): 创建服务

MRV(modify-registry-value): 修 改注册表的行为;

DRKV(delete-registry-key-value): 删除注册表关键信息的行为;

内核

CKO(create-kernelobject): 创建内核对象的行为;

MMP(modify-memory-property): 修 改内存权限的行为;

进程

CP(create-process): 创建进程;

LL(load-library): 加载 库 ;

ESI(execshellcode-instr): 执行 shellcode 指令;

EHI(exec-heapinstr): 执行堆指令;

WTPM(write-to-process-memory): 写入进程内存;

ERI(exec-ret-instr): 执行 ret 指令;

EEI(exec-esp-instr): 执行 esp 指令;

CT(create-thread): 创建线程;

网络

SDQ(send-dnsquery): 发送 DNS 请求行为;

SNP(send-network-packet): 发送网络数据包;

LOP(listen-on-port): 监听端口。

CTS(connect-to-socket): 连接 socket;

文件

CF(create-process): 创 建文件的行为;

WTF(write-to-file): 写入文件的行为;

动态分析工具

Anubis、Norman、Joebox

基因库

基因(序列):(MD5,env,object_1,object_2,action_name)按照时间排序

object_1 与 object_2 分别代表软件执行动作的对象 和路径

同一个 APT 家族下恶意软件基因集合, 称为一个 APT 家族的基 因库。

钻石模型

一个 APT 攻击包含“攻击者”、 “受害者”、 “能力”和“基础设施”4 个核心元 素。

“能力”是攻击者使用的工具或技术, 从 探查到最终目的达成, “技术”存在于每一个攻击阶 段, 每一个恶意动作对应的技术细节可体现出相应 战略意图。

“基础设施”是攻击者维持权限控制的通 道或者载体。

3.方法

Smith waterman算法+

相似的公共子序列一定是得分最高的那一条。

比对序列为:A=GGTTGACTA,B=TGTTACGG

则两序列的长度分别为len(A) = n,Len(B)=m;
s(a,b):字符a和字符b的相似分数,这里设置为3,不相等则为-3;

W1 = 2 :一个空位罚分,这里设置为2(可根据需要设置)

H:匹配分数矩阵如下。

1690796434417

  1. 初始化算法分数矩阵H。A是列向量,B是行向量。使行i表示字符ai,列j表示字符bj;

1690796255740

  1. 回溯,从矩阵H中分数最大的一项开始:
    若ai=bj,则回溯到左上角单元格
    若ai≠bj,回溯到左上角、上边、左边中值最大的单元格,若有相同最大值的单元格,优先级按照左上角、上边、左边的顺序
  2. 根据回溯路径,写出匹配字符串:
    若回溯到左上角单元格,将ai添加到匹配字串A‘,将bj添加到匹配字串B’;
    若回溯往上走,此时A走了,B没走。将ai添加到匹配字串A’,将_添加到匹配字串B’;
    若回溯到左边单元格,B走了,A没走。将_添加到匹配字串A’,将bj添加到匹配字串B’。
  3. 得到局部最优匹配序列,结束

数据处理

1.APT基因库要去重、去无效、去冗余(Smith-waterman局部序列对比算法去除相似基因,阈值为80%)。

->节省存储、简化算法复杂度

基因个体不重要,基因行为才重要。:(MD5,env,object_1,object_2,action_name),去除md5,env作关键词替换,object1只保留可执行文件。

2.公共基因:对所有APT基因库中的基因进行相似度检测,相似度90%以上的基因序列作为公共基因。

恶意软件的通用动作。

3.恶意行为基因库:各个家族APT基因库-公共基因,之后合并各个家族基因库。

4.可观测状态集:恶意软件行为可观测链:恶意软件基因序列,与恶意行为基因库进行比较。保留相似度 Thres_hold 在 90%以上的基因序列的基因尾的动作名称,不包含基因段属性信息,从而保证可观测状态可计算属性。

缺陷1:

恶意行为基因库进 行可观测链提取, 而这些基因序列来自于软件行为。

只能使用基因中 包含 C2 通信的部分进行关联和提取, 基因库的使用 率不足 30%,

5.隐藏状态集 资产发现 数据过滤 寻找入口 C2 通信

将测试集 中的样本放入各自的 APT 家族的 HMM 中计算下一 时刻攻击路径的隐藏状态和可观测状态的概率

展示结果时

将各个家族样本按照编号排序,把得分取对数,构成下面的得分图。

1690793540423

4.优势

1.精准定位到具体基因


能精准定位到具体基因, 能在一定程度上应对 恶意软件变种, 能在一定程度上应对 恶意软件变种

2.可同时对隐藏状态和可观测状态展开预测

3.在进行最终结果判定时, 将多个观测值作为判定结果

只要结果在观测值组里,就正确,可以提高实验表现,也能提供参考。

劣势

隐藏状态只有四个;

在 数据质量方面, 若一条观测链在多个模型下均展现 出极少或是单一的 APT 阶段, 就会严重影响 HMM 的构建。

在多个路径时,基因检测的识别准确率不如 HMM 检测法, 但是

xctf的一道Go语言逆向题,涉及AES加密和base64换表加密。

用IDA打开g0Re报告SP-Analysis failed错误。

1690613919182

静态检测

将文件拷贝到kali后,静态检测一番。

1
2
3
4
$ file g0Re               
g0Re: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header

readelf -hlS --w g0Re

报告如下错误:

1
2
3
4
5
6
readelf: Warning: Section 49 has an out of range sh_link value of 2302008908
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] <no-strings> 00010102: <unknown> 00000001003e0002 481cb0 000040 400003 0 0 15762873573703680
readelf: Warning: [ 1]: Link field (533963) should index a symtab section.
readelf: Warning: [ 3]: Unexpected value (1482181455) in info field.

strings找到一些特征字符串

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿kali)-[~/idawork]
└─$ strings -a -tx g0Re|grep -E '\s{4,30}'
ec OKXX$
13e FN{o
1e6 r mL
284 dRe6eYPgXygMd
295 fSCPpMP/C9DU36D2kliiYS5D9wKG/E_p
2b9 XkJb3WwcGMbUPd63r/bG8gVDS6EsZ5vv
81e18 $Info: This file is packed with the
81e67 $Id:

猜测是UPX加壳,但是UPX标志被抹去了。

UPX! 标志被抹除

unpack时会依次在三个地方检查UPX_MAGIC_LE32(即”UPX!”):

1
2
3
1、在倒数第36字节偏移处检查,如果特征值不符就会转入throwNotPacked()异常抛出函数,打印not packed by UPX;
2、".ELF"魔数前36字节处,如果这里的特征值不符就会转入throwCantUnpack()异常抛出函数,打印l_info corrupted;
3、 在倒数第46字节偏移处检查,如果特征值不符就会转入throwCompressedDataViolation()异常抛出函数,打印Exception: compressed data violation;

1690711701628

结合上面的规则2,可以很清晰地判断出出题人用0KXX代替了UPX!

1690712364661

在对应的三个位置处换上UPX!魔数。

1690713002432

1690712555711

upx脱壳。

1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿kali)-[~/idawork]
└─$ upx -d g0Re_upx
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2020
UPX 3.96 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020

File size Ratio Format Name
-------------------- ------ ----------- -----------
1331200 <- 534092 40.12% linux/amd64 g0Re_upx

Unpacked 1 file.

输入flag后的处理流程

1
_int64 __usercall sub_48CD80@<rax>(__int64 i%16@<rdi>, __int64 key[i%16]@<rsi>, __int64 a3@<r14>, __int128 a4@<xmm15>)

1.aes 2.base64 3.简单运算

1690723199822

1690723104727

aes加密 密钥获取

1
动态调试获取密钥:wvgitbygwbk2b46d

启动ida服务端。

1
2
3
4
5
6
#端口,密码,详细模式
#若希望同时维护几个调试实例,就要在不同端口启动调试服务器。
./linux_server64 -p23946 -p23946 -v
IDA Linux 64-bit remote debug server(ST) v1.22. Hex-Rays (c) 2004-2017
Listening on 0.0.0.0:23946...

1690716983450

Hex窗口与RSI同步。

1690718805904

base64换表加密

1
2
strings -tx g0Re_upx|grep -E '[A-Z]{10,64}' 
12c040 456789}#IJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123ABCDEFGH

解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#-*-encoding:utf-8
import base64
import string
string1 = "456789}#IJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123ABCDEFGH"
string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
#ascii字符范围
def filter_printable_chars(flag):
filtered_flag = ''
for byte in flag:
if byte in string1:
filtered_flag+=byte
#else:
#print("ERROR")
#break
return filtered_flag

enc=[ 0xE6, 0xCE, 0x89, 0xC8, 0xCF, 0xC5, 0xF5, 0xC9, 0xD2, 0xD9, 0xC0, 0x91, 0xCE, 0x7F, 0xAC, 0xCC, 0xE9, 0xCF, 0xB7, 0xC0, 0x96, 0xD4, 0xEA, 0x92, 0xE2, 0xD7, 0xDF, 0x84, 0xCB, 0xA5, 0xAE, 0x93, 0xA6, 0xCA, 0xBE, 0x97, 0xDF, 0xCE, 0xF0, 0xC9, 0xB7, 0xE1, 0xAE, 0x6B, 0xC4, 0xB1, 0x65, 0xDB, 0xCE, 0xED, 0x92, 0x93, 0xD6, 0x8C, 0xED, 0xC3, 0xA3, 0xDA, 0x94, 0xA5, 0xAA, 0xB2, 0xB5, 0xA7, 0x55]
key=b"wvgitbygwbk2b46d"
base = ""
for i in range(len(enc)):
base+=chr(((enc[i]-key[i%16])^0x1a) &0xff)
print(base)#uB8EAyfxAmOEvQlrhCJM8hk1qonHskb55NM4qvmxZeY#xg5mMm10x0nF6b3iRdeYÄ

#过滤base64编码表里的字符
str1=filter_printable_chars(base)
result=base64.b64decode(str1.translate(str.maketrans(string1,string2)))

#AES解密
from Crypto.Cipher import AES#
aes = AES.new(key,mode=AES.MODE_ECB)
print(aes.decrypt(result)) #b'flag{g0_1s_th3_b3st_1anguage_1n_the_wOrld!_xxx}\x01'

反汇编算法

  • 1.线性扫描反汇编 关键是确定代码起始位置,之后线性扫描整个代码段,并逐条反汇编每条指令。不会识别分支来解释控制流。
    • 优点:覆盖程序的所有代码段;
    • 缺点:假设代码段中全是代码,没法处理代码段中混入的数据。
  • 2.递归下降反汇编。根据指令间的引用关系决定是否反汇编,在一个代码块内部,还是使用线性扫描算法。
    • 优点:大部分情况下可以区分代码和数据。
    • 缺点:无法处理间接代码路径。

IDA pro是递归下降反汇编器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//add、xor、mov、栈操作push
case 顺序流指令:
线性扫描反汇编
//jnz、ja
case 条件分支指令:
反汇编两个路径,直接反汇编下一条指令对应的分支,将跳转目标指令的地址加入延迟反汇编地址列表。
//jmp
case 无条件分支指令:
确定跳转目标,反汇编目的地址;对于无条件分支后的字节不作处理。

//call
case 函数调用指令:
运行方式类似于无条件跳转,但是其后的返回地址会被直接反汇编,将跳转目标指令的地址加入延迟反汇编地址列表。
//ret,
case 返回指令:
获取接下来将要执行的指令信息;有时需要从栈顶获取,而静态反汇编器不具备访问栈的能力。此时反汇编器会开始处理延迟反汇编地址列表。

反汇编出问题的情形

1.对于无条件分支指令,例如jmp eax这样的运行时才能确定跳转地址的指令,需要人工赋值。

2.call指令,在函数内部篡改了函数返回地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
foo proc near
inc dword ptr[esp];
retn
endp
//错误的反汇编
E8 F7 FF FF FF call foo
05 89 45 F8 90 add eax,90F84589h

//正确的反汇编
E8 F7 FF FF FF call foo
05 db 5
89 45 F8 mov [ebp-8],eax
90 nop

3.ret没有提供返回地址。

1690714484074

IDA遇到的函数返回语句,检测到其栈指针值不为0。

反汇编的困难

1.编译,和自然语言的翻译一样是一个多对多操作。除了C编译器,还有Go、Python、Delphi编译器、WinAPI库,反编译器非常依赖语言和库。

2.编译过程中会丢失命名和类型信息。反汇编后最多知道变量的位数,类型信息需要通过变量的用途确定。

一些静态工具用法

  • Linux:ldd; nm展示符号,C++filter、展示重定义。

  • OSX:otool; 处理MACH-O。

  • Windows:VS里的dumpbin /dependents ;objdump

ldd显示依赖库

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿kali)-[~/idawork]
└─$ ldd linux_server64
linux-vdso.so.1 (0x00007fff323bf000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fbfef767000)
libthread_db.so.1 => /lib/x86_64-linux-gnu/libthread_db.so.1 (0x00007fbfef75c000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fbfef751000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fbfef730000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fbfef515000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fbfef3d2000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbfef1f6000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbfef782000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fbfef1d6000)

dumpbin /dependents

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Dump of file baby.exe

File Type: EXECUTABLE IMAGE

Image has the following dependencies:

KERNEL32.dll
msvcrt.dll
msvcrt.dll

Summary

1000 .CRT C runtime Lib
1000 .bss
1000 .data
1000 .debug_abbrev
1000 .debug_aranges
1000 .debug_frame
2000 .debug_info
1000 .debug_line
1000 .eh_frame
1000 .idata
1000 .rdata
3000 .text
1000 .tls

strings

-t 显示字符的文件偏移

-a 使得strings扫描整个文件,而非只有文件中可加载的、经初始化的部分

-e 搜素其他字符编码,如Unicode

几乎不会用到的工具:x86流式反汇编器,ndisasm和diStorm。

x86汇编语法

  • AT&T:GNU工具(Gas:GNU汇编器、gcc、gdb)
    • 前缀:%reg,$立即数
    • 操作数排序:mov 源操作数,目的操作数 add $0x4,%eax
  • Intel :MASM(微软汇编器)、Turbo汇编器、NASM
    • add eax,0x4

.model flat,平坦模式,代码段和数据段共用一个4G段。

局部变量、

局部变量是堆栈变量。作用域是单个子程序,从子程序返回到主程序时被释放。一种规范写法是

1
LOCAL @buffer[10]:BYTE ;LOCAL 变量名1[重复数量]:类型,变量名2[重复数量]:类型

LOCAL用于为堆栈变量预留空间。

局部变量:只能在过程中用,主程序里不能用;

堆栈变量:没有初始值,只在调用时分配。C语言中的函数值传递的本质。

一些常用类型整理

1
2
byte db,word dw,DWORD dd,fword df,QWORD dq,tbyte dt  1,2,4,6,8,10
WNDCLASS
  • TYPE返回变量大小
  • LENGTHOF 返回变量元素个数
1
2
3
4
5
6
7
;,用于表示一个变量是否结束
var6 dw 10,20,30,40,50
dw 60,70,80,90,99
var7 dw 10,20,30,40,50,
60,70,80,90,99
LENGTHOF var6 ;5
LENGTHOF var7 ;10
  • SIZE,SIZEOF返回所占字节数=LENGTHOF*TYPR

1.=和EQU伪指令

由等号或者EQU定义的符号常量不占用存储空间。

EQU类似于等号伪指令,但是EQU伪指令不许重复定义,而等号伪指令可以(同一个常量名可重复定义多次)。

1
2
3
presskey EQU <"Output is:">
.data
propt db presskey

2.$ 当前地址运算符

3.LEA伪指令

OFFSET和ADDR编译时起作用,LEA是指令,在运行时起作用。

1
2
MOV ESI,ADDR BVAL  ;相当于
LEA ESi,BVAL

计算堆栈(程序执行是分配)变量的偏移地址时,只能用lea。

4.PTR 操作符

按照指定类型在内存中读写值。

1
2
LIST DB 12H,34H,56H,78H;
MOV EAX,DWORD PTR LIST ;EAX 78563412H

5.ALIGN和EVEN伪指令

CPU处理偶数地址比处理奇数地址要快。 可以在一个内存访问周期内获取该数据

ALIGN 对齐 (1,2,4) 按照n个字节的边界值对齐

EVEN 使下一地址从偶数地址开始

6.TYPEDEF和TYPEDEF PTR操作符

类型自命名和定义指针的类型,不占用存储空间

7.LABEL伪指令

别名,不占用存储空间

8.基数控制伪指令RAIDX

1
2
3
4
.RADIX  16;默认该指令后面的数据都是16进制数,其他进制的数据需要额外说明

MOV AX,0FF ;十六进制数,对应十进制255
MOV BX ,12D ;十进制数12

9.ORG伪指令

10.REPT伪指令

11.ASSUME伪指令

将程序的段与逻辑段绑定。

12.SHORT伪指令

循环

1
2
3
4
5
6
7
8
9
10
11
12
.while(条件)
.endw

.repeat

until(条件)

.if
.elseif
.endif
.break .if ;退出循环
.continue ;结束本次循环,进入下一次循环

14.结构体和共用体

15.宏定义

16.过程

模块化代码

PROC [NEAR] 或者[FAR],默认近调用,即在ODS实模式下的一个段里(<64KB(。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
主程序中 CALL {PROC_noarg_name}

PUSH 参数2
PUSH 参数1
INVOKE {PROC_noarg_name}


{PROC_noarg_name} PROC [NEAR]
LOCAL 变量名1[重复数量]:类型,变量名2[重复数量]:类型
RET ;从栈中弹出返回地址,返回到主程序
{PROC_noarg_name} ENDP

{PROC_name} PROC,参数1:类型,参数2:类型,...


17.ENTER和LEAVE

18.RET和RETN

19.IDA的反汇编

0day 安全

文件偏移地址 = 虚拟内存地址(VA)−装载基址(Image Base)−节偏移
= RVA -节偏移

对于 栈桢可能发生移位的情况

解决办法:

一个函数返回时,esp正好指向原来存储返回地址的下一位,我们将shellcode从ret_addr的后一个位置开始填充,并将ret_addr填充为一个进程中的”jmp esp”的指令的地址,这样函数返回后就会跳到esp指向的栈顶的位置开始执行shellcode。

缓冲区组成方式,现阶段已经讲了两种:

  1. 将shellcode放到缓冲区,然后覆盖返回地址到缓冲区的起始地址。这种适用于缓冲区较大的场合。
  2. 将shellcode放到函数返回地址以后,然后覆盖返回地址为”jmp esp”之类的指令,使得函数返回时跳转到shellcode处执行指令。这种适用于缓冲区较小的场合。

  3. 找到程序运行的线程环境块TEB。

  4. TEB的起始地址偏移0x30的地方指向进程环境块PEB。
  5. PEB的地址偏移0x0C的地方存放指向PEB_LDR_DATA结构体的指针,该指针指向一个存放着被进程装载的动态链接库的信息的结构体。
  6. PEB_LDR_DATA结构体偏移位置位0x1C的地方指向模块初始化链表的头指针InInitializationOrderModuleList。
  7. 4中的链表存放PE被载入时初始化的模块信息,第一个链表节点时ntdll.dll,第二个位kernel32.dll。
  8. kernel32.dll的节点偏移0x08是kernel32.dll在内存中载入的基址。
  9. kernel32.dll的基址加0xe3C是PE头的地址。
  10. PE头偏移0x78存放着指向函数导出表的指针。
  11. 安照下述方法寻址:
    • 导出表偏移0x1C的指针指向存储导出函数偏移地址(RVA)的列表。
    • 导出表偏移0x20指针指向存储导出函数名的列表。
    • 根据函数名找到我们要的函数是导出表中的第几个,然后再地址列表中找到对应RVA。
    • RVA加上动态链接库的基址即是VA,这个也是我们在shellcode中需要的地址。

这里shellcode的构造为了尽可能的短,所以需要给每个API名字用一个hash去代替。
MessageBoxA:0x1e380a6a
ExitProcess:0x4fd18963
LoadLibraryA:0x0c917432

1
2
3
4
5
push 0x1e380a6a
push 0x4fd18963
push 0x0c917432
mov esi,esp
lea edi,[esi-0xC]

如何实现一款 shellcodeLoader

https://cloud.tencent.com/developer/article/1755926

本文记录那些恶意软件使用的关键API。

NtProtectVirtualMemory ,创建PAGE_GUARD属性的内存页,通常用于反逆向和反调试 ; 将可读可写的内存属性改为可读可执行 。