手动脱壳--elf64 的upx

手脱upx

upx壳是什么

一个基础的压缩壳,这里就以 elf的upx 加壳为示例来做.当然仅仅作为练手还是不错的.因为我的拖延,也花费了些许的时间.

upx 的压缩和解压

upx壳的压缩

分为四个步骤.

pack1

生成新的elf header.先生成能生成的一个elf header的框架

pack2

压缩原始PT_LOAD的内容,先用压缩算法预处理里面的内容,再把PT_LOAD压缩成UPX block,最后写入当前packed 数据区

pack3

追加upx的stub.用于后续解压缩

pack4

回填之前没写完的elf header 然后写入PackHeader / trailer
最后得到upx的文件的结构

1
2
3
4
5
6
7
ELF header  
Program headers
UPX 元数据
压缩数据
UPX stub
UPX 字符串
PackHeader / trailer

解压缩

大体上来说,就是 打开文件 .内存映射,修改全线,清除多于空间

这里有两个load,第一个是 需要解压的数据区.第二个是stub所在的区域

  1. 进入 stub
    之后先call进入图示位置
    Pasted-image-20260706181008
    这里打开文件自身

Pasted-image-20260706182834
这后面还有四个数据个数据
0x100:原始压缩块的偏移
0x0bef: 第二阶段解压后的长度
0x0801: 第二阶段压缩后长度
0x02 : 采用的解压算法
这些都是与 解压所stage2相关的
之后通过stage2继续解压原程序.之后可以就开始调用mmap 内存映射,之后再改这段内存的权限
之后jmp r13开始真正的还原整个程序具体流程就不说了
基本就是解压缩类似的流程,
mmap ,然后 mprotect设定全线,再清除之前多余的内存.

手动dump 脱壳

我们先找到oep,我是采用反复调试来找的. 一直f8 多调试几次.在发先,syscall 0b这个系统调用号的时候,应该是快到了,因为在清楚之前的冗余内存了.之后其实都是动态链接相关的内容.之后再到了
Pasted-image-20260706191724
这里就是oep,不过如果直接在这里dump肯定会出问题,因为oep这里,动态链接相关的内容已经被修改了.比如.got .init_array .data里面的函数指针,参考前面的动态链接的内容,.

dump

先看看gdb的dump脚本

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""GDB command: dump-elf-loads RUNTIME_EHDR OUTPUT."""

import os
import shlex
import struct

import gdb


PT_LOAD = 1


def read_memory(address, size):
inferior = gdb.selected_inferior()
return bytes(inferior.read_memory(address, size))


def rebuild(runtime_ehdr, output_path):
ehdr = read_memory(runtime_ehdr, 0x40)
if ehdr[:4] != b"\x7fELF" or ehdr[4:6] != b"\x02\x01":
raise gdb.GdbError("address is not a little-endian ELF64 header")

e_phoff = struct.unpack_from("<Q", ehdr, 0x20)[0]
e_phentsize = struct.unpack_from("<H", ehdr, 0x36)[0]
e_phnum = struct.unpack_from("<H", ehdr, 0x38)[0]
if e_phentsize < 0x38 or not 0 < e_phnum < 0x1000:
raise gdb.GdbError("invalid program header table")

phdrs = []
for index in range(e_phnum):
address = runtime_ehdr + e_phoff + index * e_phentsize
phdrs.append(struct.unpack("<IIQQQQQQ", read_memory(address, 0x38)))

header_loads = [
ph for ph in phdrs if ph[0] == PT_LOAD and ph[2] == 0
]
if not header_loads:
raise gdb.GdbError("no PT_LOAD contains the ELF header")

load_bias = runtime_ehdr - header_loads[0][3]
loads = [ph for ph in phdrs if ph[0] == PT_LOAD]
output_size = max(ph[2] + ph[5] for ph in loads)
output = bytearray(output_size)

for index, ph in enumerate(loads):
_, flags, file_offset, vaddr, _, filesz, memsz, align = ph
source = load_bias + vaddr
output[file_offset:file_offset + filesz] = read_memory(source, filesz)
gdb.write(
"LOAD[%d] memory=0x%x file=0x%x filesz=0x%x memsz=0x%x "
"flags=0x%x align=0x%x\n"
% (index, source, file_offset, filesz, memsz, flags, align)
)

# Section headers are not loaded by PT_LOAD and are optional at runtime.
struct.pack_into("<Q", output, 0x28, 0)
struct.pack_into("<H", output, 0x3A, 0)
struct.pack_into("<H", output, 0x3C, 0)
struct.pack_into("<H", output, 0x3E, 0)

with open(output_path, "wb") as stream:
stream.write(output)
os.chmod(output_path, 0o755)
gdb.write("Wrote 0x%x bytes to %s\n" % (len(output), output_path))


class DumpElfLoads(gdb.Command):
"""Rebuild an ELF64 file from PT_LOAD segments in inferior memory.

Usage: dump-elf-loads RUNTIME_EHDR OUTPUT
RUNTIME_EHDR is the address of the restored ELF header, not the UPX stub.
"""
def __init__(self):
super().__init__("dump-elf-loads", gdb.COMMAND_FILES)

def invoke(self, argument, from_tty):
args = shlex.split(argument)
if len(args) != 2:
raise gdb.GdbError(
"usage: dump-elf-loads RUNTIME_EHDR OUTPUT"
)
runtime_ehdr = int(gdb.parse_and_eval(args[0]))
rebuild(runtime_ehdr, os.path.abspath(args[1]))


DumpElfLoads()

大致流程其实就是把四个LOAD dump下来,然后先找到elf header和program header 并解析,之后再按照progam header的内容,分配并解析.ai 写的脚本,
先 gdb ./upx
先start
在之后我们只需要在我们在dump的 下一个硬件断点

1
hbreak *0x7ffff7fef15e

之后

1
source gdb_dump_elf.py 

之后运行

1
dump-elf-loads 

就行了.

修复

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
"""Convert an ELF64 PIE OEP memory dump back to file-time state."""

import argparse
import os
import struct
from pathlib import Path


PT_LOAD = 1
PT_DYNAMIC = 2

DT_NULL = 0
DT_DEBUG = 21
DT_RELA = 7
DT_RELASZ = 8
DT_RELAENT = 9
DT_JMPREL = 23
DT_PLTRELSZ = 2

R_X86_64_GLOB_DAT = 6
R_X86_64_JUMP_SLOT = 7
R_X86_64_RELATIVE = 8

# These d_un values are addresses. Numeric sizes, flags and string offsets are
# intentionally excluded.
DYNAMIC_POINTER_TAGS = {
3, # DT_PLTGOT
4, # DT_HASH
5, # DT_STRTAB
6, # DT_SYMTAB
7, # DT_RELA
12, # DT_INIT
13, # DT_FINI
17, # DT_REL
23, # DT_JMPREL
25, # DT_INIT_ARRAY
26, # DT_FINI_ARRAY
32, # DT_PREINIT_ARRAY
0x6FFFFEF5, # DT_GNU_HASH
0x6FFFFFF0, # DT_VERSYM
0x6FFFFFFC, # DT_VERDEF
0x6FFFFFFE, # DT_VERNEED
}


def parse_program_headers(data):
if data[:6] != b"\x7fELF\x02\x01":
raise ValueError("input is not a little-endian ELF64 file")

phoff = struct.unpack_from("<Q", data, 0x20)[0]
phentsize = struct.unpack_from("<H", data, 0x36)[0]
phnum = struct.unpack_from("<H", data, 0x38)[0]
if phentsize < 0x38 or not 0 < phnum < 0x1000:
raise ValueError("invalid program header table")

return [
struct.unpack_from("<IIQQQQQQ", data, phoff + i * phentsize)
for i in range(phnum)
]


def vaddr_to_offset(loads, vaddr, size=1):
for ph in loads:
file_offset, segment_vaddr, filesz = ph[2], ph[3], ph[5]
if segment_vaddr <= vaddr and vaddr + size <= segment_vaddr + filesz:
return file_offset + vaddr - segment_vaddr
raise ValueError("virtual address 0x%x is not file-backed" % vaddr)


def repair_dynamic(data, phdrs, old_base):
loads = [ph for ph in phdrs if ph[0] == PT_LOAD]
image_min = min(ph[3] for ph in loads)
image_max = max(ph[3] + ph[6] for ph in loads)
runtime_min = old_base + image_min
runtime_max = old_base + image_max

dynamic = next((ph for ph in phdrs if ph[0] == PT_DYNAMIC), None)
if dynamic is None:
raise ValueError("ELF has no PT_DYNAMIC")

dynamic_offset = vaddr_to_offset(loads, dynamic[3], dynamic[5])
values = {}
changes = []

for relative in range(0, dynamic[5], 16):
offset = dynamic_offset + relative
tag, value = struct.unpack_from("<QQ", data, offset)
if tag == DT_NULL:
break

repaired = value
reason = None
if tag == DT_DEBUG and value != 0:
repaired = 0
reason = "clear DT_DEBUG"
elif tag in DYNAMIC_POINTER_TAGS and runtime_min <= value < runtime_max:
repaired = value - old_base
reason = "remove old load bias from dynamic pointer"

if repaired != value:
struct.pack_into("<Q", data, offset + 8, repaired)
changes.append((offset + 8, value, repaired, reason))
values[tag] = repaired

return loads, values, changes


def iter_rela(data, loads, vaddr, size, entry_size):
if entry_size < 24 or size % entry_size:
raise ValueError("invalid ELF64 RELA table")
offset = vaddr_to_offset(loads, vaddr, size)
for relative in range(0, size, entry_size):
yield struct.unpack_from("<QQq", data, offset + relative)


def repair_relocation_targets(data, loads, dynamic):
rela_vaddr = dynamic.get(DT_RELA)
rela_size = dynamic.get(DT_RELASZ, 0)
rela_entry = dynamic.get(DT_RELAENT, 24)
if rela_vaddr is None:
raise ValueError("ELF has no DT_RELA")

relocations = list(iter_rela(data, loads, rela_vaddr, rela_size, rela_entry))

jmprel_vaddr = dynamic.get(DT_JMPREL)
jmprel_size = dynamic.get(DT_PLTRELSZ, 0)
if jmprel_vaddr is not None and jmprel_size:
relocations.extend(
iter_rela(data, loads, jmprel_vaddr, jmprel_size, rela_entry)
)

changes = []
for r_offset, r_info, r_addend in relocations:
relocation_type = r_info & 0xFFFFFFFF
try:
target = vaddr_to_offset(loads, r_offset, 8)
except ValueError:
continue

current = struct.unpack_from("<Q", data, target)[0]
if relocation_type == R_X86_64_RELATIVE:
repaired = r_addend & 0xFFFFFFFFFFFFFFFF
reason = "restore R_X86_64_RELATIVE addend"
elif relocation_type in (R_X86_64_GLOB_DAT, R_X86_64_JUMP_SLOT):
repaired = 0
reason = "clear resolved symbol slot"
else:
continue

if repaired != current:
struct.pack_into("<Q", data, target, repaired)
changes.append((target, current, repaired, reason))
return changes


def repair(input_path, output_path, old_base):
data = bytearray(Path(input_path).read_bytes())
phdrs = parse_program_headers(data)
loads, dynamic, dynamic_changes = repair_dynamic(data, phdrs, old_base)
relocation_changes = repair_relocation_targets(data, loads, dynamic)

Path(output_path).write_bytes(data)
os.chmod(output_path, 0o755)

for offset, old, new, reason in dynamic_changes + relocation_changes:
print("0x%04x: 0x%x -> 0x%x (%s)" % (offset, old, new, reason))
print(
"wrote %s: %d dynamic fixes, %d relocation-target fixes"
% (output_path, len(dynamic_changes), len(relocation_changes))
)


def main():
parser = argparse.ArgumentParser()
parser.add_argument("input")
parser.add_argument("output")
parser.add_argument(
"old_base",
type=lambda value: int(value, 0),
help="load bias of the process from which the OEP dump was taken",
)
args = parser.parse_args()
repair(args.input, args.output, args.old_base)


if __name__ == "__main__":
main()

这里具体脚本我就没看了.就是修复DYNAMIC这个段相关的内容.

如果想要不修,就在动态链接发生之前,也就是syscall 11这里发生之前dump

Pasted-image-20260706195819


手动脱壳--elf64 的upx
http://example.com/2026/07/06/upx/
Beitragsautor
fox
Veröffentlicht am
July 6, 2026
Urheberrechtshinweis