python

Как запатчить 11 разных прошивок и не сойти с ума от разнообразия

  • среда, 2 июля 2014 г. в 03:10:30
http://habrahabr.ru/post/228317/

Если какая-либо операция превращается в рутину — автоматизируй её. Даже если времени потратишь больше — зато ты занимался не рутиной, а интересным делом. Именно под этой вывеской вместо того, чтобы просто запатчить новые 11 версий rtsp_streamer'а для камер от TopSee, решил нарисовать автопатчер. Идеальным языком для любых наколенных изделий я считаю питон — достаточно лаконично, достаточно жестко по читабельности (хотя я всё равно умудряюсь сделать его не читаемым). В общем, сейчас я расскажу, как с помощью палки и верёвки за один вечер научиться рисовать автопатчеры.

Итак, главные требования к скриптам на коленке — максимальное соответствие ожиданиям. Он должен либо работать, либо сообщать что что-то не то. Главная ошибка таких скриптов — это какие-либо действий без проверок на соответствие ожиданиям. Так как иначе можно не заметить что что-то поменялось и требуется вмешательство человека.

Итак, вспомним что мы делали в прошлый раз, и пройдёмся еще раз весь путь вручную:
1) Грузим файл в дизассемблер
2) Находим функцию fctnl
3) Проходим по вызовам в поисках использования fcntl с O_NONBLOCK — находим две функции, makeSocketBlocking и makeSocketNonBlocking
Скрытый текст
Boolean makeSocketNonBlocking(int sock) {
#if defined(__WIN32__) || defined(_WIN32)
  unsigned long arg = 1;
  return ioctlsocket(sock, FIONBIO, &arg) == 0;
#elif defined(VXWORKS)
  int arg = 1;
  return ioctl(sock, FIONBIO, (int)&arg) == 0;
#else
  int curFlags = fcntl(sock, F_GETFL, 0);
  return fcntl(sock, F_SETFL, curFlags|O_NONBLOCK) >= 0;
#endif
}

Boolean makeSocketBlocking(int sock) {
#if defined(__WIN32__) || defined(_WIN32)
  unsigned long arg = 0;
  return ioctlsocket(sock, FIONBIO, &arg) == 0;
#elif defined(VXWORKS)
  int arg = 0;
  return ioctl(sock, FIONBIO, (int)&arg) == 0;
#else
  int curFlags = fcntl(sock, F_GETFL, 0);
  return fcntl(sock, F_SETFL, curFlags&(~O_NONBLOCK)) >= 0;
#endif
}

4) Ищем в коде функцию sendPacket() (по прошлому мы знаем, что в неё вписаны отладочные printf'ы, по которым найти не составляет труда)
Скрытый текст
Boolean RTPInterface::sendPacket(unsigned char* packet, unsigned packetSize) {
  Boolean success = True; // we'll return False instead if any of the sends fail

  for (tcpStreamRecord* streams = fTCPStreams; streams != NULL;
       streams = streams->fNext) {
    if (!sendRTPOverTCP(packet, packetSize, streams->fStreamSocketNum, streams->fStreamChannelId)) {
      printf("%s(): ", "sendPacket");
      printf("sendRTPOverTCP failed, sock: %d, chn: %d\r\n", streams->socket, streams->fStreamChannelId);
      success = False;
    }
  }

  return success;
}

5) Патчим функцию
Скрытый текст
Boolean RTPInterface::sendPacket(unsigned char* packet, unsigned packetSize) {
  Boolean success = True; // we'll return False instead if any of the sends fail

  for (tcpStreamRecord* streams = fTCPStreams; streams != NULL;
       streams = streams->fNext) {
    makeSocketBlocking(streams->socket);
    Boolean res = sendRTPOverTCP(packet, packetSize, streams->fStreamSocketNum, streams->fStreamChannelId);
    makeSocketNonBlocking(streams->socket);
    if (!res) {
      success = False;
    }
  }

  return success;
}


Итак, вот это всё нам нужно автоматизировать так, чтоб запустил и получил. Причем, в разных версиях прошивки используются где-то fcntl, где-то fcntl64; в зависимости от опций компилятора разные регистры использованы, небольшие другие отличия по оттранслированному коду наблюдаются.

Итак, начнём писать наш скрипт. Понятное дело, так как патчить будем разные версии, имя патчуемого файла передавать надо аргументом. Значит скрипт начался:
import sys
fname = sys.argv[1]


Так как мы делаем скрипт для себя, файлы небольшие (~ метр весом), памяти много, поэтому не будем заморачиваться с беготнёй по файлу — загрузим всё сразу в память:
f = open(fname, "r+b")
f.seek(0, 2)
size = f.tell()
f.seek(0, 0)
fw = f.read(size)
f.close()


Начнём искать функции makeSocketBlocking/makeSocketNonBlocking. Функции fcntl используются много где, так что реальная зацепка — это O_NONBLOCK (=0x800). С другой стороны, это библиотечные функции, которые никто не трогает, они идут подряд, можно просто найти функции «как есть»:
.text:0003C554             makeSocketBlocking
.text:0003C554 10 40 2D E9                 STMFD   SP!, {R4,LR}
.text:0003C558 03 10 A0 E3                 MOV     R1, #F_GETFL    ; cmd
.text:0003C55C 00 20 A0 E3                 MOV     R2, #0
.text:0003C560 00 40 A0 E1                 MOV     R4, R0
.text:0003C564 32 39 FF EB                 BL      fcntl
.text:0003C568 04 10 A0 E3                 MOV     R1, #F_SETFL    ; cmd
.text:0003C56C 02 2B C0 E3                 BIC     R2, R0, #O_NONBLOCK
.text:0003C570 04 00 A0 E1                 MOV     R0, R4          ; fd
.text:0003C574 2E 39 FF EB                 BL      fcntl
.text:0003C578 00 00 E0 E1                 MVN     R0, R0
.text:0003C57C A0 0F A0 E1                 MOV     R0, R0,LSR#31
.text:0003C580 10 80 BD E8                 LDMFD   SP!, {R4,PC}
.text:0003C580             ; End of function makeSocketBlocking
.text:0003C584             makeSocketNonblocking                   ; CODE XREF: sub_43524+40p
.text:0003C584                                                     ; .text:00043608p ...
.text:0003C584 10 40 2D E9                 STMFD   SP!, {R4,LR}
.text:0003C588 03 10 A0 E3                 MOV     R1, #3          ; cmd
.text:0003C58C 00 20 A0 E3                 MOV     R2, #0
.text:0003C590 00 40 A0 E1                 MOV     R4, R0
.text:0003C594 26 39 FF EB                 BL      fcntl
.text:0003C598 04 10 A0 E3                 MOV     R1, #4          ; cmd
.text:0003C59C 02 2B 80 E3                 ORR     R2, R0, #0x800
.text:0003C5A0 04 00 A0 E1                 MOV     R0, R4          ; fd
.text:0003C5A4 22 39 FF EB                 BL      fcntl
.text:0003C5A8 00 00 E0 E1                 MVN     R0, R0
.text:0003C5AC A0 0F A0 E1                 MOV     R0, R0,LSR#31
.text:0003C5B0 10 80 BD E8                 LDMFD   SP!, {R4,PC}
.text:0003C5B0             ; End of function makeSocketNonblocking


Скопируем этим опкоды в лоб, и заменим все параметры, которые отличаются от версии, адреса, настроек компилятора и тд.
blockMask = """
  ; makeSocketBlocking
 mm
  ?? ?? 2D E9 ; STMFD SP!, ....
  03 10 A0 E3 ; MOV R1, #3 ; cmd
  00 20 A0 E3 ; MOV R2, #0
  00 ?? A0 E1 ; MOV Rx, R0 ; save fd
  ?? ?? FF EB ; BL fcntl
  04 10 A0 E3 ; MOV R1, #4 ; cmd
  02 2B C0 E3 ; clear O_NONBLOCK
  ?? 00 A0 E1 ; restore fd
  ?? ?? FF EB ; BL fcntl
  00 00 E0 E1 ; MVN R0, R0
  A0 0F A0 E1 ; MOV R0, R0,LSR#31
  ?? ?? BD E8 ; LDMFD SP!, ....
 mm
  ; makeSocketNonblocking
  ?? ?? 2D E9
  03 10 A0 E3
  00 20 A0 E3
  00 ?? A0 E1
  ?? ?? FF EB
  04 10 A0 E3
  02 2B 80 E3
  ?? 00 A0 E1
  ?? ?? FF EB
  00 00 E0 E1
  A0 0F A0 E1
  ?? ?? BD E8
"""


Я дополнительно поставил метки («mm») к началам функций; все не-опкоды закомментировал ";" и так и оставил в коде как есть (на будущее, чтоб вспомнить что есть что).
Теперь надо превратить эту строку в маску для поиска. Разумеется, вручную заниматься поиском совершенно не охото, поэтому искать будем через регекспы, благо они отлажены и оптимизированы по самое не балуйся. Да и весь файл мы прочитали в память как одну большую строку — что тоже удобно. Посему пишем функцию, которая преобразует эту маску в регексп:
import re
def maskToRegex(mask):
    mask = re.sub( ";.*$", "", mask, flags=re.MULTILINE)
    mask = re.sub( "\s+", "", mask, flags=re.MULTILINE)
    masks = re.findall( "..", mask)
    rgx = ""
    for m in masks:
        if m == "??":
            rgx += "."
        elif m == "mm":
            rgx += "()"
        else:
            rgx += "\\x"+m
    return rgx

Поведение простое — удаляем все комментарии (всё от; до конца строки), удаляем все пробелы, оставшиеся символы рубим на пары, и смотрим — если это ?? — то заменяем на точку (любой символ), если «mm» — вставляем метку, для которой будем запоминать положение, иначе генерируем код символа (приписав "\x" к этой паре).

Итак, у нас есть маска и мы можем получить из неё регексп, давайте найдём наконец эти функции:
### 1. Find offset of makeSocketBlocking and makeSocketNonblocking
makeBlock = None
makeNonBlock = None
for find in re.finditer(maskToRegex(blockMask), fw, re.DOTALL):
    if makeBlock is None and makeNonBlock is None:
        makeBlock = find.start(1)
        makeNonBlock = find.start(2)
        print "Found makeNonBlock at ", hex(makeNonBlock)
        print "Found makeBlock at ", hex(makeBlock)
    else:
        print "Non-unqiue makeNonBlock/makeBlocking functions found"
        break
if makeBlock is None or makeNonBlock is None:
    print "makeNonBlock/makeBlocking functions not found"


В данном коде два важных момента. Во-1х, глобальный, соответствие ожиданиям. Мы проверяем, что под эту маску попал только один блок кода, и что он действительно попал. На время отладки добавляем распечатку смещений найденных мест. Во-2х, важно не забыть про re.DOTALL, чтобы под точку попадал действительно любой байт, мы работаем с бинарной строкой.

Итак, теперь нам надо найти функцию sendPacket. Заглянем в дизассемблятину:
.text:0006C9A0             SendPacket                              ; CODE XREF: sub_69FB8+144p
.text:0006C9A0                                                     ; .text:0006D144p ...
.text:0006C9A0 F0 4F 2D E9                 STMFD   SP!, {R4-R11,LR}
.text:0006C9A4 00 60 A0 E1                 MOV     R6, R0
 .....
.text:0006CB78 C3 FF FF 1A                 BNE     loc_6CA8C
.text:0006CB7C E2 FF FF EA                 B       loc_6CB0C
.text:0006CB7C             ; End of function SendPacket

.text:0006CB80 BC 92 0A 00 off_6CB80       DCD aS_10               ; DATA XREF: SendPacket+7Cr, SendPacket+F0r
.text:0006CB80                                                     ; "%s():  "
.text:0006CB84 E4 6B 0A 00 off_6CB84       DCD aSendpacket         ; DATA XREF: SendPacket+80r, SendPacket+F4r
.text:0006CB84                                                     ; "sendPacket"
.text:0006CB88 00 9B 0A 00 off_6CB88       DCD aSendrtpovert_0     ; DATA XREF: SendPacket+98r
.text:0006CB88                                                     ; "sendRTPOverTCP failed, sock: %d, chn: %"...
.text:0006CB8C 2C 9B 0A 00 off_6CB8C       DCD aRemovestreamso     ; DATA XREF: SendPacket+110r
.text:0006CB8C                                                     ; "removeStreamSocket, sock: %d, chnid: %d"...


Ага, ссылка на эту строку лежит сразу после кода функции. Значит, чтобы найти функцию, нам надо: найти строку с именем функции (спасибо отладочным макросам, она лежит отдельной независимой строкой), преобразовать смещение в адрес, найти этот адрес, в коде, от этого адреса отмотать вверх до инструкции STMFD SP!, {..., LR}.
Сразу возникает вопрос — найти строку не проблема, а вот как преобразовать смещение в файле в виртуальный адрес? Не заниматься же разбором файла вручную. Тут на помощь приходит всемогущий гугль: есть пакет pyelftools. Так что «pip install pyelftools», и выкуриваем приложенную документацию. Там что-то ничего нет полезного. ОК, лезем тупо в файл elffily.py и смотрим что там есть вкусного. Находим там функцию, которая делает обратную задачу — находит смещение по виртуальному адресу:
    def address_offsets(self, start, size=1):
        """ Yield a file offset for each ELF segment containing a memory region.

            A memory region is defined by the range [start...start+size). The
            offset of the region is yielded.
        """
        end = start + size
        for seg in self.iter_segments():
            if (start >= seg['p_vaddr'] and
                end <= seg['p_vaddr'] + seg['p_filesz']):
                yield start - seg['p_vaddr'] + seg['p_offset']


Всё понятно, легким движением руки превращаются данные брюки в элегантные шорты:
# удаляем f.close(), и на её месте пишем
from elftools.elf.elffile import ELFFile
f.seek(0, 0)
Elf = ELFFile(f)

def offToVA(offset):
    for k in Elf.iter_segments():
        if offset >= k['p_offset'] and offset <= k['p_offset']+k['p_filesz']:
            return k['p_vaddr']+(offset-k['p_offset'])


Теперь мы можем найти строку, и место её использования:
    s = "sendPacket"
    ## Find string itself
    offStr = re.findall(s+"\x00", fw)
    if len(offStr)==1:
        offStr = re.search(s+"\x00", fw)
        offStrVA = offToVA(offStr.start(0))
        print "offStr["+s+"] =", hex(offStrVA)
    elif len(offStr)==0:
        print s, "string marker not found"
    else:
        print "Too many", s, "string markers found"


Тут я по лени использовал другой метод проверки ожиданий — просто сперва пытаюсь найти все, и считаю, что всё ок если она одна. Иначе сообщаю об ошибке. Тоже метод не удобный, + лишние поиски, зато мне лично понятнее, так что эксперименты с finditer повторять не буду.
Ну и теперь найдём смещение, где используется строка:
    ## Find offset to string
    reStrLink = "\\x%02X\\x%02X\\x%02X\\x%02X" % (
                (offStrVA)%256,
                (offStrVA/256)%256,
                (offStrVA/256/256)%256,
                (offStrVA/256/256/256)%256 )
    offLink = re.findall(reStrLink, fw)
    if len(offLink)==1:
        offLink = re.search(reStrLink, fw)
        offLink = offLink.start(0)
        if DEBUG: print "offLink["+s+"] = ", hex(offToVA(offLink))
        return offLink
    else:
        print "Can't find usage of", s


Как вы уже догадались, вместо s = "sendPacket" на самом деле написано def findStringLink(s):, так как понадобится нам найти функцию не один раз.
Ну и теперь нам надо найти еще начало функции — прошагаем назад по 4 байта в поисках STMFD SP!, {..., LR}. Из-за лени, я ограничился поиском STMFD SP!, {...} (чтобы не анализировать биты). Причина — если найти не начало функции, дальше всё равно поиск поломается, о чем я узнаю, и тогда уже смогу решить как лучше чинить.
# Find previous function begin offset (nearest STMFD SP!, {...} instruction)
def findFuncBegin(offset, maxLen = 0x1000):
    maxStart = max(0, offset-maxLen)
    offset -= 4
    while offset > maxStart:
        if fw[offset+2:offset+4]=="\x2D\xE9":
            return offset
        offset -= 4
    return None


Итак, у нас есть всё нужное, наконец-то найдём функцию sendPacket:
### 2. Find sendPacket function
sendPacketEnd = findStringLink("sendPacket")
sendPacketStart = findFuncBegin(sendPacketEnd)
if sendPacketStart is not None:
    print "sendPacketStart = ", hex(offToVA(sendPacketStart))
else:
    print "Can't find start of sendPacket"


Теперь мы приблизились к самому интересному — нам надо найти цикл внутри sendPacket, и убедиться что это он. Маску составляем уже как научились ранее:
sendLoopMask = """
  ?? 00 00 EA ;               B       loopBody
              ; ---------------------------------------------------------------------------
 mm           ;loopNext                               ; CODE XREF: SendPacket+74j
  ?? ?? ?? E5 ;               LDR     R4, [R4,#4] (or R5)
  00 00 ?? E3 ;               CMP     R4, #0      (or R5)
  ?? 00 00 0A ;               BEQ     loc_6CA74
              ;loopBody                               ; CODE XREF: SendPacket+4Cj
 mm           ;                                       ; SendPacket+D0nj
  ?? ?? A0 E1 ;               MOV     R3, R4      (or R5)
  ?? ?? A0 E1 ;               MOV     R1, R5
  ?? ?? A0 E1 ;               MOV     R2, R7
  ?? ?? A0 E1 ;               MOV     R0, R6
 mm
  ?? ?? ?? EB ;               BL      SendRTPOverTCP
  00 00 50 E3 ;               CMP     R0, #0
  F5 FF FF AA ;               BGE     loopNext
"""


Но нам надо бы проверить, что BL внутри цикла — именно к SendRTPOverTCP, иначе это либо поменяли функцию сильно, или цикл уже был пропатчен нами, поэтому найдём еще и SendRTPOverTCP:
### 3. Find sendRTPOverTCP function
sendRTPOverTCPStart = findFuncBegin(findStringLink("sendRTPOverTCP"))
if sendRTPOverTCPStart is not None:
    print "sendRTPOverTCPStart = ", hex(offToVA(sendRTPOverTCPStart))
else:
    print "Can't find start of sendPacket"


Так, но мы должны уметь проверить туда ли ссылка. Все переходы в ARM не абсолютные, а относительные, плюс адреса задаются исчисляя +2 инструкции от текущей, да еще задаются в квантах инструкций (то есть делённая на 4). В общем, опкод рисуется как-то так:
TargetAddr = Opcode*(4 bytes/word) + CurAddr + 8

В итоге, получаем такие функции, для вычисления адреса, куда ссылается некая инструкция, и для вычисления какой будет операнд инструкции по некому адресу для перехода куда надо:
def BinArg(off):
    return ord(fw[off])+ord(fw[off+1])*256+ord(fw[off+2])*256*256
def ArgToBin(arg):
    return chr(arg%256)+chr(arg/256%256)+chr(arg/256/256%256)

def cmdTargetOffset(cmdoff):
    d1 = BinArg(cmdoff)
    if d1 >= 0x800000: d1 -= 0x1000000
    return (cmdoff+(d1+1)*4+4)

def cmdTargetArg(cmdoff, target):
    d1 = (target - (cmdoff+4))/4 - 1
    if d1 < 0: d1 += 0x1000000
    return d1


Теперь, когда кирпичи отложены, построим очередную переборку:
### 4. find loop in sendPacket
sendPacketLoopRx = maskToRegex(sendLoopMask)
sendPacketLoop = re.findall(sendPacketLoopRx, fw, re.DOTALL)
if len(sendPacketLoop)==1:
    sendPacketLoop = re.search(sendPacketLoopRx, fw, re.DOTALL)
    sendPacketLoopNext = sendPacketLoop.start(1)
    sendPacketLoopBL = sendPacketLoop.start(3)
    sendPacketLoop = sendPacketLoop.start(2)
    if DEBUG: print "sendPacket loop at ", hex(offToVA(sendPacketLoop))
elif len(sendPacketLoop)==0:
    print "Loop inside sendPacket not found"
else:
    print "Non-unqiue loops masks for sendPacket found"
## 4.1. check that loop link is really to sendRTPOverTCP
if cmdTargetOffset(sendPacketLoopBL) != sendRTPOverTCPStart:
    print "Loop's first call is not sendRTPOverTCP"
if cmdTargetArg(sendPacketLoopBL, sendRTPOverTCPStart)!=BinArg(sendPacketLoopBL):
    print "BUG! cmdTargetArg inconsistent with cmdTargetOffset!" 
if ArgToBin(cmdTargetArg(sendPacketLoopBL, sendRTPOverTCPStart)) != fw[sendPacketLoopBL:sendPacketLoopBL+3]:
    print "BUG! ArgToBin inconsistent with cmdTargetOffset!"


Итак, мы нашли цикл, проверили что BL в нём ссылается именно на sendRTPOverTCP, а заодно проверили функции адресов что они консистентны. Это всё позволяет защититься от опечаток и избавиться от лишних покрытий тестами функций — мы пишем скрипт, а не программный продукт, поэтому только необходимый максимум телодвижений.

Но для того, чтобы пропатчить, нам надо добавить 6 новых инструкций, а для этого нам надо место. Так как у нас в цикле есть два лишних принфа, как мы уже знаем, их-то мы и используем. Первый принтф 5 инструкций, значит, затирать придётся оба. Для этого их придётся найти и убедиться что это они:
### 5. Find next two printfs
printf1 = sendPacketLoopBL+4
while printf1 < sendPacketEnd:
    if fw[printf1+3]=="\xEB":
        printfStart = cmdTargetOffset(printf1)
        break
    printf1 += 4
printf1 += 4
printf2 = printf1 + 4
while printf2 < sendPacketEnd:
    if fw[printf2+3]=="\xEB":
        if cmdTargetOffset(printf2) != printfStart:
            print "ERROR! After loop not two printfs!"
        break
    printf2 += 4
printf2 += 4
if (printf1-sendPacketLoop)/4-7 != 5:
    print "WARN! First printf not 5 instructions"
if (printf1-sendPacketLoop)/4-7 > 5:
    printf2 = printf1 # no need to cleanup 2nd printf

Опять же, идём простейшим путём — берём два ближайших BL после BL sendRTPOverTCP, проверяем что они оба ссылаются на одну функцию, и проверяем их размеры. Если что либо пойдёт не так — сразу ругнёмся. Если же всё соответствует ожиданиям — то всё хорошо.

Ну а теперь собрать всё это вместе, добавить дури три ведра, и сгенерировать патч:
### 6. Generate new loop body
PatchSendPacket = ""

## 6.1. LDR R0, Socket(Rx#8)
ldrSock = "\x08\x00"+fw[sendPacketLoopNext+2]+"\xE5"
PatchSendPacket += ldrSock

## 6.2. BL makeSocketBlocking
tgtSocketBlock = cmdTargetArg(sendPacketLoop+len(PatchSendPacket), makeBlock)
PatchSendPacket += ArgToBin(tgtSocketBlock)+"\xEB"

## 6.3. Copy 4 MOVs
PatchSendPacket += fw[sendPacketLoop:sendPacketLoopBL]

## 6.4. BL sendRTPOverTCP
tgtSendRTPOverTCP = cmdTargetArg(sendPacketLoop+len(PatchSendPacket), sendRTPOverTCPStart)
PatchSendPacket += ArgToBin(tgtSendRTPOverTCP)+"\xEB"

## 6.5. STMFD   SP!, {R0}
PatchSendPacket += "\x01\x00\x2D\xE9"

## 6.6. LDR R0, Socket
PatchSendPacket += ldrSock

## 6.7. BL makeSocketNonBlocking
tgtSocketNonBlock = cmdTargetArg(sendPacketLoop+len(PatchSendPacket), makeNonBlock)
PatchSendPacket += ArgToBin(tgtSocketNonBlock)+"\xEB"

## 6.8. LDMFD   SP!, {R0}
PatchSendPacket += "\x01\x00\xBD\xE8"

## 6.9. CMP     R0, #0
PatchSendPacket += "\x00\x00\x50\xE3"

## 6.A. BGE     loopNext
tgtLoopNext = cmdTargetArg(sendPacketLoop+len(PatchSendPacket), sendPacketLoopNext)
PatchSendPacket += ArgToBin(tgtLoopNext)+"\xAA"

## 6.B. Fill up to printf2 with NOPs
Nops = (printf2 - (sendPacketLoop + len(PatchSendPacket))) / 4
PatchSendPacket += "\x00\x00\xA0\xE1" * Nops

## 6.C. Save generated patch
patches = []
patches.append( (sendPacketLoop, PatchSendPacket) )
print "Successfully patched"


Теперь, когда у нас есть все патчи (пока аж целый один, но всё вперде), сгенерируем новый файл:
### FIN: save patched file
if True:
    f = open(fname+".fixed", "w+b")
    patches.sort()
    last = 0
    for p in patches:
        f.write( fw[last:p[0]] )
        f.write( p[1] )
        last = p[0]+len(p[1])
    f.write(fw[last:])
    f.close()


Вот так, с помощью палки, верёвки, питона и грамма мозга мы легко и просто запатчили все 11 нужных мне стримера сразу, причем времени ушел один вечер, что примерно равно времени, которое ушло бы на то, чтобы вручную запатчить их все. Вот только при следующем обновлении, уже не потребуется тратить времени вообще!

for k in `(cd todo; ls -1)`; do g=$(echo $k | perl -pe 's/.*?([TV][^-]+).*?-V(2[^_]+).*/$1_$2/'); mv todo/$k .; ../repack/unpack.sh $k; cp $k.unpack/root/opt/topsee/rtsp_streamer rtsp_streamer_$g; done
for k in rtsp_streamer_*.[0-9]; do python tcpfix.py $k; done
...


p.s.: всем, кому что-то еще надо от этих прошивок, выложил скрипты в более удобное место — на гитхаб.