Skip to content

Commit

Permalink
feat: add NEW /macho/info/strings route to ipswd
Browse files Browse the repository at this point in the history
  • Loading branch information
blacktop committed Oct 14, 2024
1 parent 13aef32 commit 2aacaf6
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 82 deletions.
51 changes: 51 additions & 0 deletions api/server/routes/macho/macho.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(&params); 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})
}
25 changes: 25 additions & 0 deletions api/server/routes/macho/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions api/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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": {
Expand Down
47 changes: 6 additions & 41 deletions cmd/ipsw/cmd/dyld/dyld_macho.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
47 changes: 6 additions & 41 deletions cmd/ipsw/cmd/macho/macho_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ THE SOFTWARE.
package macho

import (
"bytes"
"crypto/ecdsa"
"crypto/rsa"
"encoding/json"
Expand All @@ -34,7 +33,6 @@ import (
"path/filepath"
"strings"
"text/tabwriter"
"unicode"

"github.com/AlecAivazis/survey/v2"
"github.com/alecthomas/chroma/v2/quick"
Expand Down Expand Up @@ -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 {
Expand Down
51 changes: 51 additions & 0 deletions internal/commands/macho/macho.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package macho

import (
"bytes"
"fmt"
"io"
"strings"
"unicode"

"github.com/blacktop/go-macho"
"github.com/blacktop/ipsw/pkg/disass"
Expand All @@ -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
}
Loading

0 comments on commit 2aacaf6

Please sign in to comment.