被离职面试的时候怎么回答,IDA Python 使用总结_idapython,带你一步一步深入Handler源码

class Block:
def __init__(self, start_ea, end_ea, imm, reg, call_target):
self.start_ea = start_ea
self.end_ea = end_ea
self.imm = imm
self.reg = reg
self.call_target = call_target

def get_block(start_ea):
global imm, reg, call_target
mnem_list = [‘pushf’, ‘pusha’, ‘mov’, ‘call’, ‘pop’]
ea = start_ea
for i in range(5):
mnem = idc.print_insn_mnem(ea)
assert mnem == mnem_list[i]
if mnem == ‘mov’:
imm = idc.get_operand_value(ea, 1)
reg = idc.print_operand(ea, 0)
elif mnem == ‘call’:
call_target = idc.get_operand_value(ea, 0)
ea += idc.get_item_size(ea)
return Block(start_ea, ea, imm, reg, call_target)

在提取出代码块之后利用提取到的有效信息可以在 call_target 中查找代码块对应的实际代码。这里有几个特殊情况:

  • 一般情况在 cmp 判断找到对应位置后会依次执行 jnz,popa,popf 三条指令,然后后面紧跟着代码块对应的实际代码。然而想下面这种情况,在执行完 popf 后面紧跟着 pusha 而不是代码块对应的实际代码,简单分析一下发现这种情况代码块对应的实际代码为 retn 。这种情况需要返回 True 表示一个 branch 的结束。

.text:000045CC popa
.text:000045CD popf
.text:000045CE pushf
.text:000045CF pusha
.text:000045D0 call dec_index
.text:000045D0
.text:000045D5 popa
.text:000045D6 popf
.text:000045D7 retn

  • 通常认为代码块对应的实际代码的结束标志为一个 jmp 指令,但是有的地方在 jmp 之后还会执行几条有效指令,因此判断实际代码的结束标志应当是 pushf 。

def get_real_code(block, new_code_ea):
ea = block.call_target
while True:
if idc.print_insn_mnem(ea) == ‘cmp’:
reg = idc.print_operand(ea, 0)
imm = idc.get_operand_value(ea, 1)
if reg == block.reg and imm == block.imm:
ea += idc.get_item_size(ea)
break
ea += idc.get_item_size(ea)

在 cmp 判断找到对应位置后会依次执行 jnz,popa,popf 三条指令

assert idc.print_insn_mnem(ea) == ‘jnz’
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == ‘popa’
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == ‘popf’
ea += idc.get_item_size(ea)

if idc.print_insn_mnem(ea) == ‘pushf’: # 第一种特殊情况,实际是 ret 指令。
return True, asm(‘ret’)

new_code = b’’
while True:
if idc.print_insn_mnem(ea) == ‘jmp’: # 第二种特殊情况,跳转过去可能还会有几条实际功能指令。
jmp_ea = idc.get_operand_value(ea, 0)
if idc.print_insn_mnem(jmp_ea) == ‘pushf’:
break
ea = jmp_ea
else:
code = mov_code(ea, new_code_ea)
new_code += code
new_code_ea += len(code)
ea += get_item_size(ea)
return False, new_code

这里涉及到了维护重定位的并查集 RelocDSU ,对应代码如下。在 get 函数中如果遇到了 jmp 指令且操作数是立即数就路径压缩到跳转的地址,直到地址在 .got.plt 或者指令不是 jmp 指令。另外判断是否是已处理代码是根据地址对应的最终地址是否不在 .text 段。

class RelocDSU:

def __init__(self):
self.reloc = {}

def get(self, ea):
if ea not in self.reloc:
if idc.print_insn_mnem(ea) == ‘jmp’ and idc.get_operand_type(ea, 0) != idc.o_reg:
jmp_ea = idc.get_operand_value(ea, 0)

if idc.get_segm_name(jmp_ea) == ‘.got.plt’:
self.reloc[ea] = ea
return self.reloc[ea], False

self.reloc[ea], need_handle = self.get(idc.get_operand_value(ea, 0))
return self.reloc[ea], need_handle
else:
self.reloc[ea] = ea
if self.reloc[ea] != ea: self.reloc[ea] = self.get(self.reloc[ea])[0]
return self.reloc[ea], idc.get_segm_name(self.reloc[ea]) == ‘.text’

def merge(self, ea, reloc_ea):
self.reloc[self.get(ea)[0]] = self.get(reloc_ea)[0]

reloc = RelocDSU()

接下来就是考虑如何提取出一个 branch 的代码了。前面提到过程序中会在代码块直接插入一些有实际功能的代码,因此需要借助 try:...except:...assert 来处理。除此之外这里还有几个特殊情况:

  • 程序中的 0x900 和 0x435c 处分别有一个获取返回地址 eip 到 ebx 和 eax 的函数,程序借助这两个函数来访问全局变量实现地址无关代码,然而重定位后代码地址改变,因此这里需要将其修正为 mov reg, xxx
  • 需要根据程序中的 jmp 指令来决定下一步需要去混淆的代码位置,这里需要判断 jmp 后面跟的是否是立即数,另外需要判断 jmp 到的代码是否是已经处理过的代码。
  • 并查集合并的时候如果是代码块,需要将代码块的地址合并到代码块对应指令的实际重定位后的地址;如果不是代码块如果是 jmp 指令且操作数是立即数,需要将 jmp 指令和该指令的重定位后的实际地址合并到指令的原本地址,然后将指令的原本地址合并到指令的跳转地址,否则将该指令的地址合并到重定位后的地址。

def handle_one_branch(branch_address, new_code_ea):
new_code = b’’
ea = branch_address
while True:
try:
block = get_block(ea)
is_ret, real_code = get_real_code(block, new_code_ea)
reloc.merge(ea, new_code_ea)
ea = block.end_ea
new_code_ea += len(real_code)
new_code += real_code
if is_ret: break
except:
get_eip_func = {0x900: ‘ebx’, 0x435c: ‘eax’}
if idc.print_insn_mnem(ea) == ‘call’ and get_operand_value(ea, 0) in get_eip_func:
reloc.merge(ea, new_code_ea)
real_code = asm(‘mov %s, 0x%x’ % (get_eip_func[get_operand_value(ea, 0)], ea + 5), new_code_ea)
else:
if idc.print_insn_mnem(ea) == ‘jmp’ and idc.get_operand_type(ea, 0) != idc.o_reg:
reloc.merge(new_code_ea, ea)
else:
reloc.merge(ea, new_code_ea)
real_code = mov_code(ea, new_code_ea)

new_code += real_code
if real_code == asm(‘ret’): break
new_code_ea += len(real_code)
if idc.print_insn_mnem(ea) == ‘jmp’ and idc.get_operand_type(ea, 0) != idc.o_reg: # jmp reg is a swtich
jmp_ea = idc.get_operand_value(ea, 0)
if reloc.get(jmp_ea)[1] == False: break # 跳回之前的代码说明是个循环
ea = reloc.get(jmp_ea)[0]
else:
ea += get_item_size(ea)
return new_code

能够处理 branch 后,我们就可以 bfs 依次处理所有的 function 和 branch 了,这里还有几个特殊情况:

  • 0x4148 地址处的函数中有一个 switch ,由于是通过跳转表跳转,去混淆脚本分析不到跳转的分支,因此需要读取跳转表找到跳转的 branch 然后添加到 branch_queue 中。
  • 寻找新的 branch 时需要判断 jcc 的操作数类型是否是立即数。

func_queue = Queue()
func_queue.put(entry_point)

while not func_queue.empty():
func_address = func_queue.get()
if reloc.get(func_address)[1] == False: continue
reloc.merge(func_address, new_code_ea)
branch_queue = Queue()
branch_queue.put(func_address)
if func_address == 0x4148: # 特判 0x4148 地址处的函数,读取跳转表。
assert new_code_ea == 0x963d0
for eax in range(0x20):
jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
new_jmp_target, need_handle = reloc.get(jmp_target)
if need_handle: branch_queue.put(jmp_target)

while not branch_queue.empty():
branch_address = branch_queue.get()
new_code = handle_one_branch(branch_address, new_code_ea)
ida_bytes.patch_bytes(new_code_ea, new_code)

当前 branch 去完混淆之后需要遍历代码找到 call 和 jmp 指令从而找到其他的 function 和 branch 。

ea = new_code_ea
while ea < new_code_ea + len(new_code):
idc.create_insn(ea)
if idc.print_insn_mnem(ea) == ‘call’:
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle: func_queue.put(call_target)
elif idc.print_insn_mnem(ea)[0] == ‘j’ and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle == True:
branch_queue.put(jcc_target)
ea += get_item_size(ea)
new_code_ea += len(new_code)

在完成代码去混淆之后需要对代码进行重定位,重定位的时候需要注意 jmp 指令长度的变化。

ea = new_code_start
while ea < new_code_ea:
idc.create_insn(ea)
mnem = idc.print_insn_mnem(ea)

if mnem == ‘call’:
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm(‘call 0x%x’ % (call_target), ea))
elif mnem[0] == ‘j’ and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm(‘%s 0x%x’ % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b’\x90’))
elif mnem == ‘pushf’:
ida_bytes.patch_bytes(ea, b’\x90’ * 9)
ea += 9
continue
ea += get_item_size(ea)

最后去混淆后的 switch 不能被 ida 正常识别出来,具体原因是前面获取返回地址 eip 的函数被 patch 成了 mov reg, xxx 指令,导致其与编译器默认编译出的汇编不同(程序开启了 PIE,直接访问跳转表的地址 ida 不能正确识别),因此需要将这里的代码重新 patch 回去。

同时为了不影响原本程序中的数据,这里我将修复的跳转表放到了其他位置。另外还有两个字符串全局变量也移动到了正确位置。

new_jmp_table = (0xA6000 - 0x2D54, 0xA6000)

移动并修复跳转表

for eax in range(0x20):
jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
new_jmp_target, need_handle = reloc.get(jmp_target)
assert need_handle == False
ida_bytes.patch_dword(new_jmp_table[0] + eax * 4, (new_jmp_target - new_jmp_table[1]) & 0xFFFFFFFF)

need_patch_addr = 0x963D7
ida_bytes.patch_bytes(need_patch_addr, asm(‘call 0x900;add ebx, 0x%x’ % (new_jmp_table[1] - (need_patch_addr + 5)), need_patch_addr)) # 修复指令
ida_bytes.patch_bytes(new_jmp_table[1] - 0x2d7a, ida_bytes.get_bytes(jmp_table[1] - 0x2d7a, 0x26)) # 复制字符串到正确位置

最终去混淆脚本如下:

from queue import *
import ida_bytes
from idc import *
import idc
from keystone import *
from capstone import *

asmer = Ks(KS_ARCH_X86, KS_MODE_32)
disasmer = Cs(CS_ARCH_X86, CS_MODE_32)

def disasm(machine_code, addr=0):
l = “”
for i in disasmer.disasm(machine_code, addr):
l += “{:8s} {};\n”.format(i.mnemonic, i.op_str)
return l.strip(‘\n’)

def asm(asm_code, addr=0):
l = b’’
for i in asmer.asm(asm_code, addr)[0]:
l += bytes([i])
return l

def print_asm(ea):
print(disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea))

class RelocDSU:

def __init__(self):
self.reloc = {}

def get(self, ea):
if ea not in self.reloc:
if idc.print_insn_mnem(ea) == ‘jmp’ and idc.get_operand_type(ea, 0) != idc.o_reg:
jmp_ea = idc.get_operand_value(ea, 0)

if idc.get_segm_name(jmp_ea) == ‘.got.plt’:
self.reloc[ea] = ea
return self.reloc[ea], False

self.reloc[ea], need_handle = self.get(idc.get_operand_value(ea, 0))
return self.reloc[ea], need_handle
else:
self.reloc[ea] = ea
if self.reloc[ea] != ea: self.reloc[ea] = self.get(self.reloc[ea])[0]
return self.reloc[ea], idc.get_segm_name(self.reloc[ea]) == ‘.text’

def merge(self, ea, reloc_ea):
self.reloc[self.get(ea)[0]] = self.get(reloc_ea)[0]

reloc = RelocDSU()

class Block:
def __init__(self, start_ea, end_ea, imm, reg, call_target):
self.start_ea = start_ea
self.end_ea = end_ea
self.imm = imm
self.reg = reg
self.call_target = call_target

def mov_code(ea, new_code_ea):
return asm(disasm(idc.get_bytes(ea, idc.get_item_size(ea)), ea), new_code_ea)

def get_real_code(block, new_code_ea):
ea = block.call_target
while True:
if idc.print_insn_mnem(ea) == ‘cmp’:
reg = idc.print_operand(ea, 0)
imm = idc.get_operand_value(ea, 1)
if reg == block.reg and imm == block.imm:
ea += idc.get_item_size(ea)
break
ea += idc.get_item_size(ea)

在 cmp 判断找到对应位置后会依次执行 jnz,popa,popf 三条指令

assert idc.print_insn_mnem(ea) == ‘jnz’
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == ‘popa’
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == ‘popf’
ea += idc.get_item_size(ea)

if idc.print_insn_mnem(ea) == ‘pushf’: # 第一种特殊情况,实际是 ret 指令。
return True, asm(‘ret’)

new_code = b’’
while True:
if idc.print_insn_mnem(ea) == ‘jmp’: # 第二种特殊情况,跳转过去可能还会有几条实际功能指令。
jmp_ea = idc.get_operand_value(ea, 0)
if idc.print_insn_mnem(jmp_ea) == ‘pushf’:
break
ea = jmp_ea
else:
code = mov_code(ea, new_code_ea)
new_code += code
new_code_ea += len(code)
ea += get_item_size(ea)
return False, new_code

def get_block(start_ea):
global imm, reg, call_target
mnem_list = [‘pushf’, ‘pusha’, ‘mov’, ‘call’, ‘pop’]
ea = start_ea
for i in range(5):
mnem = idc.print_insn_mnem(ea)
assert mnem == mnem_list[i]
if mnem == ‘mov’:
imm = idc.get_operand_value(ea, 1)
reg = idc.print_operand(ea, 0)
elif mnem == ‘call’:
call_target = idc.get_operand_value(ea, 0)
ea += idc.get_item_size(ea)
return Block(start_ea, ea, imm, reg, call_target)

def handle_one_branch(branch_address, new_code_ea):
new_code = b’’
ea = branch_address
while True:
try:
block = get_block(ea)
is_ret, real_code = get_real_code(block, new_code_ea)
reloc.merge(ea, new_code_ea)
ea = block.end_ea
new_code_ea += len(real_code)
new_code += real_code
if is_ret: break
except:
get_eip_func = {0x900: ‘ebx’, 0x435c: ‘eax’}
if idc.print_insn_mnem(ea) == ‘call’ and get_operand_value(ea, 0) in get_eip_func:
reloc.merge(ea, new_code_ea)
real_code = asm(‘mov %s, 0x%x’ % (get_eip_func[get_operand_value(ea, 0)], ea + 5), new_code_ea)
else:
if idc.print_insn_mnem(ea) == ‘jmp’ and idc.get_operand_type(ea, 0) != idc.o_reg:
reloc.merge(new_code_ea, ea)
else:
reloc.merge(ea, new_code_ea)
real_code = mov_code(ea, new_code_ea)

new_code += real_code
if real_code == asm(‘ret’): break
new_code_ea += len(real_code)
if idc.print_insn_mnem(ea) == ‘jmp’ and idc.get_operand_type(ea, 0) != idc.o_reg: # jmp reg is a swtich
jmp_ea = idc.get_operand_value(ea, 0)
if reloc.get(jmp_ea)[1] == False: break # 跳回之前的代码说明是个循环
ea = reloc.get(jmp_ea)[0]
else:
ea += get_item_size(ea)
return new_code

def solve():
entry_point = 0x48F4
new_code_start = 0x96150
new_code_ea = new_code_start

jmp_table = (0x892ac, 0x8c000) # [0x8c000 + (eax>>2) - 0x2d54] + 0x8c000

for _ in range(0x10000): idc.del_items(new_code_ea + _)
ida_bytes.patch_bytes(new_code_ea, 0x10000 * b’\x90’)

func_queue = Queue()
func_queue.put(entry_point)

while not func_queue.empty():
func_address = func_queue.get()
if reloc.get(func_address)[1] == False: continue
reloc.merge(func_address, new_code_ea)
branch_queue = Queue()
branch_queue.put(func_address)
if func_address == 0x4148: # 特判 0x4148 地址处的函数,读取跳转表。
assert new_code_ea == 0x963d0
for eax in range(0x20):
jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
new_jmp_target, need_handle = reloc.get(jmp_target)
if need_handle: branch_queue.put(jmp_target)

while not branch_queue.empty():
branch_address = branch_queue.get()
new_code = handle_one_branch(branch_address, new_code_ea)
ida_bytes.patch_bytes(new_code_ea, new_code)

当前 branch 去完混淆之后需要遍历代码找到 call 和 jmp 指令从而找到其他的 function 和 branch 。

ea = new_code_ea
while ea < new_code_ea + len(new_code):
idc.create_insn(ea)
if idc.print_insn_mnem(ea) == ‘call’:
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle: func_queue.put(call_target)
elif idc.print_insn_mnem(ea)[0] == ‘j’ and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
if need_handle == True:
branch_queue.put(jcc_target)
ea += get_item_size(ea)
new_code_ea += len(new_code)

ea = new_code_start
while ea < new_code_ea:
idc.create_insn(ea)
mnem = idc.print_insn_mnem(ea)

if mnem == ‘call’:
call_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm(‘call 0x%x’ % (call_target), ea))
elif mnem[0] == ‘j’ and idc.get_operand_type(ea, 0) != idc.o_reg:
jcc_target, need_handle = reloc.get(get_operand_value(ea, 0))
assert need_handle == False
ida_bytes.patch_bytes(ea, asm(‘%s 0x%x’ % (mnem, jcc_target), ea).ljust(idc.get_item_size(ea), b’\x90’))
elif mnem == ‘pushf’:
ida_bytes.patch_bytes(ea, b’\x90’ * 9)
ea += 9
continue
ea += get_item_size(ea)

new_jmp_table = (0xA6000 - 0x2D54, 0xA6000)

移动并修复跳转表

for eax in range(0x20):
jmp_target = (ida_bytes.get_dword(jmp_table[0] + eax * 4) + jmp_table[1]) & 0xFFFFFFFF
new_jmp_target, need_handle = reloc.get(jmp_target)
assert need_handle == False
ida_bytes.patch_dword(new_jmp_table[0] + eax * 4, (new_jmp_target - new_jmp_table[1]) & 0xFFFFFFFF)

need_patch_addr = 0x963D7
ida_bytes.patch_bytes(need_patch_addr, asm(‘call 0x900;add ebx, 0x%x’ % (new_jmp_table[1] - (need_patch_addr + 5)), need_patch_addr)) # 修复指令
ida_bytes.patch_bytes(new_jmp_table[1] - 0x2d7a, ida_bytes.get_bytes(jmp_table[1] - 0x2d7a, 0x26)) # 复制字符串到正确位置

for _ in range(0x10000): idc.del_items(new_code_ea + _)
idc.jumpto(new_code_start)
ida_funcs.add_func(new_code_start)

print(“finish”)

solve()

例题:SUSCTF2022 tttree

附件下载链接

首先将 0x1400100740x140017EFA140018C67 起始处的数据转换为汇编。

观察汇编,发现很多代码块之间相互跳转,因此先按照 retn 划分代码块。通过对代码块的观察,发现这些代码块按照 call $+5;pop rax(即 E8 00 00 00 00 58 ) 的出现次数可以分为三种:

  • 出现 0 次:
    在这里插入图片描述
    本质上是 其它操作 + retn
  • 出现 1 次:
    在这里插入图片描述
    这种代码块本质为 其它操作 + jmp target ,注意 其它操作 中可能包含 branch 。
  • 出现 2 次:
    在这里插入图片描述

这个可以看做 2 个出现 1 次的代码块两个拼在一起,其中前面一个代码块去掉 retn 。执行完前面一个代码块后由于没有 retn ,因此 target1 留在栈中。执行第 2 个代码块跳转到 target2 执行 ,在 target2 代码块返回时会返回到 target1 。因此这种代码块本质上相当于 其它操作 + call target2 且下一个要执行的代码块为 target1

我们定义代码块 Block 几个关键信息:

  • start_addr:代码块的起始地址。
  • asm_list:代码块的有效汇编,由于汇编指令可能包含 [rip + xxx] ,因此需要记录汇编指令的地址以便后续修正。
  • direct_next:执行完此代码块后接下来要执行的代码块地址。
  • branch_list:代码块中的所有条件跳转语句跳到的地址。
  • call_target:代码块调用函数地址。

class Block:
def __init__(self, start_ea, asm_list, direct_next, branch_list, call_target):
self.start_ea = start_ea
self.asm_list = asm_list
self.direct_next = direct_next
self.branch_list = branch_list
self.call_target = call_target

def __str__(self):
return ‘start_ea: 0x%x\ndirect_next: 0x%x\ncall_target: 0x%x\nbranch_list: %s\nasm_list:\n%s\n’ % (
0 if self.start_ea == None else self.start_ea,
0 if self.direct_next == None else self.direct_next,
0 if self.call_target == None else self.call_target,
str([hex(x) for x in self.branch_list]),
str(‘\n’.join([hex(addr) + ’ ’ + asm for addr, asm in self.asm_list]))
)

get_block 函数可以获取给定地址处的代码块并提取相关信息。代码块中可能有 push xxx;pop xxx; 这样的无意义指令,可以通过栈模拟来去除。

def get_block(start_ea):
ea = start_ea
stack = []
asm_list = []
branch_list = []
call_target = None
direct_next = None

while True:
idc.create_insn(ea)
mnem = idc.print_insn_mnem(ea)

处理混淆中跳转的情况

if mnem == ‘pushfq’:
ea += idc.get_item_size(ea)

assert idc.get_bytes(ea, idc.get_item_size(ea)) == b’\xE8\x00\x00\x00\x00’
ea += idc.get_item_size(ea)
jmp_base = ea

assert idc.print_insn_mnem(ea) == ‘pop’ and idc.get_operand_type(ea, 0) == o_reg
reg = idc.print_operand(ea, 0)
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == ‘add’ and idc.print_operand(ea, 0) == reg
assert idc.get_operand_type(ea, 1) == o_imm

jmp_target = (jmp_base + idc.get_operand_value(ea, 1)) & 0xFFFFFFFFFFFFFFFF
ea += idc.get_item_size(ea)

assert idc.get_bytes(ea, idc.get_item_size(ea)) == asm(‘mov [rsp + 0x10], %s’ % reg, ea)
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == ‘popfq’
ea += idc.get_item_size(ea)

assert idc.print_insn_mnem(ea) == ‘pop’ and idc.print_operand(ea, 0) == reg
assert len(stack) != 0 and stack[-1][0] == ‘push’ and stack[-1][1] == reg
stack.pop()
asm_list.pop()

assert len(stack) != 0 and stack[-1][0] == ‘push’ and stack[-1][1] == reg
stack.pop()
asm_list.pop()

ea += idc.get_item_size(ea)

if idc.print_insn_mnem(ea) == ‘retn’:
if direct_next == None:
direct_next = jmp_target
elif call_target == None:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Python工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Python开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img



既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Python开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注Python)
img

一、Python所有方向的学习路线

Python所有方向路线就是把Python常用的技术点做整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。

二、学习软件

工欲善其事必先利其器。学习Python常用的开发软件都在这里了,给大家节省了很多时间。

三、入门学习视频

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

//img-blog.csdnimg.cn/img_convert/9f49b566129f47b8a67243c1008edf79.png)

二、学习软件

工欲善其事必先利其器。学习Python常用的开发软件都在这里了,给大家节省了很多时间。

三、入门学习视频

我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-FKMYZAOl-1712574659780)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值