人人都可以拯救正版硬件受害者(Jlink提示Clone)
近來準備搞搞usb,翻出我的正版開發板,啟動正版visualgdb,插上盜版v9, 工程向導識別不到開發板無法下一步,于是我換了一下最新版本的7.58c驅動,打開準備升級一下固件來著,好家伙,直接彈出一個好家伙,大意是:
"現在連接的探頭是個克隆版jlink,在克隆硬件上用我們的軟件既不合理又不合法,請聯系我們并附上截圖。"
雖然后面我發現工程向導從openocd里面間接驅動jlink才能下一步并且成功調試,可這個segger的提示深深的打動了我。
眾所周知,假貨寶上的v9已經不知道出過多少版本了,大的小的,帶殼的裸板的,都是包最新驅動,還真沒遇到segger檢測到的情況,去問了賣家和程序員一樣的回答”我這里好好的”。
得,網上搜也搜不到信息,估計是因為clone提示文字也是全新的,以前好像出錯提示是defective,新的提示是clone。
賣家不管,我自己折騰,首先我懷疑是不是那個簽名問題,于是找了下jlink_x64.dll彈框的地方的函數,發現沒有call它的調用,只有一個傳參引用。不死心,開x64dbg跟了一下,是從線程調用來的。
那么再看這個傳參引用的位置,x64dbg用animate trace記錄了一下會發現他在檢測和比較逗號分隔的特性字符串,比較到特定的字符串”RDI”就跳轉到將這個彈窗函數入參的分支了。如果將字符串比較的jz都給跳過,則不會彈窗。
回到IDA調試和整理下這個函數,首先我們要摸清這個逗號分隔的字符串哪來的,是從一個0x80字節的緩沖區統計來的,每0x10開頭是ascii的部分加到字符串里。
如果是做過山寨版的朋友可能就知道了,這信息是在0800BF20開始的地方,而0800BF00處是序列號。
通過整理和調試, 得出的這個新版驅動的彈窗分支條件依次如下:
0 序列號不能為黑名單里面的那幾個。此條不重要,因為沒人去用那些特殊序列號。
1 新型號不能內置GDBFull,有了直接報錯。所有型號不能內置RDDI,有了直接報錯。
2 硬件版本v9~v11并且序列號開頭為26,5,82的,和版本號v1并且序列號開頭為80的,不許內置JFlash或RDI特性。
看到這里聰明的小伙伴可能要問了,這些不都是專門針對盜版的嗎?但隨著我在網上和閑魚搜集正版的序列號,我發現26開頭是edu的特征,而5開頭是base版的特征,80開頭是edu mini的特征。這里說的開頭就是第8、9位的數字。
山寨版的序號那可就五花八門了,有很多直接-1(4294967295)。在搜索中我還看到小竅門原版edu并 通過addfeature指令增加jflash和rdi特性的,還在論壇看到了Segger去年成立中國部門,壇友表示擔憂,還有最近原版自己加feature的壇友被報告clone的帖子。
略作思考我覺得我破案了,這個嶄新的中國部門怕是讀了論壇的帖子后,把這個當作成果匯報上去了,然后segger程序員一琢磨,來個根據型號限制功能, 可齊活了,這一磚頭主要砸到了買正版edu并且加了內置特性的,盜版序列號-1或者瞎寫的都沒被誤傷。
我們可以選擇補dll,拆機重刷,但我選擇了最程序正義的一種:讓jlink自己去掉feature。
此篇文章也就是從一個和嵌入式開發關系不大的視角上展示如何利用基礎推理能力來撥開云霧得報大仇,閱讀只需要有一定的調試經驗,不需要做漏洞分析。好了,閑話不多說,我們正式就順著這個Feature字符串來摸。因為新版驅動的commander不支持AddFeature指令了,我在老版JlinkARM.dll搜索發現AddFeature命令附近有一個ClearFeatures。
這個命令也是非公開的,和AddFeature、ChangeSN一樣的流程,執行后會把現有ota的Features區域全部修改為0,發送更新ots信息請求讓設備去更新。但我測試了一下設備上的固件卻無法成功的把GDBFull或者JFlash字樣給修改為00。
通過查閱STM32的flash編程手冊PM0059,明確說可以將非0的bit改為0,不需要擦除再改寫。
再看看固件更新ots有啥限制。固件怎么來呢,可以從JLinkARM.dll解。老版本的方法大家都知道了吧,新版本7.2后廠家給一部分固件加了壓縮,我就用另外思路寫了個工具解壓它。
可以參考附件:
pos = 0; otsptr = ots_sig; do { func_readbf00_ots_b700_sig(flashb, pos, 1u); // 如果features都是0, 那么此處可以通過 newb = *otsptr++; if ( (uint8_t)(flashb[0] & newb) != newb ) goto retneg1; ++pos; } while ( pos < 0x900 );
固件中對客戶端發過去的新內容檢查也是檢查沒有出現0變1的bit,然后就送入內存中的函數來修改flash了。
看了下內存中的這個函數,寫的歪七扭八的,除了加了個跳過寫入FF功能,沒有會導致非FF不擦寫的bug。編程手冊上每次寫入都要拉高一次PG,它給簡化為設置一次,循環寫入了。估計其實不需要。
話說其實修改flash的代碼完全沒必要放在內存,因為要修改的目標地址是sector2,和執行的都不在一個sector,而且它末尾還調用了flash里面的memcmp函數判斷寫入是否成功,白隔離了。
flash->CR = 0x200; // PSize=Word(32bit) flash->CR |= 1u; // PG // 跳過新寫入FF請求 while ( *src == -1 ) { ++src; ++dest;prognextword: if ( !--bufword ) goto done; } w32 = *src++; *dest++ = w32; do { wwdg->CR = 0x7F; iwdg->KR = 0xAAAA; SR = flash->SR; } while ( (SR & 0x10000) != 0 ); // FLASH_SR_BSY if ( (SR & 0xFE) == 0 ) goto prognextword; // 沒有5bit錯誤位done: flash->CR = 0x80000000; // lock
修改里面沒有額外的判斷了,沒有什么貓膩,感覺是STM32本身不允許非全1的被改寫?也就是說ST資料寫錯了?
拋開這個不談,我們要還原已經包含ascii的features區域,勢必要重刷該區域所在的sector并且寫入擦除前的內容,這也是flash修改的常規操作了, Features位于0800BF20,所以要擦掉08008000~0800BFFF對應的sector2。
那么原本更新bf00的代碼為啥不去做擦除重寫呢?實際這個函數根據最后一個參數是有擦除sector功能的。估計是廠家設計的時候模擬的單次OTP,不擦硬改,所以一旦不是FF了就不能再次寫入。
如果不大改函數讀出全部再回寫,08008000~0800B6FF這13.75k的內容就會變FF。(后記:實際變了也無所謂,應該這段是空的),所以我選擇寫一段代碼來完成讀出/擦除/寫回操作。
要測試我們寫的代碼是否工作正常,一定得在真實環境下調試,強迫癥半仙表示,還得是由原版的bootloader引導的。接下來我們分析Bootloader。
Bootloader怎么來呢,可以從正版里面提取。可以搜索thxlp的”人人都可以提取V9的Bootloader一文”。
IDE當然選用了IAR,因為經過分析,JLinkV9的固件和Bootloader都是IAR編譯的,最新的758c固件是IAR 6.40.5編譯的,bootloader也應該是6.40.x編譯。
當然了我們用IAR 7.x 8.x都是可以的,segger用的還是stdperiph庫,我們也可以用LL/HAL開發。只要搞清bootloader怎么才肯加載我們刷入的app”分區”就可以了。
bootloader自身和信息存儲被官方安排在08010000之前,app分區從08010000開始到0803FFFF, Vector為08010000,可用內存是20000008開始到20020000,這些在工程選項里面可以設置。
經過分析bootloader引導時候檢查內存20000000處不是0x12344321和app分區的08010210處是不是” J-Link V9”,還有08010000起的2FFFE字節的crc是不是和固件末尾的2字節crc相等。這個crc的算法是CRC-16/KERMIT。
正好IAR的鏈接器有嵌入checksum功能,經過苦苦搜索,我并沒有找到怎么用他的自定義CRC生成KERMIT校驗的選項來。或許我應該寫個小工具自己來postbuild里面修改out文件?
就在我放棄并且補丁了bootloader來調試后,經過一頓嘗試,我找到了這個組合:

按照這個選項設置,鏈接時候生成的crc便可通過bootloader的校驗。
當然不想填充的話,那補bootloader也是可以的,位置如下
ROM:08000E7C 03 D1 BNE locret_8000E86 ; patch to nop!
注意看我源碼里的main.c,
#pragma location=0x08010210
__root const char fwversion[] = "J-Link V9 "
這段可以讓指定地址出現這個字串,滿足bootloader對此處檢測。
我們把bootloader加入到raw binary image里面,設置好符號,段名,然后在icf加入定位指令指示它放置到08000000位置,這樣我們調試app時候, 就會將app和bootloader一起燒錄并調試了。
然后我們還要知道的是因為bootloader本身已經完成了主頻初始化和對應的flash延遲配置,我們這里不用重復初始化。如果是STM32CubeMX建立的模板,不要調用SystemClock_Config。可以參考我提供的工程,下載調試可以看到bootloader放行了我們程序,并且成功的斷在了main函數處。
接下來我們還要完成一個額外的功能才可以安心的往正版里面刷,我們的代碼執行完自身代碼后,需要返回刷機狀態,這樣才可以繼續刷回原版固件。不然每次都進我們固件,回不去了。
通過分析固件和bootloader,只需要我們將20000000處置為12344321這個魔法數值,然后芯片復位就可以。bootloader引導過程檢測到這個標志就會進入恢復狀態,恢復狀態官方工具連上去就會刷固件進去。
經過少量的調試,按照編程手冊做好了擦除功能,重刷后dump可以驗證我們每次燒進去時候序列號和feature在重啟前就被清掉了,同時我們還保留了B700開始的0x800大小的簽名。
萬里長城剩下最后一步,怎么把固件給刷進去,這也難不倒我們,首先我們看原版固件里面進入刷機狀態代碼,也就是設置vectorbase和重啟代碼,發現是在06號指令里面調用的,也就是cmd_06_update_firmware,然后會給主機回應1并且復位讓bootloader進入恢復狀態。
嵌入式開發的內容不是我們的重點,看代碼說明就可以。
bootloader的06號指令的處理程序里,先回應0,然后收一個變長長度,然后收取0x500字節的固件頭部,根據頭部和本身已有固件的特征,判斷是刷機,還是假裝刷機。這個假裝的意思是它照常收你的固件但不刷到flash,還會在收完你查詢固件版本時候,返回給你緩沖的假版本號,增加破解成本,不過毫無用處。
真刷機檢測的內容如下,這四個條件至少需要滿足一個:
1、現有固件Banner不是JLink V9
2、現有固件crc出錯
3、現有固件末尾08003FF00沒有www.segger.com
4、現有固件日期比傳進來的新固件老(或新/老日期任一方為0)
在我們不拆機燒寫的前提下,前三項都無法控制,但第四項可以,我們傳進來的新日期可以比現有版本新或者為0。日期要新好說,把版本號里面的年份往大里改就可以,但日期為0是怎么回事呢,原來segger在這里留了一個后門, 當處理月份縮寫發現月份不在他內置的12種縮寫內,則對應的日期直接默認為0。
這個日期算法就這么任性,精確到分鐘但是它每年是按照12*31天算的。所以我們隨便寫個錯誤月份或者不寫都可以無視版本號,這也是invalidatefw官方降級固件背后的小竅門吧。
刷機工具寫的很快啊,主要操作就是讓jlink進入刷機模式,然后對讀取的固件擴大并填充FF和www.segger.com并計算校驗,最后發給刷機狀態的jlink。如果想要自動化我還可以刷之前存一下現有固件,最后一步再恢復,但懶得寫了。
刷完經過驗證確實達到自動抹除序列號和feature的效果了。通過調整代碼還可以只處理feature到默認列表,不動序號。
搞完這一切要不要挖新坑呢!我去買了正版的v10 edu,下篇折騰折騰v10的吧,不過nxp的文檔和bug我算是領教過了,感覺比stm棘手。
還有一個方向就是我們可以用固件來刷bootloader?搞個自制bootloader能完成引導和刷機就能替代官方的吧,我看了下就實現了10個命令,比完全自制固件要可行?
以下是V10挖坑的更新
我已提取v10的bootloader,分析了下,它啟動固件前多了一些檢測但跟固件相關的還是jlink v10字樣和末尾的crc,其他的判斷正版本來就能通過。V10因為內置bootloader設置了CRP標志,封鎖了串口刷bootloader,但我們用固件來刷bootloader還是可以的。誰讓他把”OTS”和bootloader放在一起呢,架構設計就這樣了,能刷OTS就得讓我刷bootloader。
特別要提醒各位,要從正版里面提取bootloader,因為買了個山寨版拆開發現它用的是LPC4325,它flashA只有0x60000字節,只能放下0x58000大小的固件,而官方固件是0x78000大小。所以它直接沒bootloader直接復位函數跳轉到app分區。
而且就算把官方的bootloader弄個去也要去掉簽名校驗, 還要改刷機部分寫入范圍。可能因為4327/4337/4357等能代用的芯片都被炒高了?我問了下4337剪板都要一百塊。同時我還發現有點山寨版賣家自制了bootloader,這就不一般了,我剛才還說想挖坑做OpenV9自制bootloader呢,別人都做了V10了,佩服。
v10 bootloader和v9的不同主要是廠家不一樣,邏輯上程序員沒動,返回bootloader用同樣的代碼。甚至地址也保留用了20000000。這在lpc43xx上可不是主sram,不曉得他們咋想的。
v10的sn位于1a005e00,feature開始于1a005e20,剛好也是sector2(0x1A004000-0x1A005FFF)。
然后刷正版過程中,又發現刷進去的自制固件不執行,通過修改原版固件刷機測試,發現v10的固件1A0開始有簽名,bootloader會檢查固件開始2A0到固件末尾-0x100字節的內容是否符合該簽名,大膽推測多半是公鑰解密的防止刷入未授權固件。但沒關系啊,我們把代碼塞進這個范圍之外的地方,不就可以免簽名執行了嗎?都不用找漏洞。
為了多壓榨點空間,我禁用了中斷,并且去掉了除了0x20后的所有中斷服務指針,這樣可以把0x20開始指針區域到1A0之間三百多和末尾二百多字節利用起來,感覺寫iap擦除夠用了。懂點單片機開發的小伙伴可能要驚奇了,這vector指針區域也能放代碼執行的嗎,確實可以我都試過了。
最新更新
因為之前說的山寨版無法測試bootloader刷機,咸魚拍正版又沒等到合適的,因此冒險用真機刷機,刷進去才發現不知道是vector指針精簡過頭還是什么原因,執行完demo沒能重啟到bootloader狀態。
因為正版的電路板飛線抹除費事,我把它拆到山寨版的板子上調的時候發現,不是有個給全局變量賦魔法數值的語句嗎,在動了優化選項后被優化沒了。改正后我再刷就不磚了!之前在山寨版上測試擦除demo是可以的。想辦法塞下去就行。有朋友或許要問了,你這才五百多字節,想塞入一些復雜點的算法都不行啊?其實有兩種辦法:
1、這五百多字節塞入一個串口接收循環,把代碼收到sram執行,這芯片有136k sram,完全夠了(比爾蓋茨微笑)。
2、這五百多字節就做一件事,補丁掉bootloader,然后下次刷自制就不校驗簽名了,啥時候不想支持自制了再補回去。

測試成功,整體不到0x180字節塞到頭部刷機就會清掉煩惱的根源了,不過因為要借用官方固件,我得想個辦法合法的操作,或許備份當前固件合成進去, 刷完再恢復原版固件?
順便貼下正版edu調試接口定義:

注意:想調試需要修改CRP標志,也就是flash 1A0002FC位置的四字節值, 官方是0x12345678,修改為FFFFFFFF或者其他非特征值,重啟后才可以調試。
附件:iar自制工程,jlink bootloader和固件的代碼片段,jlinkupdater應用,自制迷你固件。
附錄:我如何判斷固件用的IAR版本呢,主要是看不同iar的庫lz77_init.o里面的iar_lz77_init3函數大小,哪些版本和固件里該函數大小13E吻合。我如何判斷是stdperiph庫是因為在我自動命名vector的函數指針時候,發現有多余的在startup_stm32f205xx.s中不存在的中斷服務。而標準庫沒有細分到子型號,里面有這些中斷服務入口。我如何發現固件用的是IAR編譯的呢,看Reset入口處的特征和main之前要執行的初始化數據函數(__iar_data_init3)。