From 2aacaf66d22ec894e3fd0c1571e4ee5cbdc2202b Mon Sep 17 00:00:00 2001 From: blacktop Date: Mon, 14 Oct 2024 14:54:40 -0600 Subject: [PATCH] feat: add NEW `/macho/info/strings` route to `ipswd` --- api/server/routes/macho/macho.go | 51 ++++++++++++++++++++++++++ api/server/routes/macho/routes.go | 25 +++++++++++++ api/swagger.json | 61 +++++++++++++++++++++++++++++++ cmd/ipsw/cmd/dyld/dyld_macho.go | 47 +++--------------------- cmd/ipsw/cmd/macho/macho_info.go | 47 +++--------------------- internal/commands/macho/macho.go | 51 ++++++++++++++++++++++++++ www/static/api/swagger.json | 61 +++++++++++++++++++++++++++++++ 7 files changed, 261 insertions(+), 82 deletions(-) diff --git a/api/server/routes/macho/macho.go b/api/server/routes/macho/macho.go index b778cfa4b5..41d5583d86 100644 --- a/api/server/routes/macho/macho.go +++ b/api/server/routes/macho/macho.go @@ -22,6 +22,13 @@ type machoInfoResponse struct { Info *macho.File `json:"info"` } +// swagger:response +type machoStringsResponse struct { + Path string `json:"path"` + Arch string `json:"arch"` + Strings map[string]map[string]uint64 `json:"strings"` +} + func machoInfo(c *gin.Context) { var m *macho.File var params Info @@ -58,3 +65,47 @@ func machoInfo(c *gin.Context) { } c.IndentedJSON(http.StatusOK, machoInfoResponse{Path: params.Path, Arch: params.Arch, Info: m}) } + +func machoStrings(c *gin.Context) { + var m *macho.File + var params Info + + if err := c.BindQuery(¶ms); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, types.GenericError{Error: err.Error()}) + return + } + + fat, err := macho.OpenFat(params.Path) + if err != nil { + if err == macho.ErrNotFat { // not a fat binary + m, err = macho.Open(params.Path) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, types.GenericError{Error: err.Error()}) + return + } + defer m.Close() + } else { + c.AbortWithStatusJSON(http.StatusInternalServerError, types.GenericError{Error: err.Error()}) + return + } + } else { // fat binary + defer fat.Close() + if params.Arch == "" { + c.AbortWithStatusJSON(http.StatusInternalServerError, types.GenericError{Error: "'arch' query parameter is required for universal binaries"}) + return + } + for _, farch := range fat.Arches { + if strings.EqualFold(farch.SubCPU.String(farch.CPU), params.Arch) { + m = farch.File + } + } + } + + cstrs, err := m.GetCStrings() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, types.GenericError{Error: err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, machoStringsResponse{Path: params.Path, Arch: params.Arch, Strings: cstrs}) +} diff --git a/api/server/routes/macho/routes.go b/api/server/routes/macho/routes.go index 890c5e8ed1..97734754bc 100644 --- a/api/server/routes/macho/routes.go +++ b/api/server/routes/macho/routes.go @@ -38,6 +38,31 @@ func AddRoutes(rg *gin.RouterGroup) { // 400: genericError // 500: genericError m.GET("/info", machoInfo) + // swagger:route GET /macho/info/strings MachO getMachoInfoStrings + // + // Strings + // + // Get MachO strings. + // + // Produces: + // - application/json + // + // Parameters: + // + name: path + // in: query + // description: path to MachO + // required: true + // type: string + // + name: arch + // in: query + // description: architecture to get info for in universal MachO + // required: false + // type: string + // Responses: + // 200: machoStringsResponse + // 400: genericError + // 500: genericError + m.GET("/info/strings", machoStrings) // m.GET("/lipo", handler) // TODO: implement this // m.GET("/o2a", handler) // TODO: implement this diff --git a/api/swagger.json b/api/swagger.json index 26fe77d371..d32061ef3e 100644 --- a/api/swagger.json +++ b/api/swagger.json @@ -1374,6 +1374,45 @@ } } }, + "/macho/info/strings": { + "get": { + "description": "Get MachO strings.", + "produces": [ + "application/json" + ], + "tags": [ + "MachO" + ], + "summary": "Strings", + "operationId": "getMachoInfoStrings", + "parameters": [ + { + "type": "string", + "description": "path to MachO", + "name": "path", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "architecture to get info for in universal MachO", + "name": "arch", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/machoStringsResponse" + }, + "400": { + "$ref": "#/responses/genericError" + }, + "500": { + "$ref": "#/responses/genericError" + } + } + } + }, "/mount/{type}": { "post": { "description": "Mount a DMG inside a given IPSW.", @@ -5173,6 +5212,28 @@ } } }, + "machoStringsResponse": { + "description": "", + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint64" + } + } + }, + "headers": { + "arch": { + "type": "string" + }, + "path": { + "type": "string" + }, + "strings": {} + } + }, "mountReponse": { "description": "", "headers": { diff --git a/cmd/ipsw/cmd/dyld/dyld_macho.go b/cmd/ipsw/cmd/dyld/dyld_macho.go index ce22712b27..75e048ffe1 100644 --- a/cmd/ipsw/cmd/dyld/dyld_macho.go +++ b/cmd/ipsw/cmd/dyld/dyld_macho.go @@ -25,12 +25,10 @@ import ( "bytes" "encoding/hex" "fmt" - "io" "os" "path/filepath" "slices" "strings" - "unicode" "github.com/apex/log" "github.com/blacktop/go-macho/pkg/fixupchains" @@ -439,45 +437,12 @@ var MachoCmd = &cobra.Command{ fmt.Println("STRINGS") fmt.Println("=======") } - // TODO: add option to dump all strings - https://github.com/robpike/strings/blob/master/strings.go - for _, sec := range m.Sections { - if sec.Flags.IsCstringLiterals() || sec.Name == "__os_log" || (sec.Seg == "__TEXT" && sec.Name == "__const") { - uuid, off, err := f.GetOffset(sec.Addr) - if err != nil { - return fmt.Errorf("failed to get offset for %s.%s: %v", sec.Seg, sec.Name, err) - } - dat, err := f.ReadBytesForUUID(uuid, int64(off), sec.Size) - if err != nil { - return fmt.Errorf("failed to read cstrings in %s.%s: %v", sec.Seg, sec.Name, err) - } - - fmt.Printf("\n[%s.%s]\n", sec.Seg, sec.Name) - - csr := bytes.NewBuffer(dat) - - for { - pos := sec.Addr + uint64(csr.Cap()-csr.Len()) - - s, err := csr.ReadString('\x00') - if err != nil { - if err == io.EOF { - break - } - return fmt.Errorf("failed to read string: %v", err) - } - - s = strings.Trim(s, "\x00") - - if len(s) > 0 { - for _, r := range s { - if r > unicode.MaxASCII || !unicode.IsPrint(r) { - continue // skip non-ascii strings - } - } - fmt.Printf("%s: %s\n", symAddrColor("%#09x", pos), symNameColor(fmt.Sprintf("%#v", s))) - } - } - } + strs, err := mcmd.GetStrings(m) + if err != nil { + return fmt.Errorf("failed to get strings: %v", err) + } + for pos, s := range strs { + fmt.Printf("%s: %s\n", symAddrColor("%#09x", pos), symNameColor(fmt.Sprintf("%#v", s))) } if cfstrs, err := m.GetCFStrings(); err == nil { diff --git a/cmd/ipsw/cmd/macho/macho_info.go b/cmd/ipsw/cmd/macho/macho_info.go index 311746223d..ef8fe9f147 100644 --- a/cmd/ipsw/cmd/macho/macho_info.go +++ b/cmd/ipsw/cmd/macho/macho_info.go @@ -22,7 +22,6 @@ THE SOFTWARE. package macho import ( - "bytes" "crypto/ecdsa" "crypto/rsa" "encoding/json" @@ -34,7 +33,6 @@ import ( "path/filepath" "strings" "text/tabwriter" - "unicode" "github.com/AlecAivazis/survey/v2" "github.com/alecthomas/chroma/v2/quick" @@ -984,45 +982,12 @@ var machoInfoCmd = &cobra.Command{ fmt.Println("STRINGS") fmt.Println("=======") } - // TODO: add option to dump all strings - https://github.com/robpike/strings/blob/master/strings.go - for _, sec := range m.Sections { - if sec.Flags.IsCstringLiterals() || sec.Name == "__os_log" || (sec.Seg == "__TEXT" && sec.Name == "__const") { - off, err := m.GetOffset(sec.Addr) - if err != nil { - return fmt.Errorf("failed to get offset for %s.%s: %v", sec.Seg, sec.Name, err) - } - dat := make([]byte, sec.Size) - if _, err = m.ReadAt(dat, int64(off)); err != nil { - return fmt.Errorf("failed to read cstring data in %s.%s: %v", sec.Seg, sec.Name, err) - } - - fmt.Printf("\n[%s.%s]\n", sec.Seg, sec.Name) - - csr := bytes.NewBuffer(dat) - - for { - pos := sec.Addr + uint64(csr.Cap()-csr.Len()) - - s, err := csr.ReadString('\x00') - if err != nil { - if err == io.EOF { - break - } - return fmt.Errorf("failed to read string: %v", err) - } - - s = strings.Trim(s, "\x00") - - if len(s) > 0 { - for _, r := range s { - if r > unicode.MaxASCII || !unicode.IsPrint(r) { - continue // skip non-ascii strings - } - } - fmt.Printf("%s: %s\n", symAddrColor("%#09x", pos), symNameColor(fmt.Sprintf("%#v", s))) - } - } - } + strs, err := mcmd.GetStrings(m) + if err != nil { + return fmt.Errorf("failed to get strings: %v", err) + } + for pos, s := range strs { + fmt.Printf("%s: %s\n", symAddrColor("%#09x", pos), symNameColor(fmt.Sprintf("%#v", s))) } if cfstrs, err := m.GetCFStrings(); err == nil { diff --git a/internal/commands/macho/macho.go b/internal/commands/macho/macho.go index 6fac687bf5..e801c9f8b1 100644 --- a/internal/commands/macho/macho.go +++ b/internal/commands/macho/macho.go @@ -1,7 +1,11 @@ package macho import ( + "bytes" "fmt" + "io" + "strings" + "unicode" "github.com/blacktop/go-macho" "github.com/blacktop/ipsw/pkg/disass" @@ -28,3 +32,50 @@ func FindSwiftStrings(m *macho.File) (map[uint64]string, error) { return engine.FindSwiftStrings() } + +// TODO: add option to dump all strings - https://github.com/robpike/strings/blob/master/strings.go +func GetStrings(m *macho.File) (map[uint64]string, error) { + strs := make(map[uint64]string) + + for _, sec := range m.Sections { + if sec.Flags.IsCstringLiterals() || sec.Name == "__os_log" || (sec.Seg == "__TEXT" && sec.Name == "__const") { + off, err := m.GetOffset(sec.Addr) + if err != nil { + return nil, fmt.Errorf("failed to get offset for %s.%s: %v", sec.Seg, sec.Name, err) + } + dat := make([]byte, sec.Size) + if _, err = m.ReadAt(dat, int64(off)); err != nil { + return nil, fmt.Errorf("failed to read cstring data in %s.%s: %v", sec.Seg, sec.Name, err) + } + + fmt.Printf("\n[%s.%s]\n", sec.Seg, sec.Name) + + csr := bytes.NewBuffer(dat) + + for { + pos := sec.Addr + uint64(csr.Cap()-csr.Len()) + + s, err := csr.ReadString('\x00') + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("failed to read string: %v", err) + } + + s = strings.Trim(s, "\x00") + + if len(s) > 0 { + for _, r := range s { + if r > unicode.MaxASCII || !unicode.IsPrint(r) { + continue // skip non-ascii strings + } + } + strs[pos] = s + } + } + } + } + + return strs, nil +} diff --git a/www/static/api/swagger.json b/www/static/api/swagger.json index 26fe77d371..d32061ef3e 100644 --- a/www/static/api/swagger.json +++ b/www/static/api/swagger.json @@ -1374,6 +1374,45 @@ } } }, + "/macho/info/strings": { + "get": { + "description": "Get MachO strings.", + "produces": [ + "application/json" + ], + "tags": [ + "MachO" + ], + "summary": "Strings", + "operationId": "getMachoInfoStrings", + "parameters": [ + { + "type": "string", + "description": "path to MachO", + "name": "path", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "architecture to get info for in universal MachO", + "name": "arch", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/machoStringsResponse" + }, + "400": { + "$ref": "#/responses/genericError" + }, + "500": { + "$ref": "#/responses/genericError" + } + } + } + }, "/mount/{type}": { "post": { "description": "Mount a DMG inside a given IPSW.", @@ -5173,6 +5212,28 @@ } } }, + "machoStringsResponse": { + "description": "", + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "uint64" + } + } + }, + "headers": { + "arch": { + "type": "string" + }, + "path": { + "type": "string" + }, + "strings": {} + } + }, "mountReponse": { "description": "", "headers": {