Skip to main content

apk_info_axml/
axml.rs

1use std::borrow::Cow;
2
3use apk_info_xml::Element;
4use log::warn;
5use winnow::error::{ContextError, ErrMode};
6use winnow::prelude::*;
7use winnow::token::take;
8
9use crate::ARSC;
10use crate::errors::AXMLError;
11use crate::structs::{
12    ResChunkHeader, ResourceHeaderType, StringPool, XMLHeader, XMLResourceMap, XmlCData,
13    XmlEndElement, XmlNamespace, XmlParse, XmlStartElement, attrs_manifest,
14};
15
16/// Default android namespace
17pub const ANDROID_NAMESPACE: &str = "http://schemas.android.com/apk/res/android";
18
19/// Represents an Android Binary XML (AXML) file.
20///
21/// This struct holds the root element of the parsed XML structure.
22///
23/// You can use this struct to traverse the XML tree, extract attributes,
24/// or get a string representation of the XML.
25#[derive(Debug)]
26pub struct AXML {
27    pub root: Element,
28}
29
30impl AXML {
31    /// Parses a byte slice into an `AXML` structure.
32    ///
33    /// # Example
34    ///
35    /// ```ignore
36    /// let axml = AXML::new(&mut input_bytes, Some(&arsc))?;
37    /// ```
38    pub fn new(input: &mut &[u8], arsc: Option<&ARSC>) -> Result<AXML, AXMLError> {
39        // basic sanity check
40        if input.len() < 8 {
41            return Err(AXMLError::TooSmallError);
42        }
43
44        // parse header
45        let header = ResChunkHeader::parse(input).map_err(|_| AXMLError::HeaderError)?;
46
47        // header size must be 8 bytes, otherwise is non valid axml
48        if header.header_size != 8 {
49            return Err(AXMLError::HeaderSizeError(header.header_size));
50        }
51
52        // parse string pool
53        let string_pool = StringPool::parse(input).map_err(|_| AXMLError::StringPoolError)?;
54
55        // parse resource map
56        let xml_resource = XMLResourceMap::parse(input).map_err(|_| AXMLError::ResourceMapError)?;
57
58        // parse and get xml tree
59        let root = Self::get_xml_tree(input, arsc, &string_pool, &xml_resource)
60            .ok_or(AXMLError::MissingRoot)?;
61
62        Ok(AXML { root })
63    }
64
65    fn get_xml_tree<'a>(
66        input: &mut &[u8],
67        arsc: Option<&ARSC>,
68        string_pool: &'a StringPool,
69        xml_resource: &'a XMLResourceMap,
70    ) -> Option<Element> {
71        let mut stack: Vec<Element> = Vec::with_capacity(16);
72
73        loop {
74            let chunk_header = match ResChunkHeader::parse(input) {
75                Ok(v) => v,
76                Err(ErrMode::Backtrack(_)) => break,
77                Err(_) => return None,
78            };
79
80            // Skip non-xml chunks
81            if chunk_header.type_ < ResourceHeaderType::XmlStartNamespace
82                || chunk_header.type_ > ResourceHeaderType::XmlLastChunk
83            {
84                warn!("not a xml resource chunk: {chunk_header:?}");
85
86                let _ =
87                    take::<u32, &[u8], ContextError>(chunk_header.content_size()).parse_next(input);
88                continue;
89            }
90
91            // another malware technique
92            if chunk_header.header_size != 0x10 {
93                warn!("xml resource chunk header size is not 0x10: {chunk_header:?}, skipped");
94
95                let _ =
96                    take::<u32, &[u8], ContextError>(chunk_header.content_size()).parse_next(input);
97                continue;
98            }
99
100            let xml_header = match XMLHeader::parse(input, chunk_header) {
101                Ok(v) => v,
102                Err(_) => break,
103            };
104
105            match xml_header.header.type_ {
106                ResourceHeaderType::XmlStartNamespace => {
107                    let _ = XmlNamespace::parse(input, xml_header);
108                }
109                ResourceHeaderType::XmlEndNamespace => {
110                    let _ = XmlNamespace::parse(input, xml_header);
111                }
112                ResourceHeaderType::XmlStartElement => {
113                    let node = match XmlStartElement::parse(input, xml_header) {
114                        Ok(v) => v,
115                        Err(_) => break,
116                    };
117
118                    let Some(name) = string_pool.get(node.name) else {
119                        continue;
120                    };
121
122                    let mut element = Element::with_capacity(name, node.attributes.len());
123
124                    if name == "manifest" {
125                        element.set_attribute_with_prefix(
126                            Some("xlmns"),
127                            "android",
128                            ANDROID_NAMESPACE,
129                        );
130                    }
131
132                    for attribute in &node.attributes {
133                        let Some(attribute_name) =
134                            string_pool.get_with_resources(attribute.name, xml_resource, true)
135                        else {
136                            continue;
137                        };
138
139                        // skip garbage strings
140                        if attribute_name.contains(char::is_whitespace) {
141                            warn!("skipped garbage attribute name: {:?}", attribute_name);
142                            continue;
143                        }
144
145                        let ns_prefix = if string_pool
146                            .get_with_resources(attribute.namespace_uri, xml_resource, false)
147                            .is_some()
148                        {
149                            Some("android")
150                        } else {
151                            None
152                        };
153
154                        let value_str = attrs_manifest::get_attr_value(
155                            attribute_name,
156                            &attribute.typed_value.data,
157                        )
158                        .unwrap_or_else(|| {
159                            Cow::Owned(attribute.typed_value.to_string(string_pool, arsc))
160                        });
161
162                        element.set_attribute_with_prefix(ns_prefix, attribute_name, &value_str);
163                    }
164
165                    stack.push(element);
166                }
167                ResourceHeaderType::XmlEndElement => {
168                    let _ = XmlEndElement::parse(input, xml_header);
169
170                    if stack.len() > 1 {
171                        let finished = stack.pop().unwrap();
172                        stack.last_mut().unwrap().append_child(finished);
173                    }
174                }
175                ResourceHeaderType::XmlCdata => {
176                    let _ = XmlCData::parse(input, xml_header);
177                }
178                _ => {
179                    warn!("unknown header type: {:#?}", xml_header.header.type_);
180                }
181            }
182        }
183
184        (!stack.is_empty()).then(|| stack.remove(0))
185    }
186
187    /// Returns the pretty-printed XML as a string.
188    ///
189    /// # Example
190    ///
191    /// ```ignore
192    /// let xml_string = axml.get_xml_string();
193    /// println!("{}", xml_string);
194    /// ```
195    #[inline]
196    pub fn get_xml_string(&self) -> String {
197        self.root.to_string()
198    }
199
200    /// Retrieves the value of an attribute from a specific tag.
201    pub fn get_attribute_value(
202        &self,
203        tag: &str,
204        name: &str,
205        arsc: Option<&ARSC>,
206    ) -> Option<String> {
207        // check if root itself matches (<manifest> tag)
208        let value = if self.root.name() == tag {
209            self.root.attr(name)
210        } else {
211            // otherwise check other child elements
212            self.root
213                .descendants()
214                .find(|el| el.name() == tag)
215                .and_then(|el| el.attr(name))
216        };
217
218        match value {
219            // resolve reference we found
220            Some(v) if v.starts_with('@') => {
221                if let Some(arsc) = arsc {
222                    // safe slice, checked before
223                    let name = &v[1..];
224                    arsc.get_resource_value_by_name(name)
225                } else {
226                    Some(v.to_string())
227                }
228            }
229            // just a value, not a reference
230            Some(v) => Some(v.to_string()),
231            None => None,
232        }
233    }
234
235    /// Returns an iterator over attribute values for direct children with a specific tag.
236    ///
237    /// This is a faster version of [AXML::get_all_attribute_values] that only iterates over the root's direct children
238    #[inline]
239    pub fn get_root_attribute_values<'a>(
240        &'a self,
241        tag: &'a str,
242        name: &'a str,
243    ) -> impl Iterator<Item = &'a str> + 'a {
244        self.root
245            .childrens()
246            .filter(move |el| el.name() == tag)
247            .flat_map(move |el| {
248                el.attributes()
249                    .filter(move |attr| attr.name() == name)
250                    .map(|attr| attr.value())
251            })
252    }
253
254    /// Returns an iterator over attribute values for all descendants with a specific tag.
255    #[inline]
256    pub fn get_all_attribute_values<'a>(
257        &'a self,
258        tag: &'a str,
259        name: &'a str,
260    ) -> impl Iterator<Item = &'a str> + 'a {
261        self.root
262            .descendants()
263            .filter(move |el| el.name() == tag)
264            .flat_map(move |el| {
265                el.attributes()
266                    .filter(move |attr| attr.name() == name)
267                    .map(|attr| attr.value())
268            })
269    }
270
271    /// Extracts the main launcher activities from an APK manifest.
272    ///
273    /// Algorithm:
274    /// 1. Search for all `<activity>` and `<activity-alias>` tags.
275    /// 2. Look for `android.intent.action.MAIN` with `android.intent.category.LAUNCHER` or `android.intent.category.INFO`.
276    ///
277    /// See: <https://xrefandroid.com/android-16.0.0_r2/xref/frameworks/base/core/java/android/app/ApplicationPackageManager.java#310>
278    pub fn get_main_activities(&self) -> impl Iterator<Item = &str> {
279        self.root
280            .childrens()
281            .filter(|c| c.name() == "application")
282            .flat_map(|app| app.childrens())
283            .filter_map(|activity| {
284                // check tag and enabled state
285                let tag = activity.name();
286                if (tag != "activity" && tag != "activity-alias")
287                    || activity.attr("enabled") == Some("false")
288                {
289                    return None;
290                }
291
292                for intent_filter in activity.childrens() {
293                    if intent_filter.name() != "intent-filter" {
294                        continue;
295                    }
296
297                    let mut has_main = false;
298                    let mut has_launcher = false;
299
300                    for child in intent_filter.childrens() {
301                        match (child.name(), child.attr("name")) {
302                            ("action", Some("android.intent.action.MAIN")) => has_main = true,
303                            ("category", Some("android.intent.category.LAUNCHER"))
304                            | ("category", Some("android.intent.category.INFO")) => {
305                                has_launcher = true
306                            }
307                            _ => {}
308                        }
309                    }
310
311                    if has_main && has_launcher {
312                        return activity.attr("name");
313                    }
314                }
315
316                None
317            })
318    }
319}