首页
社区
课程
招聘
[原创]国产电纸书Bambook破解笔记
2017-4-5 21:45 30379

[原创]国产电纸书Bambook破解笔记

2017-4-5 21:45
30379

感觉这个属于一个硬件分析的笔记, 比起android版发这里更合适.

word版不知道怎么附件传不上, 只得一点点粘过来

        从硬件到软件

    引子

    事情的开头要从一个国产单片机群说起. 群里潜水多年, 经常会遇见一群神人发表这样的言论:

    1 我的代码检测到自己被修改后, 就会擦写自毁

    2 我的代码会把电源连到地上, 让芯片自毁

    3 我的代码会控制一个接地的gpio引脚输出VCC, 烧了芯片

    4我的代码会控制2个连在一起的gpio一个输出VCC一个输出GND, 烧了芯片

    5我的电路会在打开产品外壳时候, 产生高压击毁芯片

    6 我的电路会在抄板后, 产生短路, 工作不起来

    7我的电路板上一些元件是错的, 标的是电阻其实是电容

    看起来大多数程序员都对"烧毁芯片"有特殊的爱好

    锦书硬破

    可能以后我会写一些笑话帖子时候, 再继续这个话题, 今天我们说的是, 改造盛大的一个封闭系统电纸书.

    当年, 大部分的eink reader都经过深度定制和限制, 不支持用户安装app, 开机自动进入阅读器主程序. 看书, 设置, 书城, 全部一个阅读器apk搞定.

    这机器型号是SD928, 面向公众的第一款. 从出错画面看, 系统应该是android. 我曾经想要把它的系统dump出来, 然后研究自制固件, 刷机, root. 为此还买了一个碎屏的机子, 准备拆下来用编程器读取. 但当年我烙铁焊不下来屏蔽罩, 当时也有办法就是从背面挖穿, 但完美拆机主义的我, 舍不得破坏完整的电路板.

    很多年过去了, 偶然淘宝一看还有人在卖坏机, 电池鼓包. 卖家表示应该还是好的, 遂77元购入, 拆开一看已经鼓得后盖变形了, 还好电路板没事. 又起了念头继续研究下刷机, 说时迟那时快, 请了热心网友帮我拆下了eMMC芯片, 又吹到eMMC转接板上, 插读卡器, 读取. 然后从dump下来的分区发现, 这机器的eMMC芯片是当作用户数据用的, 系统并不从eMMC启动. 不过eMMC的分区里面, 存储的有解密后的固件镜像. 应该是上次刷机过程残留的.

    既然系统不从eMMC引导, 那么是从哪里引导的呢? 在电路板上找到另一块之前忽略的芯片MT29C2G24MAKJAJC-75, eMCP的, 封装了DRAM和NAND flash.

    通过解压固件镜像, 我们可以提取系统的rootfs, 通过分析发现固件由PC端发到设备, 设备从服务器获取key, 解密后在/mnt/data/updates目录得出snda_s.firmware(System), snda_l.firmware(Logo) , snda_k.firmware(Kernel), boot_nontrust.bin(Bootloader) 几个文件 , 再通过fwupdate和bootloaderupdate工具刷到eMCP中.

    官方的固件里面, adbd是禁用的, usb连接上以后就是一个RNDIS设备, 没有其他功能.

    如果我们能在系统开机后, 执行fwupdate, 我们就可以实现本地刷机了. 之前主板的eMMC拆下后, 量出来了DAT/CLK/CMD等和旁边的电阻对应的接线关系, 我们可以实现免拆芯片在板烧写eMMC, 但还缺乏一个入口.

 

图1-1 在线烧写eMMC焊线图

    经过多次尝试, 发现当在菜单里面选择"恢复工厂设定", 系统就会执行/mnt/data/scripts/clear-user-data.sh脚本文件.

    同目录下还有个start.sh, 但它只在刷机下一次进入系统前执行. 以后每次重启和开机都不会执行. (可能老版本固件也会每次执行吧)

    因为我们测试比较频繁, 所以选择修改clear-user-data.sh, 去掉该脚本原有的清除用户数据命令, 并且加上如下几行

setprop service.adb.tcp.port 5555
/mnt/data/scripts/tinyftp -s 0.0.0.0 -p 3389 -c / > ./ftp.txt &
/sbin/adbd > ./adbd.txt
      

    这样在系统设置里面执行"恢复工厂设定"以后, 就会在5555端口提供adbd服务并且在3389开启ftp服务.

    这个tinyftp是从网上的代码修改的, ndk编译, 需要设置APP_PLATFORM := android-3.

    然后我通过固件解包再打包的方式, 修改init.rc, 生成新的snda_k.firmware重新刷机, 成功后就开启了adbd服务并且可以通过wifi连接上了.

    硬改到软改

    当然, 我不可能让大家都跑去拆机硬改, 这只是挖出厂家做为"秘密"封闭起来的镜像和系统架构的攻城车.

    有了adb shell以后, 我们可以编译一些工具进系统测试了. 考虑先试试老的CVE有没有办法利用, 查看系统是android 1.5, 内核2.6.28, 找了一个浏览器入口的CVE-2010-1807, 影响Android 2.2以前浏览器.

    锦书默认是个封闭系统但后面新的固件支持阅读器(SndaBrowser)里面运行html的widget, 可以用来测试该bug. 我做了一个widget打开这个cve网页. 再在阅读器里面载入.

    经过反复测试和dump SndaBrowser进程, 我修改了Itzhak Zuk的利用代码, 企图让该shellcode在锦书上执行. 最后试验结果是, 会引发Segmentation fault, 内存里面滑板代码也得到了填充, 但并没有成功跳转. 具体分析原因当时钉钉是web版没记录记不清了, 在此感谢村长的帮助.

    图1-2 SndaBrowser崩溃前的内存dump

    不过不用担心, 别人的洞不好用自己挖也可以. 之前分析它官方PC客户端跟设备之间通讯的时候, 我们在libSndaEBook.so的onSyncServerReceiveFile函数看到一些可以利用的地方:

    此bug有2个子bug, 其一是拼接字符串时候, 用到了filename, 这个字符串我们可以通过自制PC客户端来控制, 在文件名中传入分隔符后, system调用可以执行额外指令.

    其二是install-bookimage.sh写的不好.

    我们来看一下install-bookimage.sh:

#!/system/bin/sh
echo 'Decompress book image to /mnt/data/'
tar zxf $1 -C /mnt/data/
echo 'Install keybase'
chmod 777 /mnt/data/key/skb-newrsa-file
/mnt/data/key/skb-newrsa-file /mnt/data/key/rsa-pair.txt
echo 'Remove key directory'
rm -r /mnt/data/key/
echo 'Install book image success!!'

    这个脚本做的事情非常简单, 将PC端发来的文件解压到iNAND的用户分区, 然后给/mnt/data/key/skb-newrsa-file加上可执行权限, 再执行它.

    我查看了下, 用户分区本身没有这个可执行文件. 也就是说设计上他是上一步释放出来的. 很显然, 这是一个比刚才更好的bug, 它不但能够直接植入文件, 还顺便帮你加可执行权限, 再执行它.

    从提示信息来看它应该是厂家为了弄一些"正版图书大礼包"预留的接口, 不是开放给用户使用的. 经过分析, 官方的云梯客户端, 也没有这个接口.

    这里跟刚才那个case属于同一个switch, 可以看到传送固件也是走的这个接口, 只是封包中包含的fileType字段不同. 我们可以模仿官方的协议来通信, 或者偷懒在官方的dll上补一下实现我们的需求.

    经过分析BambookCore.dll, 我们找到一处给设备传输书架信息的ReplaceCatelogFile函数, 它会给设备发一个fileType是12的catelog.xml文件. 注意这里的0D设备上switch的fileType将是0C.

    .text:1000B5C7 0A4 C7 44 24 7C 0D 00 00 00                 mov     [esp+0A0h+type], 0Dh

    .text:1000B5B0 0A4 68 C4 0F 0E 10                          push    offset aCatalog_xml ; "catalog.xml"

    PC端通讯参数

    设备端逻辑分支

    这个可以结合上面shell脚本的bug, 实现执行我们的代码. 我做了一个小demo, 把mov补为0E, 然后发送自己构造的tar.gz

    我们把tar包里面key/skb-newrsa-file换成了可执行文件(后来发现脚本也能用), 感觉它这个skb-newrsa-file原本是可执行文件, 作用是给snb文件安装授权的 (因为不是用户通过云中书城下载的私有书).

    经过测试成功得到执行, 而且这个bug中开启的shell是root权限. 我们设置了adbd的属性后直接执行adbd, 就可以通过内置的fwupdate进行刷机了. 该工具使用未加密非打包的固件镜像, 也不用考虑再封包的问题.

    由软入硬

    刷机工具和Bootloader

    通过分析刷机工具和系统mtd设备, 我们整理出了eMCP的NAND分区划分:

0x00000000-0x00100000 : "Bootloader"            active
0x00100000-0x00400000 : "Kernel"
0x00400000-0x04a00000 : "system"
0x04a00000-0x06300000 : "userdata"
0x06300000-0x06400000 : "logo"
0x06400000-0x06500000 : "Bootloader_backup"     factory?      mdtblock5
0x06500000-0x06800000 : "Kernel_backup"         factory       mdtblock6
0x06800000-0x0ae00000 : "system_backup"         factory       mtdblock7
0x0ae00000-0x0af00000 : "logo_backup"           factory       mtdblock8
0x0af00000-0x0b200000 : "Kernel_reserve"        shadow        mtdblock9
0x0b200000-0x0f800000 : "system_reserve"        shadow        mtdblock10
0x0f800000-0x0f900000 : "logo_reserve"          shadow        mtdblock11
0x0f900000-0x0f920000 : "flags"          mtdblock12
0x0f920000-0x0f940000 : "systeminfo"     mtdblock13
0x0f940000-0x0f9c0000 : "EinkFW"         mtdblock14

可见Kernel/system/logo都存在了3份拷贝. 最前面是活跃的, 刷机时候fwupdate参数可以控制刷入到backup(出厂预设)或者reserve(正常刷机)的分组, 不会刷新当前活跃的分区.

    我们推测Bootloader会在刷完机下次引导时候选择reserve复制到活跃的分区.

    额外提一句, 这里的分区表是在设计系统时候预先划分好的, bootloader和系统都使用同样的硬编码偏移. 所对应的地址就是NAND的原始地址.

    我们先看Bootloader. 根据Marvell的文档(PXA3xx_TavorP_BootROM_Ref_Manual.pdf), 我们知道它是有结构的, bootrom按照头部进行装载. 不是整个装入内存或者挂载到指令总线上从0偏移执行.

    可以看出这个Bootloader没有Trusted标记, 有3个子image, 一个是TIMH, 就是这个头部本身. 一个是OBMI, 位于flash的0x20000偏移, 会被bootrom载入到内存的0x5C013000, 另一个是OSLO, 位于0x40000会被载入到0x81000000.

    那么OBMI跟OSLO是不是就运行在这2个内存地址呢? 这个我们要再次确认.

    查看OBMI的ResetHandler, 它会把自身image复制到0x800007FC位置, 0x80000000位置存放的是ResetHandler地址本身, 这中间的7F8空间可能是被用作变量空间了. 这类的image,  并不像大家想的那样都是位置无关的, 大部分都是位置相关再加一个检查/复制/跳转代码的.

    举个例子, iBoot的Reset Handler, 根据VectorTable地址我们可以判断代码需要运行在9FF00000地址, 通常iBoot的loader会初始化好内存map并将其加载到合适的地址, 但iBoot自身也额外带了检查和复制代码.

    当发现自己不在指定位置时候, 就会依次复制代码/常量/变量到指定的地址.

    这些信息可以用来指导我们进一步划分/增加区段.

    代码2-1 iBoot拷贝片段注意ADR是取PC相对地址.


    图2-2 初步整理后的iBoot区段


    有一些芯片, 是支持从flash直接执行的, 不需要占用内存. 这时候不管是rtOS还是baremetal的image, 都会有一个从链接后的image中, 复制一部分已有初值的变量到sram/dram的流程.

    具体的内存布局不同, 要看对应的编译链接脚本和安排这一切的程序员.

    介绍这些并不是废话, 在我们分析时候, 进行正确的分段和添加堆的内存区域能提高逆向效率.

    例如iBoot我们确定了.data这部分信息, 逆向过程中可以新建RAM段并且载入, 以便观察一些全局变量的初值.


    最后我们确定在OBMI中包含名为Blob的xscale专用bootloader, 运行在80008000. 在flash的偏移是40000.

    正常刷机流程中, 应该是fwupdate设置systeminfo分区的里面的几个标志位, 下次启动时候, bootloader检测这个标志位并更新活跃的分区, 再去掉该标志.

    而另一套备份固件, 怀疑需要特殊的按键组合触发. 我们怎么找这些按键的组合呢? 首先这些电子设备, 一般不会像电脑一样, 还做一套PS2/USB接口来外挂键盘. 最为常见的线路接法有GPIO单独检测, 行列扫描两种. 两种都支持多键组合检测.  该设备cpu型号是PXA310, 根据marvell提供的介绍, 该cpu具有directkey和matrix key两套键盘接口. 它的matrix扫描是片内设备实现的, 并且支持自动扫描, 我们可以直接从Keypad Matrix Key (KPMK) Register读取有哪几行被按下, 然后从KPASMKP0~KPASMKP3读出每行按下的列. 每个寄存器包含2行, 一共64个按键都在其中.

    通过查找参考手册里面的寄存器地址, 我们在IDA中寻找敏感的地址, 找到如下代码

    (参考手册PXA3xx_DM_vol_III.pdf, Table 254: Keypad Controller Register Summary)


ROM:8000F5FC 41 04 A0 E3+                MOV     R0, #0x41500000 ; keypadbase
ROM:8000F604 3D FD FF EB                 BL      func_config_keymatrix_MFPR
   各位, 你们一定以为我是二进制搜索00005041来定位的吧, 其实还真不是, 我在整理函数名时候把初始化部分挨个看了一遍, 所有落在外设范围内的地址我都肉眼观察了一下.

再说了, XScale是活在没有thumb2 (主要是MOVW, MOVT)的时代, ARM代码中很常见的优化会把一个常数用各种移位和算术运算拼出来, 拼不出来才会用2条LDR加一个函数末尾的常数的方式(效率较低). 二进制搜索很难奏效.

    不过贴心的IDA里面默认会将拼接常量的指令合并为一条伪指令. 使用Search immediate value即可. 当然也要注意有时候编译器在拼接常量的过程中插入了其他指令, 试试文本搜索(针对最后一次拼接的注释)和搜索常量的一部分或许也有找到的时候.

    这里说句题外话, 在movw/movt中间嵌入svc或者自制的不影响寄存器的call, 也是我以前喜欢的干扰IDA分析方法之一.

ROM:800098F0 2E 19 00 EB                 BL      _DoUpdateFirmware
ROM:800098F4 FA FE FF EB                 BL      func_check_eink_firmware
ROM:800098F8 40 FE FF EB                 BL      func_mayshowlogo
ROM:800098FC 41 04 A0 E3+                MOV     R0, #0x41500000 ; kpcbase
ROM:80009904 BB 19 00 EB                 BL      _Key_DoUpdateFirmware

同时我们找到的还有另一个使用该地址的函数, 将会在后面分析.

    首先看初始化键盘控制寄存器的, 我们根据代码的配置可以得出需要测量的CPU引脚编号, 再根据引脚编号测量硬件按键对应的GPIO.

    这里注释的内容是跟后面提到的func_config_UART3_and_enable_clock一样的方式推导出来的.

    至于BGA脚位, 参考文档PXA3xx_EMTS.pdf, 4.1.2.1 PXA310 Processor 13mm2 VF-BGA Ball Map

    图2-6, 翻页键排线座


    我们用夹子夹到这些引脚上, 再测试它和哪些cpu引脚相通, 列表记录.

引脚编号

GPIO

 角色

F14

(GPIO122)

KP_MKOUT<1>

 A15

(GPIO115)

KP_MKIN<0>

A16

(GPIO116)

KP_MKIN<1>

VSS

 

 

    表2-7 翻页键接线

    此表分别记录了BGA引脚编号 GPIO编号设计中角色


    图2-8, 五向键排线座


引脚编号

 GPIO

角色

 Alias

F14

                    (GPIO122)

                    KP_MKOUT<1>


               

E13

                    (GPIO121)

                    KP_MKOUT<0>

                    a

B13

                    (GPIO123)

                    KP_MKOUT<2>

                    b

F12

                    (GPIO125)

                    KP_MKOUT<4>

                    c

Y18

                    (GPIO4_2)

                    KP_MKOUT[5]

                    f

Y19

                    (GPIO6_2)

                    KP_MKOUT[7]

                    d

W15

                    (GPIO2_2)

                    KP_MKIN<6>


               

VSS

 

 


               

    表2-9 五向键接线


    这里翻页键是1*2矩阵, 五向键是6*1矩阵.


    电路板上有一个区域是窝仔片构成的导航和数字键, 我们按照横向ABC竖向abcdef助记方式, 测试每个键处于哪个交点上.

    为了测量和描述方便, 我们把横向称为行(row), 纵向叫做列(col), 其中行为输入,  列为输出. 这也跟Marvell的设计一致.


    图2-10 数字和菜单键矩阵相同的大写字母为同行相同小写字母为同列


    首先我们看看软件中的col分别对应这些导航和数字的哪几列

    col0=a col2=b col4=c col7=d col5=f

    不在这个主板上的五方向键其实也占了单独一行.

    fivekey=row6

    A nav/num 789 B14(GPIO119) = row4

    B memu/num456/# C15(GPIO118) = row3

    C num123*0=E14(GPIO117)=row2

    e num258 = Y20(GPIO5 _2)=col6

    以下略



    整理矩阵表如下


 col0(a)col1col2(b)col3col4(c)col5(f)col6(e)col7(d)
KPASMKP0x28 0x30 0x38 0x40 
row0 pgup unused    
row1 pgdn      
row2(C)*   0321
row3(B)菜单 听书 #654
row4(A)找书 互动 书架987
row5        
row6确定 返回 
row7音量+音量-      

     

    hold 上侧C12(GPIO127), 角色KP_DKIN<0>, 它估计设计时候每次刷新一下墨水屏就睡眠, 所以用DirectKey来做中断唤醒.

    音量加减也是矩阵, 音量按键右侧公共端是行, CPU脚位测得W20(GPIO3_2), 对应row7, 音量减的列为F14, 音量加为a(E13)


    然后接着看另一个使用了0x41500000做参数的Key_DoUpdateFirmware函数, 其中kpcbase就是传递来的0x41500000, 通过查编程手册我们可以搞清这些寄存器的用途, 为了方便听众老爷我记录在刚才那张表里了. 然后让我们来还原这些按键检测的真面目:

    代码2-12 手动恢复固件片段

    我们略过第二层if从它末尾看, 在上面的第一层if包含了以下的判断: 第一个的true分支根据代码看是检查硬件加密芯片, 触发条件显然是菜单+8, 第二个if的false分支则是检测音量+/音量-同时按下, 执行DoReservedFirmwareUpdate.

    我们再看上面第一次if那个判断的false分支, 也就是[找书]+[1]成立, 执行的是DoFactoryFirmwareUpdate

    这个g_sysflags实际是systeminfo分区存放的内容. unReserveFWReady和unFactoryFWReady分别是指示普通升级过程的保留分区是否存在数据和出厂固件分区是否存在数据.

    我们在系统的fwupdate和对应的so里面, 同样可以找到将固件写到这两套分区并设置不同的标志的代码. 这里估计是做的防护措施, 一旦活跃的固件不小心损坏了, 售后人员可以直接操作组合键先来恢复一下出厂固件, 也不用把一些内部维护工具发到维修站, 以免泄露出去.

    当然这个机器, 除了Bootloader级别的组合键, 还有进入系统后再检测的组合键.

    下面讲一下发现的过程. 我假装无意中从它的system分区发现了一些有意思的apk, 比如FoxconnTest.apk, SndaBrowser.apk. 前者是富士康测试程序, 后者是该阅读器的主界面.

    当初我假设机器的启动脚本中, 根据某个特殊的按键组合, 选择启动二者之一. 后来实际分析发现并非如此. 在snda.browser.Browser中发现一个IsDiagMode函数

  public boolean IsDiagMode()
  {
    if (new File("/dev/sagadiag").exists())
    {
      Log.d("Browser", "!!!!!!!!!!!!!! is in DiagMode");
      return true;
    }
    Log.d("Browser", "!!!!!!!!!!!!!! is not in DiagMode");
    return false;
  }
   他靠设备目录下是否存在/dev/sagadiag判断是否是DiagMode.

    接着看在onCreate中有如下代码引用这个函数

    this.m_isInDiagMode = IsDiagMode();
    this.isInFoxConnDiagMode = getOtherDiagMode().equals("1+7");
    这个getOtherDiagMode()就是读取/proc/driver/diagmode的3字节内容.

    我们可以认为它至少有两种诊断模式.

    然后搜索DiagMode, 找到另一段代码:

  public boolean factoryTestOnKeyUp(int
paramInt, KeyEvent paramKeyEvent)
  {
    Log.d("Browser", "factoryTestOnKeyUp, keyCode =" + paramInt);
    switch (paramInt)
    {
    default:
      return false;
    case 8:
      startActivity("com.android.mfgri", "com.android.mfgri.MfgRIAct");
      return true;
    case 9:
      startActivity("com.iacs.fa", "com.iacs.fa.SagaFA");
      return true;
    case 10:
      startActivity("com.iac.encrypt", "com.iac.encrypt.encrypt");
      return true;
    case 11:
      startActivity("com.iac", "com.iac.DemoApp");
      return true;
    }
    if (this.isInFoxConnDiagMode) {
      startActivity("com.foxconn", "com.foxconn.FoxConnTestActivity");
    }
    return true;
  }
   这段代码是在onKeyUp里调用的
    if ((this.m_isInDiagMode) || (this.isInFoxConnDiagMode)) {
      return factoryTestOnKeyUp(paramInt, paramKeyEvent);
    }

前四个姑且不论, 这个com.foxconn.FoxConnTestActivity正是我们的富士康测试app. 他将在/proc/driver/diagmode 的内容为"1+7"时候启动测试界面. 也就是说SndaBrowser这里充当了Launcher.

    我们在解开的initramfs和system包中分别搜索diagmode和sagadiag, 发现以下有意思的内容

DIAG_MODE=`cat /proc/driver/diagmode`
if [ "$DIAG_MODE" = "1+5" ]; then
    mke2fs -j /dev/block/mmcblk0p2
else
    e2fsck -yf /dev/block/mmcblk0p2
    tune2fs -j /dev/block/mmcblk0p2
    #e2fsck -p /dev/block/mmcblk0p2
    tune2fs -m 0 /dev/block/mmcblk0p2
fi
mount -t ext3
/dev/block/mmcblk0p2 /mnt/data

这段是用来选择格式化/mnt/data或者检查分区的.

DIAG_MODE_CONTENT=`cat
/proc/driver/diagmode`
if [ -n "$DIAG_MODE_CONTENT" ] && [ "$DIAG_MODE_CONTENT" != "1+9" ] && [ "$DIAG_MODE_CONTENT" != "1+5" ]; then
                echo "FA MODE: $DIAG_MODE_CONTENT"
                rmmod g_ether
                insmod /system/lib/modules/g_file_storage.ko file=/dev/block/mmcblk0
                if [ "$DIAG_MODE_CONTENT" = "1+4" ]; then
                                vcom_foxconn_test
                                exit 0
                fi
else
                echo "Snda Init Script"
                busybox cp /system/app/snda_instsp.sh /tmp/snda_instsp.sh
                chown system.system /tmp/snda_instsp.sh
                chmod 777 /tmp/snda_instsp.sh
                /tmp/snda_instsp.sh
fi

这一段则是去掉usb口RDNIS共享上网的模块, 并将eMMC芯片挂载为U盘. 应该是工厂用来复制用户数据分区的.

    从我们这里和刚才的信息看, 1+7, 1+5, 1+9,可能就是键盘上数字键的组合.

    图2-19 Foxconn Diagnostic截图

    经过验证, 成功进入这个Foxconn测试界面.

    改造Bootloader计划

    上面我们分析时候, 看到Bootloader有一个输出信息是

    Autoboot aborted

    Type "help" to get a list of commands

    我怀疑它可能留有调试用的串口, 可以用来查看这些输出信息.

    虽然这个Blob编译时候因为bootdelay设置为0导致无法进入这个boot console, 但我看了下里面很多命令都没有阉割. 包括通过串口/网卡刷写flash, 聪明的听众们是不是猜到这个有什么用了, 对这个可以预防我们做固件时候变砖. 不过我们已经有更好的方案, 就是找到的组合键可以选择恢复backup或者reserve的固件之一, 只要我们不神经一次性刷坏两套, 总可以进到系统的.

    我们补上进菜单功能的话, 可以不怕一次刷坏两套备份.

    或许有人说了, 刷firmware的几个分区没有危险, 如果我们改Bootloader不小心某次改坏了, 它的恢复firmware分区功能无效了, 那怎么办, 不要紧, 我们还有JTAG.

    JTAG接线的挖掘过程可以看附录的"寻找测试点"章节.

    Jtag接口本身的介绍, 估计大家一搜就是什么标准专家组, 什么多芯片串联接法, 我要说的是没那么复杂, 我们改造出来的JTAG刷机跟使用手机开发板/单片机开发板一样都是一对一的.

    TODO:

    如何制作刷不死bootloader, 如何找uart

    我们在Bootloader的代码中, 可以看出puts依次调用SerialOutputString, serial_write, serial_driver->write(c).

    查看该全局变量赋值位置:

    serial_driver = &g_liittleton_serial_driver;

    再次查看g_littleton_serial_driver, 它的几个成员如下:

ROM:8002B5CC 28 7B 01 80+g_liittleton_serial_driver uart3_funcs <func_uart3_init, func_uart3_getch, func_uart3_putch, \
ROM:8002B5CC DC 7A 01 80+                                        ; DATA XREF: _main+38o
ROM:8002B5CC 04 7B 01 80+                                        ;
ROM:off_800099B0o ...
ROM:8002B5CC B4 7A 01 80+                             func_uart3_poll, func_uart3_flush_input, \
ROM:8002B5CC 3C 7A 01 80+                             func_uart3_flush_output>
   为什么我们那么确定这个串口是uart3, 因为func_uart3_init里面调用的配置函数如下:
void func_config_UART3_and_enable_clock()
{
    v40E10628 = 0xC041;                                         // GPIO109 = UART3_TXD
    v40E1062C = 0x4041;                                         // GPIO110 = UART3_RXD
    v4134000C |= 0x800000u;                                     // D0 Mode Clock Enable Register A (D0CKEN_A)
                                                                // CKEN23 = 1
                                                                // UART3 Clock Enable
}
   


   

    它这些地址在Marvell的文档里面可以查出来是对应什么外设范围, 叫做什么寄存器. 前面2个就是配置"路由", 也就是物理引脚切换到什么逻辑设备上(时间长了, 此处需要核对手册).

    可以查看手册PXA3xx_DM_Vol_I.pdf, Table 12: PXA31x Processor Alternate Function Table, Table 16: PXA31x Processor Pad Control Addresses, 4.11.2 Multi-Function Pin Registers (MFPR)

    前面一个表是描述每个物理引脚可以连接到的功能, 而第二个表就是控制这些功能的寄存器地址, 最后一张表则是描述这个寄存器地址写入的值功能. 我们主要关心它最后3bit, 在我们这两句赋值里, 都是0b001 = Alternate function 1

    至于这些func_uart3_init风格函数名是根据功能和Blob网上找到的参考代码整理的. Bootloader本身当然不带符号.

    我的习惯是如果能确定官方的函数名我就不用func_前缀. 这样在功能列表打个过滤可以明显的看出有多少sub_是完全没搞清楚功能, 多少func_是没有官方名称.

    我们来看main函数里面引导部分, 盛大发布的固件设置了boot_delay为0并且删掉了等待串口字符打断自动引导的代码, 不过不知道为啥后面那个命令模式的console功能却没删. 而它那个比较超时时间大于-1的也没删. 简单的改的话,  CMN R3这里改为比较0, 就会次次都进命令模式. 或者改为一个按键检测, 复位时候按住某个键则不引导系统, 进命令模式, 当然要修改的尽善尽美, 可能需要补充较多的子函数, 这时候手工补opcode就很烦了. 不过还好, 之前我做过一个insanelinker, 用来补3ds的游戏跟iBoot的, 能把obj文件嵌入或附加到二进制文件里并解析符号, 当然符号要从IDA导出. 如果用于这个Bootloader, 要将修栈和搬代码的地方修改修改. 当然这怎么说也是个TODO, 真有哪位念旧dalao愿意接手做自制, 我可以花点时间填上这个坑.

    附录

    一些常识

    GPIO: 软件和硬件的桥梁

    各位听众朋友你们肯定或多或少的听过驱动, IO这样的词语.

    如果你做过手机或者嵌入式, 查看过驱动的代码, 会发现很多时候, 我们操作一个地址, 并不是改变内存, 而是改变管脚输出电平, 或者操作一个硬件的工作状态, 或者直接用某种电平组合从外部接口发出一组数据.

    这里有几个概念, GPIO, soc, peripheral. GPIO通俗来说, 就是可以测量或者输出高低电平(高阻)的物理引脚. 当然除了少数情况下, 一个在真实世界的物理引脚, 是可以在内部借由多路开关(路由)连接到不同的功能模块的. GPIO是功能模块之一, 你可以认为它跟串口/DAC/定时器这一类设备一样, 都属于"资源". 单片机为了区分对指定地址的访问是存储器还是外设, 他有一张MemoryMap, 有的是可以配置的, 有的是固定的. 访问硬件资源的区域, 就是peripheral接口. 其中外设我们沿用的还是几十年之前的概念, 现代的cpu片内都集成了大量的接口和功能模块,  我们通常说的就是这些集成在芯片内的片上外设.这种架构就叫做SystemOnChip, 简称Soc. 有的芯片甚至集成了CPLD可以让用户扩充标准外设甚至编写非标准的外设. 但是是不是所有的厂商都会把常用电气接口都给集成在芯片呢? 是不是集成的都是最好的呢?


     这一点有一些例外, 比如USB HighSpeed/SuperSpeed, 很多cpu是要靠外部的芯片实现电信号(D + /D - )的编码解码的, 有线网卡也是, CAN也是绝大多数需要收发器芯片的.

    这些芯片我们通俗的叫他PHY, 多数是对外接口是数据引脚少而频率高, 对cpu接口数据引脚多而频率低, 又或者需要特别供电, 或者信号线上有干扰, 这样的一些应用场景. 也有的是因为用来传输信号的方式不同, 举个例子, 不同项目需要同轴, 双绞线, 光纤来传输, 都做片上集成不说成本, 兼容性肯定众口难调.

    我们研究一套硬件的时候, 在真实世界看到的是引脚编号, 在代码中看到的是对地址的操作. 如果用户写代码都是一堆读写地址, 岂不是逆向的时候光查手册都累趴下了? 还好很多用户有使用厂家封装的外设库习惯, 我们需要对照厂家的文档, 通过查看使用参数/或者直接操作peripheral区域的代码, 逐个整理这些被静态链接到固件中的函数名称. 一份好的符号将大大有利于我们的分析. 当然, 如果你可以分析出固件编译和链接所用的工具链和大致的版本号, 你还可以使用BinDiff等工具, 让你的函数排查更精确.


    形形色色的寄存器

    刚才我们提到有些外设寄存器, 是通过地址访问的, 那这个寄存器跟R0~R15这样的寄存器, 有什么区别? 首先, 我们开发和逆向时候遇到的寄存器有三类, 第一就是R0~R15这样的CPU通用寄存器, 其中浮点寄存器VFP/NEON, 也可以归为这类. 他们要总结相同点就是能够直接访问里面的数据, 第二类就是特殊寄存器, 无法直接操作, 需要先复制到通用寄存器或者必须从通用寄存器传值, 有些可以改变协处理器的工作状态, 有的可以操作内存控制器. 不止是Cortex有, 一些8位16位芯片, 也有特殊寄存器, 比如STC, HCS12x, 不过有些不是专用指令复制而是通过低位地址访问.

    第三类是外设寄存器, 不用严格区分片上外设还是片外的, 片内外设, 基本都是通过地址访问的. 这种地址有别于内存地址, 在逆向中我们需要注意到, 有些寄存器是只写的, 有些则是写入后(等待一段时间)会被擦掉某个bit的, 我们反编译时候, IDA无法识别这个地址的数据写入和读出会不一致, 可以参考IDA的说明

    https://www.hex-rays.com/products/decompiler/manual/tricks.shtml

    但我们可能一段地址区域, 里面只有少数几个是这样的寄存器, 把它们分别定义为不同的段很麻烦.

    还有一个需要留意的地方, 外设寄存器不一定是和处理器字宽相同. 我简单想了下, 可能编译器的对齐访问的优化可能会意外的对另一个寄存器执行读取再写入的操作, 对内存地址无所谓, 对外设地址可能一读一写就会出现bug. 不过没有具体遇到这样的bug, 也不知道有没有相关的利用.

    可能是我想多了吧.

    外设寄存器还有一类(大多是无法挂到地址线的外设), 需要通过接口专属的协议, 比如IIC, SPI, 8080, 更加间接的读取. 而有些外设接口是可以挂载到地址直接访问的.

    关于测试点的闲话

    零售机上为何会有测试点

    常看小说的人都会知道, 弃城撤退时候, 一般都会烧毁粮草,  砸碎攻城器械, 甚至井水下毒, 结合我们引子里面的话, 是不是有种似曾相似的感觉?

    我们没法用产品来打击敌人, 但我们也不能资敌, 在我看来, 保留着调试/开发用的功能, 和电路板上关键测试点就是一种无法理解的行为. 可能很多搞硬件设计或者固件的不以为然, 我们再防, 别人只要重新做块板子/找人解密就可以搞定了, 不如做点实际的, 烧毁自身让破解的恶心一下…

    原则上说, 很多测试点都不应该留在零售机上. 应该工程机和零售机上使用不同的电路板布线甚至不同的芯片, 防止用户将原本用于开发的接口变为分析调试的助手. 比如我就感觉盛大放的这些测试点方便了我.

    但实际来看现阶段大部分手机和其他嵌入式设备(路由器/穿戴设备/智能家居)都预留了一些串口/SWD/JTAG的测试点/排针座.

    手机维修领域的硬件设备, 比如Riff box, JPR, 东海等等, 都有大量机型的测试点接线资料. 很多手机维修小店铺, 就是靠这个来修砖赚钱的.

    非要编一些理由的话, 比如有些故障, 在工程机环境就是不发生, 或者随着将零售机拆开/吹焊芯片后短时间内就无法再现, 必须要保留这个来检修/检测故障, 又或者由于成本考虑, 省略了原型, 开发, 批量阶段的硬件投入,  直接用同一套设计, 连测试架都不用重新订做了.

    如何寻找特定的测试点

    我们关心的可能是串口(看看是不是有特殊日志输出), 也可能是调试接口(JTAG/SWD/BDM). 首先我们需要cpu的文档, 确定脚位和可重新分配的功能表.

    因为很多CPU引脚的JTAG/SWD脚位都是不可路由的, 所以只要有线路图或者PCB图就可以很轻易的找到. 我们有了板子,找抄板公司整理出PCB图纸无疑是最方便的, 当然如果你对整机原理没有研究的需要的话, 还可以用一类叫做JTAG Finder的工具来定位, 上面提到的几个手机维修工具都有这种功能.

    当然这类工具也有适用范围, 因为需要满足一次测试过程尽可能包含所有JTAG信号, 如果主板上测试点非常多, 可能多次组合才能确定下来, 只碰上一两根时候根据工具的原理和连上的线, 有可能测不出来, 有可能结果错误. 影响你分析.

    硬件工具列表参考http://www.cnblogs.com/shangdawei/p/4815819.html

    如果没有引脚查找工具, 而且cpu封装是BGA/WCSP这类无引脚封装, 可以先拆下cpu, 从焊盘中定位目标功能可能对应的引脚, 焊接飞线并测试是否跟PCB上某测试点相通. 这种情况可以见后面章节的示例.

    对于引脚裸露的封装的cpu (DIP, SOP, QFP, 单层QFN也算), 还可以尝试直接按照资料对应的脚位飞线出引线, 再测试是否是需要的功能.

    这里提醒一句, 绝大多数用了单片机的消费产品, 都烧写后禁用了读出. 没人乖乖的等着你免费抄他们的东西, 老外也不例外.

    JTAG/SWD/SWV关系

    除了我们说的传统JTAG(单节点使用6~7条信号线), 还有一类缩减引脚的JTAG协议, 只使用TMS/TCK两个信号线. SWD也是这种协议. 有一些入门级的Cortex-M芯片甚至只提供SWD引脚, 不提供完整的JTAG引脚. 有的芯片额外提供了SWO引脚, 如果测试中发现它跟SWDIO/SWCLK测试点放在一起, 可能开发者开发过程中用它来输出一些调试用的业务信息. 因为有时候semihosting和uart都无法满足速度的要求. 当然正式版固件依然输出调试信息的可能性不大, 但确实遇到过. 比如不使用#define来彻底移除调试打印, 而是定义一个变量, 通过"只有厂家才知道的"指令/其他配置可以打开日志. 这样的靠变量控制隐藏的"秘密日志"我想很多人分析时候都遇到过.

    如何防止芯片的调试功能被利用

    幸运的是, 开发阶段的芯片可控性跟零售产品之间的矛盾soc厂商已经想到了. 各个soc厂商假设了一些安全需求, 并实现了一些不同的保护功能. 举例来说: Marvell的PXA3X0系列处理器, 其芯片内置的bootrom, 在trust boot流程中, 会由bootrom本身禁用JTAG接口. 当然这个接口物理上还是存在的, 可以通过发送证书来重新开启.

    有些厂家使用了熔丝, 在零售机出厂前使用某些指令将其熔断, 物理上断开内部的硬件调试模块. 也不排除有些厂家实现方式是用内部EEPROM/flash存储禁用调试接口的配置, 然后禁止对应外部引脚路由到内部调试模块. 这样勉强算是属于硬件实现, 只是并非物理上的熔断.

    还有些厂家做的不彻底, 调试功能只能在上电后由用户代码来禁用, 这就属于鸵鸟政策.


芯片

厂家

                    调试接口

调试接口禁用

代码读出控制

                    USB引导

                    USB烧录

                    串口烧录

STM32

ST

SWD(JTAG)

用户软件,烧录软件

option bytes,去除ROP绑定擦写操作

 

 

通用串口+官方ISP,可擦写后重新启用JTAG

A5

Apple

SWD

                    物理熔断

 

                    bootrom

引导可实现

                    未知

Nano100

新唐

SWD

                    用户软件

LOCK bit

 

 

 

PXA3x0

Marvell

JTAG

                    bootrom


               

                    bootrom

                    支持


               

PIC

Microchip

ICSP

 

熔丝

 

 

                    需要用ICD

MC9S12X

Freescale

BDM

无法禁用

security bits, unsecure会全部擦写                

 

                    无usb


               

ESP32

乐鑫

SWD

新版有eFuse

新版有eFuse  (不确定是否物理熔断)

 

                    无usb


               

STC15

宏晶

需要IAP芯片

无调试功能

只写

 

部分型号

通用串口,几乎全部型号

EZ-USB

Cypress

 UART

 

 

支持

 


               

    表5-1 各厂家的保护方式

    烧录, 测试点, 工程模式

    提到调试功能很多人都会想到JTAG, 但你看到其他介绍硬件分析的都会煞有其事的告诉这东西是用来测试电路的, 然后就是经典的一个JTAG接口如何挂载多个被测设备接法. 让有心研究硬件hack的兄弟们知难而退.

    半仙告诉你不要沉迷那些高大上的标准, 我们只需要知道现在手里面拿的产品, 它里面就是用普通的物理测试点在质检流程测试是否正常工作, 有的也有一些工程模式或者工程固件来测试周边的输入输出设备是否正常.

    换句话说现实中JTAG不是用来"Test"的, 它留在电路板上要么是厂家用来刷机/维修, 要么就是早期开发过程调试. 也有很大可能你拿到的设备, 它们的开发人员就不用JTAG/SWD等等仿真接口调试, 这就要看做电路板时候工程师是不是习惯性预留了.

    当然越来越多的cpu支持usb发送bootloader/payload进行引导或者烧录, 开发板厂家也会做好各种Bootloader用SD卡引导, 绝大多数产品研发都不需要接触烧写步骤, 哪怕你改了Bootloader也只写一下SD卡即可, 以后手机领域的开发人员可能逐渐就远离五花八门的各种仿真器了吧. 可能现在就工控, 穿戴, 还有各种xx杯比赛需要写裸机程序的, 这些人还会手里拿着一大把各种厂家的仿真器.

    回到测试点的话题, 能找到定义最好的办法就是有维修手册或者电路图泄露. 当然这些只能出现在很流行但又不是最新的设备上.

    一些小众的, 或者太新太老的就只能自己动手了. 如果是双面板, 可能我们把所有元件都吹下来, 进行抄板是最快的方式. 如果是四层乃至6,8层板, 个人来搞就很容易磨废掉. 这时候我们可以不打磨只测量. 当然, 我们关心的测试点是属于cpu/nand的部分.

    如果这些芯片是BGA/wlcsp的, 需要预先拆下.

 

图3-1 拆掉后的eMCP(左)和CPU(右)

    测试点是什么呢, 看图说话, 右边CPU白色方块旁边三个金色的铜箔就是. 说到这种BGA的布线, 肯定有人会看到布线规范里面要求把所有pad全部fanout, 不管你产品运行用的着用不着, 而是要留着品质测试(测试焊接质量?), 但是显然我国工程师擅长的是把6层板的东西缩到4层, 4层缩到双面, 所以不要太指望一些我们需要的引脚厂家帮我们做了测试点.

    实际如果没有引出我们也有办法, 不过这需要我们去验证到底哪些引脚已经引出了. 刚才提到过抄板和自己找, 这里就演示一下自己找的例子. 这里面有个技巧, 可以焊接一个8pin排针, 然后对应8条不同颜色硅胶线(不要太粗30AWG即可), 再用一个8pin排座都短接在一起.

    将这些排线空闲的一端选8个焊接到靠近的8个测试点上, 插到排座上再用万用表通断档在芯片引脚上扫过, 发现有导通的, 拔掉排座, 再测导通的是8pin里面哪一色的pin.

    因为有颜色区分, 抄的时候不容易弄错临近的点. 甚至我考虑过用74系列+语音芯片简单做一个多pin测试的, 探通后自动扫描每一pin.

    如果是LQFP这样封装的, 或者说测试点数量多于关注的引脚位置, 还可以8条导线焊接到cpu/flash引脚上, 反过去扫测试点.

    我们把板子放到扫描仪上扫出背面图, 然后在Photoshop里面按照颜色选取, 将所有裸露的铜箔部分标记为绿色, 然后打印在A4纸上.

    然后按照上述办法, 每找到一个跟cpu/eMCP芯片存在导通的测试点, 就用笔记录在该测试点的位置. 遇到对应的是JTAG或者可能存在uart复用的端口, 我们再最终记录测试.

 

图3-2整理中的标记示例


 

图3-3 焊接好的JTAG测试点

    当然工厂的生产流程, 是会有制作一个测试冶具(支架), 需要在线测试的位置安装探针, PCBA放置固定后, 自动进行质检的.

    我们自己用直接用细一些的导线或者漆包线焊上即可(我推荐铁氟龙线和硅胶线驳接的方式)


    接着是找UART测试点, 前面我们分析过串口应该是GPIO109和GPIO110, 但在第一次测试中, 我们扫遍了电路板正面所有测试点都没有导通的. 后来测试键盘矩阵揭开窝仔片后还有一些触点也暴露出来, 果不其然, 其中正好有跟cpu的这2个脚位相通的.

    应该只是凑巧被贴住了, 感觉电路板挂在测试架上时候是没有贴这块键盘的.

图3-3 找到的UART测试点, 在数字键3的右侧.


    附录2

    我们需要什么样的装备

    基础维修工具

    焊台/烙铁, 常见杜邦线排针排母压线钳胶壳等等.

    如果需要拆焊和焊接smt的IC, 最好有热风枪.

    如果需要处理引脚较多的BGA, 或者其他尺寸大的ic, 还需要一个预热台.

    万用电表有基础通断功能的就可以.

    如果要抓非标准协议, 可能需要逻辑分析仪, 这种国内有很多山寨selease的, 也有一些是原创的. 如果要分析模拟信号, 还需要示波仪. 别买玩具款的就行, 深度不足的话可能根本无法抓到异常波形.

    调试用的仿真器设备

    一般来说, soc厂家自己会有生产仿真器, 但有些原厂仿真器便宜, 有的奇贵, 可能上万, 还不卖给个人.

    不过不用怕我们有强大的山寨版, 甚至很多有1:1的高仿, 直接用官方固件能够自动升级的. 不喜欢盗版的, 也可以考虑第三方的仿真器, 如果不是那么罕见, 还可以找有免费电路图提供的, 自己制作.

    也有的开发板厂商, 配套仿真器是自己生成的, 这一些有的是开源方案, 有的是简易的LPT方案, 有些国内厂家也直接给你上山寨的.

    比如Marvell的XDB, 现在是想买也买不到. 它很多东西包括资料也只给单位客户, 个人连开发社区都不能注册.

    不过因为前些年PXA在手机中流行过, 市面上也出现了一些可以调试PXA的JTAG Adapter.


图4-1 Bus Blaster v4

    这是基于FT2232H+CPLD的可编程buffer方案, 软件支持是openocd, 但bug很多. 开源的就这样.

图4-2 并口简易JTAG

    WDT LPT Jtag, 固定buffer方案. 可以配合jflashmm烧写PXA320的闪存. 这是某韩国开发板厂家Hybus修改的烧写程序和配套的电路.

    当然我们万能的山寨jlink, 配合openpxa补丁, 也可以用来调试不过我一定要说, 开源的东西我还没遇到在我手里不出bug的.

    它调试起来各种bug, dump nand也数据不对. 我脑海中已经出现项目的作者"哎哟编译通过了连接成功了可以发布了"的画面.

 

图4-3 使用openocd+山寨Jlink-v8连接bambook

    sudo openocd -f interface/jlink.cfg -f target/pxa3xx.cfg -c "adapter_khz 1000" -c "nand device pxa3xx.flash pxa3xx pxa3xx.cpu"

 

图4-4 使用山寨JLink-v8+openocd读取PXA310的nor flash闪存.

    nand dump offset size file.bin

    其他支持PXA的硬件我还找到有H-Jtag, banyan-U, 其中banyan-U我测试过, 也有一些问题.

    还有一个是JtagKey, 同样基于FT2232的, 开发板厂商Toradex的一些烧录和调试软件支持这个转换器. 这个东西我们可以自己造, 网上有很多电路图.


    附录3 无用信息删除



        下期预告:

    如何防止二进制代码被dump出来

    对于外置flash的机型来说, 除非cpu本身支持代码加密, 否则拆下nor/NAND/iNAND以后, 放进编程器一读, bootloader等内容也就直接到手了, 也可以拆下来焊到转接板或者飞线出来用读卡器或者单片机dump,  当然这些都是在你还没有能直接dd mtd分区手段之前的敲门砖.

    对于内置flash的来讲(例如大部分mcu), 我举个科普的例子(但不科学), 比如你有台电脑, 你BIOS里禁了USB接口, BIOS设置了密码, 以防止里面资料泄露. 但你放在自己家那是有效的, 如果放在别人家呢? 要么我可以CMOS放电, 要么我可以直接拔了你的硬盘来dump啊. 这就是传说中的开盖读取. 曾经还有传言有厂家用EEPROM保存代码, EPROM保存secure bit, 结果连个人摊子都能用紫外线照一下就能读了, 熔丝修复打金线什么的统统不用. 这个传言是真是假就不说了, 据我所知现在的单片机内部flash乱序存储, 加密都已经很普遍了, 但对于要克隆你产品的来说, 唯一的区别就是解密工作室开价不同, 只能让人多花钱, 难以让人多花时间.


        感谢列表

    2 感谢Zdi勘误

   


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

上传的附件:
收藏
点赞3
打赏
分享
打赏 + 2.00雪花
打赏次数 2 雪花 + 2.00
 
赞赏  CCkicker   +1.00 2017/05/08
赞赏  kanxue   +1.00 2017/05/04
最新回复 (33)
雪    币: 3747
活跃值: (3202)
能力值: ( LV15,RANK:500 )
在线值:
发帖
回帖
粉丝
曾半仙 12 2017-4-5 21:53
2
0

晕倒, 发完发现似乎帖子长度还有限制. 接上.

PS: 不是限制, 是某个词语后面的内容都会被截掉, 那就是USB的(D

雪    币: 26415
活跃值: (18468)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2017-4-5 21:59
3
0
哇,曾大仙回来了,感谢分享!

最后几张图没显示出来。
雪    币: 3747
活跃值: (3202)
能力值: ( LV15,RANK:500 )
在线值:
发帖
回帖
粉丝
曾半仙 12 2017-4-5 22:05
4
0
坛主顶帖速度一流!  前几年受过打击,  好久没搞这些了
雪    币: 26415
活跃值: (18468)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2017-4-5 22:11
5
0
曾半仙 坛主顶帖速度一流! 前几年受过打击, 好久没搞这些了
过段时间,喊大伙一起聚聚
雪    币: 216
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Gkey 2017-4-6 09:01
6
0
顶一个
雪    币: 6767
活跃值: (4417)
能力值: (RANK:600 )
在线值:
发帖
回帖
粉丝
gjden 14 2017-4-6 10:38
7
0
感谢分享,信息量很大,欢迎大牛归来!
雪    币: 133
活跃值: (233)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
不追浮云的人 2017-4-6 14:39
8
0
思路清晰,赞
雪    币: 2581
活跃值: (825)
能力值: ( LV12,RANK:1010 )
在线值:
发帖
回帖
粉丝
loudy 10 2017-4-6 17:12
9
0
mark,感谢分享
雪    币: 11421
活跃值: (2760)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
suiwinder 2017-4-6 17:31
10
0
破解是个力气活啊,现在高人很少露面了。
雪    币: 493
活跃值: (3657)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
龙飞雪 2017-4-6 18:03
11
0
厉害
雪    币: 16429
活跃值: (59390)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2017-4-6 20:57
12
0

这就是传说中的牛人?
改天登门拜访,做个采访

雪    币: 1364
活跃值: (3104)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
方向感 2017-4-7 11:48
13
0
高手,学习
雪    币: 1482
活跃值: (1935)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
huluxia 2017-4-8 09:34
14
0
进来膜拜
雪    币: 2925
活跃值: (1323)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
MsScotch 2017-4-11 10:13
15
0
下载学习,感谢分享,
雪    币: 312
活跃值: (113)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
金箍棒 2017-4-11 11:24
16
0
膜拜
雪    币: 6004
活跃值: (1925)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
shuichon 2017-4-18 23:52
17
0
写的很是深入浅出,即便不是该行业的人,也能从中有所收获,很不错的帖子,希望论坛这类帖子越来越多。
雪    币: 409
活跃值: (1531)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Thead 2017-4-20 21:06
18
0
膜拜大佬,想想以前倒腾树莓派的日子……
雪    币: 26415
活跃值: (18468)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2017-5-4 17:03
19
0
测试下赞赏功能
雪    币: 38
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
X虚影 2017-5-5 18:33
20
0
简直太强了.
雪    币: 37
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xlixiang 2017-5-15 10:20
21
0
涨姿势了,顶一下
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
恶露你去 2017-5-19 17:42
22
0
老哥能不能给个普通用户可以用的方案,,,好久没在网上看到这么有技术性的文章了
雪    币: 34
活跃值: (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
likeseesnow 2017-6-10 21:36
23
0
看到这么有含金量的硬破帖子,佩服得顶起来!
雪    币: 563
活跃值: (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lixupeng 2017-6-10 22:54
24
0
先收藏!
雪    币: 12
活跃值: (42)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
muzhangfan 2017-6-13 08:22
25
0
厉害,完全看不懂
游客
登录 | 注册 方可回帖
返回