diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5172429 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba6805a --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual Environment +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Application specific +.frida_gui/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 5729f98..47ba9b8 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,122 @@ -# Frida GUI Injector - -A graphical user interface for Frida script injection and app launching on Android devices, with special support for Nox emulator. - -![image](https://github.com/gru122121/FridaGUI/blob/main/image.png?raw=true) - -## Features - -- Device scanning and selection -- Application listing with package names -- Automatic Frida server setup and management -- Script injection during app launch -- Support for Nox emulator -- Real-time output logging - -## Prerequisites - -- Python 3.7+ -- ADB (Android Debug Bridge) -- Rooted Android device or emulator -- USB debugging enabled on target device - -## Installation - -1. Clone the repository: + + +# FridaGUI + +A modern and powerful GUI tool for Frida script management and injection, created by **Oliver Stankiewicz**. + +## 🚀 Features +- 🔍 **Script Injection with Live Preview** +- 🌐 **CodeShare Browser & Integration** +- ⭐ **Favorites System** +- 📱 **Android/iOS Device Support** +- 💻 **Process Management** +- 🎨 **Modern Dark Theme UI** +- 📊 **Real-time Process Monitoring** +- 📝 **Script History Tracking** +- 🔄 **Auto-injection Support** + +--- + +## 📥 Installation + +### Prerequisites +- **Python 3.8+** +- **Frida** +- **ADB** (for Android device support) + +### Steps +1. Clone the repository: + ```bash + git clone https://github.com/oliverstankiewicz/FridaGUI.git + cd FridaGUI + ``` + +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Run the application: + ```bash + python src/main.py + ``` + +--- + +## 📂 Project Structure +``` +FridaGUI/ +├── src/ +│ ├── gui/ +│ │ ├── widgets/ +│ │ │ ├── device_panel.py +│ │ │ ├── data_visualizer.py +│ │ │ ├── history_page.py +│ │ │ ├── injection_panel.py +│ │ │ ├── output_panel.py +│ │ │ ├── process_monitor.py +│ │ │ ├── process_panel.py +│ │ │ └── script_editor.py +│ │ └── main_window.py +│ ├── utils/ +│ │ └── themes.py +│ └── main.py +├── requirements.txt +├── requirements-dev.txt +├── LICENSE +└── README.md +``` + +--- + +## 🧩 Core Components + +### **Device Panel** +- USB/Network device support +- Android device detection +- Frida server management +- Process listing + +### **Script Editor** +- Code editing with syntax highlighting +- Script management and injection controls +- Real-time output monitoring + +### **Process Monitor** +- Real-time process list with filtering +- Memory tracking and auto-refresh + +### **Data Visualizer** +- Process data visualization +- Memory usage graphs and performance metrics +- Real-time updates + +### **History Page** +- Script history and injection logs +- Quick re-injection functionality +- Session tracking + +--- + +## 📜 Dependencies +- Refer to the `requirements.txt` file for a complete list of dependencies. + +--- + +## 📄 License +This project is licensed under the **agplv3 License**. See the [LICENSE](LICENSE) file for details. + +--- + +## 👤 Author +**Oliver Stankiewicz** + +--- + +## 🤝 Contributing +Pull requests are welcome! For major changes, please open an issue first to discuss your ideas. + +--- + +## 🛠️ Support +If you encounter any issues or have questions, please file an issue on [GitHub](https://github.com/oliverstankiewicz/FridaGUI/issues). diff --git a/image.png b/image.png deleted file mode 100644 index 29321e8..0000000 Binary files a/image.png and /dev/null differ diff --git a/main.py b/main.py deleted file mode 100644 index ca10da9..0000000 --- a/main.py +++ /dev/null @@ -1,406 +0,0 @@ -import sys -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, - QHBoxLayout, QComboBox, QPushButton, QTextEdit, - QLabel, QMessageBox, QCheckBox) -from PyQt5.QtCore import Qt, QThread, pyqtSignal -import frida -import subprocess -import time -import os -import requests -import platform -import lzma - -class FridaWorker(QThread): - output_signal = pyqtSignal(str) - error_signal = pyqtSignal(str) - - def __init__(self, action, **kwargs): - super().__init__() - self.action = action - self.kwargs = kwargs - - def run(self): - try: - if self.action == "scan_devices": - self._scan_devices() - elif self.action == "inject": - self._inject_script() - elif self.action == "launch": - self._launch_app() - except Exception as e: - self.error_signal.emit(str(e)) - - def _scan_devices(self): - try: - # Kill and restart ADB server - subprocess.run(['adb', 'kill-server'], capture_output=True) - time.sleep(1) - subprocess.run(['adb', 'start-server'], capture_output=True) - time.sleep(2) - - # Try connecting to Nox - nox_ports = ['62001', '62025', '62026', '62027', '62028', '62029'] - for port in nox_ports: - try: - subprocess.run(['adb', 'connect', f'127.0.0.1:{port}'], - capture_output=True) - except: - continue - - time.sleep(2) - self.output_signal.emit("Device scan completed") - except Exception as e: - self.error_signal.emit(f"Error scanning devices: {str(e)}") - - def _inject_script(self): - try: - device_id = self.kwargs['device_id'] - process_id = self.kwargs['process_id'] - script_content = self.kwargs['script'] - - # Special handling for Nox - if '127.0.0.1' in device_id: - # Ensure frida-server is running - try: - subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "killall -9 frida-server"'], - capture_output=True) - time.sleep(1) - subprocess.Popen(['adb', '-s', device_id, 'shell', 'su -c "/data/local/tmp/frida-server &"'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - self.output_signal.emit("Restarted frida-server on Nox") - time.sleep(3) - except Exception as e: - self.error_signal.emit(f"Error restarting frida-server: {str(e)}") - - # Try to attach multiple times - max_retries = 3 - last_error = None - - for i in range(max_retries): - try: - device = frida.get_device(device_id) - session = device.attach(int(process_id)) - script = session.create_script(script_content) - - def on_message(message, data): - if message['type'] == 'send': - self.output_signal.emit(f"Script message: {message['payload']}") - elif message['type'] == 'error': - self.error_signal.emit(f"Script error: {message['description']}") - - script.on('message', on_message) - script.load() - self.output_signal.emit("Script injected successfully") - return - except Exception as e: - last_error = str(e) - self.output_signal.emit(f"Injection attempt {i+1} failed, retrying...") - time.sleep(2) - continue - - raise Exception(f"Failed after {max_retries} attempts. Last error: {last_error}") - - except Exception as e: - self.error_signal.emit(f"Injection error: {str(e)}") - - def _launch_app(self): - try: - device_id = self.kwargs['device_id'] - package_name = self.kwargs['package_name'] - script_content = self.kwargs['script'] - - # Special handling for Nox - if '127.0.0.1' in device_id: - try: - # Setup frida-server first - if not self.setup_frida_server(device_id): - raise Exception("Failed to setup frida-server") - - # Ensure connection to Nox - subprocess.run(['adb', 'connect', device_id], capture_output=True) - time.sleep(1) - - # Start frida-server - subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "killall -9 frida-server"'], - capture_output=True) - time.sleep(1) - subprocess.Popen(['adb', '-s', device_id, 'shell', 'su -c "/data/local/tmp/frida-server &"'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - self.output_signal.emit("Started frida-server on Nox") - time.sleep(3) - - # Kill existing app - subprocess.run(['adb', '-s', device_id, 'shell', 'am force-stop ' + package_name], - capture_output=True) - time.sleep(1) - - # Get device and spawn app - device = frida.get_device(device_id) - pid = device.spawn([package_name]) - self.output_signal.emit(f"Spawned app with PID: {pid}") - - # Attach to the spawned process - session = device.attach(pid) - - # Create and load script - script = session.create_script(script_content) - - def on_message(message, data): - if message['type'] == 'send': - self.output_signal.emit(f"Script message: {message['payload']}") - elif message['type'] == 'error': - self.error_signal.emit(f"Script error: {message['description']}") - - script.on('message', on_message) - script.load() - self.output_signal.emit("Script loaded") - - # Resume the app - device.resume(pid) - self.output_signal.emit("App resumed with injected script") - - except Exception as e: - raise Exception(f"Nox launch failed: {str(e)}") - - self.output_signal.emit("App launched successfully with script") - except Exception as e: - self.error_signal.emit(f"Launch error: {str(e)}") - - def download_frida_server(self): - try: - # Get device architecture - device_id = self.kwargs.get('device_id', '') - abi = subprocess.check_output( - ['adb', '-s', device_id, 'shell', 'getprop', 'ro.product.cpu.abi'], - text=True - ).strip() - - # Map Android ABI to Frida architecture - abi_to_arch = { - 'arm64-v8a': 'arm64', - 'armeabi-v7a': 'arm', - 'x86': 'x86', - 'x86_64': 'x86_64' - } - arch = abi_to_arch.get(abi, 'arm64') - - # Get latest Frida release version - response = requests.get('https://api.github.com/repos/frida/frida/releases/latest') - latest_version = response.json()['tag_name'] - - # Construct download URL - download_url = f'https://github.com/frida/frida/releases/download/{latest_version}/frida-server-{latest_version[0:]}-android-{arch}.xz' - self.output_signal.emit(f"Downloading Frida server from: {download_url}") - - # Download frida-server - response = requests.get(download_url) - response.raise_for_status() - - # Decompress XZ file - decompressed_data = lzma.decompress(response.content) - - # Save as frida-server in current directory - frida_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'frida-server') - with open(frida_path, 'wb') as f: - f.write(decompressed_data) - - # Make executable - os.chmod(frida_path, 0o755) - - self.output_signal.emit(f"Successfully downloaded frida-server {latest_version}") - return frida_path - except Exception as e: - self.error_signal.emit(f"Error downloading frida-server: {str(e)}") - return None - - def setup_frida_server(self, device_id): - try: - # Check if frida-server exists locally - frida_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'frida-server') - if not os.path.exists(frida_path): - self.output_signal.emit("Downloading frida-server...") - frida_path = self.download_frida_server() - if not frida_path: - raise Exception("Failed to download frida-server") - - # Push frida-server to device - self.output_signal.emit("Pushing frida-server to device...") - result = subprocess.run( - ['adb', '-s', device_id, 'push', frida_path, '/data/local/tmp/'], - capture_output=True, - text=True - ) - - if "error" in result.stderr.lower(): - # Try with su if normal push fails - self.output_signal.emit("Trying with root permissions...") - subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "mount -o rw,remount /system"']) - subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "chmod 777 /data/local/tmp"']) - subprocess.run(['adb', '-s', device_id, 'push', frida_path, '/data/local/tmp/']) - - # Set permissions - subprocess.run(['adb', '-s', device_id, 'shell', 'su -c "chmod 755 /data/local/tmp/frida-server"']) - - self.output_signal.emit("Frida server setup completed") - return True - except Exception as e: - self.error_signal.emit(f"Error setting up frida-server: {str(e)}") - return False - -class FridaGUI(QMainWindow): - def __init__(self): - super().__init__() - self.setWindowTitle("Frida Script Injector") - self.setMinimumSize(800, 600) - - # Create main widget and layout - main_widget = QWidget() - self.setCentralWidget(main_widget) - layout = QVBoxLayout(main_widget) - - # Device selection - device_layout = QHBoxLayout() - self.device_combo = QComboBox() - scan_button = QPushButton("Scan Devices") - scan_button.clicked.connect(self.scan_devices) - device_layout.addWidget(QLabel("Select Device:")) - device_layout.addWidget(self.device_combo) - device_layout.addWidget(scan_button) - layout.addLayout(device_layout) - - # Process selection - process_layout = QHBoxLayout() - self.process_combo = QComboBox() - list_button = QPushButton("List Processes") - list_button.clicked.connect(self.list_processes) - process_layout.addWidget(QLabel("Select Process:")) - process_layout.addWidget(self.process_combo) - process_layout.addWidget(list_button) - layout.addLayout(process_layout) - - # Launch button - launch_button = QPushButton("Launch App") - launch_button.clicked.connect(self.launch_app) - layout.addWidget(launch_button) - - # Launch with inject checkbox - self.inject_on_launch = QCheckBox("Inject on Launch") - self.inject_on_launch.setChecked(True) # Enable by default - layout.addWidget(self.inject_on_launch) - - # Script editor - layout.addWidget(QLabel("Frida Script:")) - self.script_editor = QTextEdit() - self.script_editor.setPlainText('''Java.perform(function() { - console.log("Loaded!"); - // Enable SSL logging - var modules = Process.enumerateModules(); - modules.forEach(function(module) { - if (module.name.indexOf(".so") !== -1) { - console.log("Module " + module.name + " SSL logging started."); - } - }); -});''') - layout.addWidget(self.script_editor) - - # Output area - layout.addWidget(QLabel("Output:")) - self.output_area = QTextEdit() - self.output_area.setReadOnly(True) - layout.addWidget(self.output_area) - - # Initial device scan - self.scan_devices() - - def log_output(self, message): - self.output_area.append(message) - - def scan_devices(self): - self.worker = FridaWorker("scan_devices") - self.worker.output_signal.connect(self.log_output) - self.worker.error_signal.connect(self.log_output) - self.worker.finished.connect(self._update_devices) - self.worker.start() - - def _update_devices(self): - try: - self.device_combo.clear() - devices = frida.enumerate_devices() - for device in devices: - self.device_combo.addItem(f"{device.name} ({device.type})", device.id) - except Exception as e: - self.log_output(f"Error updating devices: {str(e)}") - - def list_processes(self): - try: - device_id = self.device_combo.currentData() - if not device_id: - raise Exception("Please select a device first") - - device = frida.get_device(device_id) - - # Get installed packages instead of processes - packages = [] - adb_output = subprocess.check_output( - ['adb', '-s', device_id, 'shell', 'pm', 'list', 'packages', '-f'], - text=True - ).strip().split('\n') - - self.process_combo.clear() - for line in adb_output: - if line: - # Extract package name from line - package = line.split('=')[-1] - # Get app name using aapt - try: - app_path = line.split(':')[1].split('=')[0] - aapt_output = subprocess.check_output( - ['adb', '-s', device_id, 'shell', 'dumpsys', 'package', package], - text=True - ) - app_name = package # Default to package name - for line in aapt_output.split('\n'): - if 'applicationInfo' in line and 'label=' in line: - app_name = line.split('label=')[1].split(' ')[0] - break - - self.process_combo.addItem(f"{app_name} ({package})", package) - except: - # If we can't get the app name, just show package name - self.process_combo.addItem(package, package) - - self.log_output("Applications listed successfully") - except Exception as e: - self.log_output(f"Error listing applications: {str(e)}") - - def launch_app(self): - try: - device_id = self.device_combo.currentData() - package_name = self.process_combo.currentData() - script = self.script_editor.toPlainText() if self.inject_on_launch.isChecked() else "" - - if not device_id or not package_name: - raise Exception("Please select device and application first") - - if self.inject_on_launch.isChecked() and not script: - raise Exception("Please provide a script to inject") - - self.worker = FridaWorker("launch", - device_id=device_id, - package_name=package_name, - script=script) - self.worker.output_signal.connect(self.log_output) - self.worker.error_signal.connect(self.log_output) - self.worker.start() - except Exception as e: - self.log_output(f"Error launching app: {str(e)}") - -def main(): - app = QApplication(sys.argv) - window = FridaGUI() - window.show() - sys.exit(app.exec()) - -if __name__ == '__main__': - main() diff --git a/requirements.txt b/requirements.txt index 1821c8b..b8374a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ -frida-tools>=11.0.0 -frida>=16.0.19 -PyQt5==5.15.9 -requests>=2.26.0 \ No newline at end of file +frida +PyQt5 +qtawesome +requests +beautifulsoup4 +psutil +aiohttp \ No newline at end of file diff --git a/src/core/__pycache__/android_helper.cpython-311.pyc b/src/core/__pycache__/android_helper.cpython-311.pyc new file mode 100644 index 0000000..2a9ae0d Binary files /dev/null and b/src/core/__pycache__/android_helper.cpython-311.pyc differ diff --git a/src/core/__pycache__/history_manager.cpython-311.pyc b/src/core/__pycache__/history_manager.cpython-311.pyc new file mode 100644 index 0000000..0dd5d41 Binary files /dev/null and b/src/core/__pycache__/history_manager.cpython-311.pyc differ diff --git a/src/core/__pycache__/process_monitor.cpython-311.pyc b/src/core/__pycache__/process_monitor.cpython-311.pyc new file mode 100644 index 0000000..4eddac0 Binary files /dev/null and b/src/core/__pycache__/process_monitor.cpython-311.pyc differ diff --git a/src/core/__pycache__/script_history.cpython-311.pyc b/src/core/__pycache__/script_history.cpython-311.pyc new file mode 100644 index 0000000..572ebb6 Binary files /dev/null and b/src/core/__pycache__/script_history.cpython-311.pyc differ diff --git a/src/core/__pycache__/script_templates.cpython-311.pyc b/src/core/__pycache__/script_templates.cpython-311.pyc new file mode 100644 index 0000000..b495ef9 Binary files /dev/null and b/src/core/__pycache__/script_templates.cpython-311.pyc differ diff --git a/src/core/android_helper.py b/src/core/android_helper.py new file mode 100644 index 0000000..cc4ecb9 --- /dev/null +++ b/src/core/android_helper.py @@ -0,0 +1,175 @@ +import subprocess +import os +import time +import requests +import platform + +class AndroidHelper: + FRIDA_SERVER_URL = "https://github.com/frida/frida/releases/download/16.1.4/frida-server-16.1.4-android-arm64.xz" + + @staticmethod + def get_adb_path(): + """Get the ADB executable path""" + if platform.system() == "Windows": + return "adb.exe" + return "adb" + + @staticmethod + def is_device_connected(device_id): + """Check if device is connected""" + try: + output = subprocess.check_output([AndroidHelper.get_adb_path(), 'devices'], text=True) + return device_id in output + except: + return False + + @staticmethod + def get_device_arch(device_id): + """Get device architecture""" + try: + output = subprocess.check_output([ + AndroidHelper.get_adb_path(), '-s', device_id, 'shell', 'getprop ro.product.cpu.abi' + ], text=True).strip() + + if 'arm64' in output: + return 'arm64' + elif 'arm' in output: + return 'arm' + elif 'x86_64' in output: + return 'x86_64' + elif 'x86' in output: + return 'x86' + return 'arm64' # Default to arm64 + except: + return 'arm64' + + @staticmethod + def start_frida_server(device_id): + """Start frida-server on device""" + try: + adb = AndroidHelper.get_adb_path() + + # Get device architecture + arch = AndroidHelper.get_device_arch(device_id) + server_url = f"https://github.com/frida/frida/releases/download/16.1.4/frida-server-16.1.4-android-{arch}.xz" + + # First, try to get root access + subprocess.run([adb, '-s', device_id, 'root']) + time.sleep(1) # Wait for root to take effect + + # Remount system as read-write + subprocess.run([adb, '-s', device_id, 'remount']) + + # Download and push frida-server (always get fresh copy) + print(f"Downloading frida-server for {arch}...") + response = requests.get(server_url) + server_path = os.path.join(os.path.expanduser('~'), '.frida_gui', f'frida-server-{arch}') + + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(server_path), exist_ok=True) + + # Save and extract + with open(server_path + '.xz', 'wb') as f: + f.write(response.content) + + try: + subprocess.run(['xz', '-d', '-f', server_path + '.xz']) # Force extraction + except: + print("Error extracting with xz, trying alternative method...") + import lzma + with lzma.open(server_path + '.xz') as f: + with open(server_path, 'wb') as out: + out.write(f.read()) + + # Push to device + print("Pushing frida-server to device...") + subprocess.run([ + adb, '-s', device_id, 'push', + server_path, '/data/local/tmp/frida-server' + ]) + + # Kill any existing frida-server processes + kill_commands = [ + 'pkill -f frida-server', + 'killall -9 frida-server', + 'kill $(pidof frida-server)', + ] + + for cmd in kill_commands: + subprocess.run([adb, '-s', device_id, 'shell', cmd], stderr=subprocess.PIPE) + + # Set permissions and start server + start_commands = [ + 'chmod 755 /data/local/tmp/frida-server', + 'su -c "chmod 755 /data/local/tmp/frida-server"', + 'su -c "setenforce 0"', + 'su -c "/data/local/tmp/frida-server -D"', # Run in daemon mode + '/data/local/tmp/frida-server -D' # Fallback without su + ] + + for cmd in start_commands: + try: + subprocess.run([ + adb, '-s', device_id, 'shell', cmd + ], stderr=subprocess.PIPE, timeout=5) + time.sleep(1) + if AndroidHelper.is_frida_running(device_id): + print("Frida server started successfully") + return True + except subprocess.TimeoutExpired: + # This might actually be good - server could be running + if AndroidHelper.is_frida_running(device_id): + print("Frida server started successfully") + return True + except: + continue + + print("Failed to start frida-server") + return False + + except Exception as e: + print(f"Error starting frida-server: {e}") + return False + + @staticmethod + def is_frida_running(device_id): + """Check if frida-server is running on device""" + try: + # Try different ps commands as they vary by Android version + commands = [ + 'ps -A | grep frida-server', + 'ps -ef | grep frida-server', + 'ps | grep frida-server', + 'top -n 1 | grep frida-server', + 'pidof frida-server' + ] + + for cmd in commands: + try: + output = subprocess.check_output( + [AndroidHelper.get_adb_path(), '-s', device_id, 'shell', cmd], + text=True, + stderr=subprocess.PIPE, + timeout=2 + ) + if ('frida-server' in output and 'grep' not in output) or output.strip().isdigit(): + return True + except: + continue + + # Try netstat as last resort + try: + output = subprocess.check_output( + [AndroidHelper.get_adb_path(), '-s', device_id, 'shell', 'netstat -tlnp'], + text=True, + stderr=subprocess.PIPE, + timeout=2 + ) + if ':27042' in output: # Default frida port + return True + except: + pass + + return False + except: + return False \ No newline at end of file diff --git a/src/core/history_manager.py b/src/core/history_manager.py new file mode 100644 index 0000000..1c3fd02 --- /dev/null +++ b/src/core/history_manager.py @@ -0,0 +1,53 @@ +from datetime import datetime +import json +import os +from collections import deque +import weakref + +class HistoryManager: + def __init__(self): + self.history_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'history.json') + self._history = deque(maxlen=1000) # Limit history size + self.load_history() + + def load_history(self): + try: + if os.path.exists(self.history_file): + with open(self.history_file, 'r') as f: + # Load directly into deque with max size + data = json.load(f) + self._history.extend(data[-1000:]) # Only keep last 1000 entries + except Exception as e: + print(f"Error loading history: {e}") + + def save_history(self): + try: + os.makedirs(os.path.dirname(self.history_file), exist_ok=True) + with open(self.history_file, 'w') as f: + # Convert deque to list for JSON serialization + json.dump(list(self._history), f, indent=2) + except Exception as e: + print(f"Error saving history: {e}") + + def add_entry(self, action_type, details): + entry = { + 'timestamp': datetime.now().isoformat(), + 'type': action_type, + 'details': details + } + self._history.appendleft(entry) # Use deque's appendleft + + # Periodically save to prevent memory buildup + if len(self._history) % 10 == 0: # Save every 10 entries + self.save_history() + + def clear_history(self): + self._history.clear() + self.save_history() + + @property + def history(self): + return list(self._history) # Return a copy to prevent memory leaks + + def __del__(self): + self.save_history() \ No newline at end of file diff --git a/src/core/process_monitor.py b/src/core/process_monitor.py new file mode 100644 index 0000000..deedb96 --- /dev/null +++ b/src/core/process_monitor.py @@ -0,0 +1,67 @@ +from PyQt5.QtCore import QObject, pyqtSignal, QTimer +import psutil +import frida +import weakref + +class ProcessMonitor(QObject): + process_started = pyqtSignal(str, int) # name, pid + process_ended = pyqtSignal(str, int) # name, pid + memory_updated = pyqtSignal(str, float) # pid, memory_usage + + def __init__(self, refresh_rate=1000): + super().__init__() + self.refresh_rate = refresh_rate + self.monitored_processes = {} + self.timer = QTimer() + self.timer.timeout.connect(self.check_processes) + self._stopped = False + + def start_monitoring(self): + self._stopped = False + self.timer.start(self.refresh_rate) + + def stop_monitoring(self): + self._stopped = True + self.timer.stop() + self.monitored_processes.clear() # Clear the dictionary + + def check_processes(self): + if self._stopped: + return + + current_processes = {} + + try: + device = frida.get_local_device() + # Use iterator instead of list comprehension + for process in device.enumerate_processes(): + if self._stopped: + return + + current_processes[process.pid] = process.name + + # New process + if process.pid not in self.monitored_processes: + self.process_started.emit(process.name, process.pid) + + # Update memory usage + try: + p = psutil.Process(process.pid) + memory_mb = p.memory_info().rss / 1024 / 1024 + self.memory_updated.emit(str(process.pid), memory_mb) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + # Check for ended processes - use iterator + for pid in list(self.monitored_processes.keys()): + if pid not in current_processes: + name = self.monitored_processes[pid] + self.process_ended.emit(name, pid) + + self.monitored_processes = current_processes + + except Exception as e: + print(f"Error monitoring processes: {str(e)}") + + def __del__(self): + self.stop_monitoring() \ No newline at end of file diff --git a/src/core/script_history.py b/src/core/script_history.py new file mode 100644 index 0000000..bff4756 --- /dev/null +++ b/src/core/script_history.py @@ -0,0 +1,76 @@ +import json +import os +from datetime import datetime + +class ScriptHistory: + def __init__(self): + self.base_dir = os.path.join(os.path.expanduser('~'), '.frida_gui') + self.history_file = os.path.join(self.base_dir, 'script_history.json') + self.favorites_file = os.path.join(self.base_dir, 'favorites.json') + self.ensure_dirs() + self.load_history() + + def ensure_dirs(self): + os.makedirs(self.base_dir, exist_ok=True) + + def load_history(self): + try: + if os.path.exists(self.history_file): + with open(self.history_file, 'r') as f: + self.history = json.load(f) + else: + self.history = { + 'local': [], + 'codeshare': [], + 'favorites': [] + } + except Exception as e: + print(f"Error loading history: {e}") + self.history = {'local': [], 'codeshare': [], 'favorites': []} + + def save_history(self): + try: + with open(self.history_file, 'w') as f: + json.dump(self.history, f, indent=2) + except Exception as e: + print(f"Error saving history: {e}") + + def add_to_history(self, script_type, script_info): + """Add script to history with timestamp""" + entry = { + 'timestamp': datetime.now().isoformat(), + 'info': script_info + } + + # Keep only last 50 entries + self.history[script_type] = ([entry] + + [x for x in self.history[script_type] + if x['info'].get('id') != script_info.get('id')])[:50] + self.save_history() + + def add_to_favorites(self, script_info): + """Add script to favorites""" + if script_info not in self.history['favorites']: + self.history['favorites'].append(script_info) + self.save_history() + + def remove_from_favorites(self, script_id): + """Remove script from favorites""" + self.history['favorites'] = [ + x for x in self.history['favorites'] + if x.get('id') != script_id + ] + self.save_history() + + def get_recent_scripts(self, script_type, limit=10): + """Get recent scripts of specified type""" + return self.history[script_type][:limit] + + def get_favorites(self): + """Get all favorite scripts""" + return self.history['favorites'] + + def is_favorite(self, script_id): + """Check if script is in favorites""" + return any(x.get('id') == script_id + for x in self.history['favorites']) \ No newline at end of file diff --git a/src/core/script_manager.py b/src/core/script_manager.py new file mode 100644 index 0000000..9607ab2 --- /dev/null +++ b/src/core/script_manager.py @@ -0,0 +1,74 @@ +from PyQt5.QtCore import QObject, pyqtSignal +from pygments import highlight +from pygments.lexers import JavascriptLexer +from pygments.formatters import HtmlFormatter +from cryptography.fernet import Fernet +import json +import os + +class ScriptManager(QObject): + script_loaded = pyqtSignal(str, str) # name, content + script_saved = pyqtSignal(str) # name + + def __init__(self): + super().__init__() + self.scripts_dir = os.path.join(os.path.expanduser('~'), '.frida_gui', 'scripts') + self.key = Fernet.generate_key() + self.cipher_suite = Fernet(self.key) + self._ensure_dirs() + + def _ensure_dirs(self): + os.makedirs(self.scripts_dir, exist_ok=True) + + def save_script(self, name, content, encrypt=False): + """Save script with optional encryption""" + script_path = os.path.join(self.scripts_dir, f"{name}.js") + metadata_path = f"{script_path}.meta" + + if encrypt: + content = self.cipher_suite.encrypt(content.encode()).decode() + + with open(script_path, 'w') as f: + f.write(content) + + metadata = { + 'name': name, + 'encrypted': encrypt, + 'tags': [], + 'description': '', + 'version': '1.0' + } + + with open(metadata_path, 'w') as f: + json.dump(metadata, f) + + self.script_saved.emit(name) + + def load_script(self, name): + """Load script and handle decryption if needed""" + script_path = os.path.join(self.scripts_dir, f"{name}.js") + metadata_path = f"{script_path}.meta" + + try: + with open(metadata_path, 'r') as f: + metadata = json.load(f) + + with open(script_path, 'r') as f: + content = f.read() + + if metadata.get('encrypted', False): + content = self.cipher_suite.decrypt(content.encode()).decode() + + self.script_loaded.emit(name, content) + return content + except Exception as e: + print(f"Error loading script: {str(e)}") + return None + + def get_highlighted_script(self, content): + """Return HTML-formatted highlighted script""" + return highlight( + content, + JavascriptLexer(), + HtmlFormatter(style='monokai') + ) \ No newline at end of file diff --git a/src/core/script_templates.py b/src/core/script_templates.py new file mode 100644 index 0000000..840b9f3 --- /dev/null +++ b/src/core/script_templates.py @@ -0,0 +1,94 @@ +SCRIPT_TEMPLATES = { + 'API_LOGGING': ''' +Java.perform(function() { + // Common Android API hooks + var HttpURLConnection = Java.use('java.net.HttpURLConnection'); + var OkHttpClient = Java.use('okhttp3.OkHttpClient'); + var Retrofit = Java.use('retrofit2.Retrofit'); + + // HTTP URL Connection + HttpURLConnection.connect.implementation = function() { + console.log('[+] HttpURLConnection.connect() called'); + console.log('URL: ' + this.getURL().toString()); + console.log('Method: ' + this.getRequestMethod()); + this.connect(); + }; + + // OkHttp + OkHttpClient.newCall.implementation = function(request) { + console.log('[+] OkHttpClient.newCall() intercepted'); + console.log('URL: ' + request.url().toString()); + console.log('Method: ' + request.method()); + console.log('Headers: ' + request.headers().toString()); + return this.newCall(request); + }; + + // Retrofit + Retrofit.create.implementation = function(service) { + console.log('[+] Retrofit API Service created'); + console.log('Service: ' + service.toString()); + return this.create(service); + }; +}); +''', + + 'SSL_PINNING_BYPASS': ''' +Java.perform(function() { + var TrustManager = Java.registerClass({ + name: 'com.custom.TrustManager', + implements: [Java.use('javax.net.ssl.X509TrustManager')], + methods: { + checkClientTrusted: function(chain, authType) {}, + checkServerTrusted: function(chain, authType) {}, + getAcceptedIssuers: function() { return []; } + } + }); + + var SSLContext = Java.use('javax.net.ssl.SSLContext'); + SSLContext.init.implementation = function(keyManager, trustManager, secureRandom) { + console.log('[+] Bypassing SSL Pinning'); + var trustManagers = [TrustManager.$new()]; + this.init(keyManager, trustManagers, secureRandom); + }; +}); +''', + + 'APP_INFO': ''' +Java.perform(function() { + var ActivityThread = Java.use('android.app.ActivityThread'); + var Context = Java.use('android.content.Context'); + + var currentApplication = ActivityThread.currentApplication(); + var context = currentApplication.getApplicationContext(); + + console.log('\\n[App Information]'); + console.log('Package Name:', context.getPackageName()); + console.log('Process Name:', ActivityThread.currentProcessName()); + console.log('App Version:', context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName.value); + console.log('Target SDK:', context.getApplicationInfo().targetSdkVersion.value); + + // List all activities + var packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), + Java.use('android.content.pm.PackageManager').GET_ACTIVITIES.value); + console.log('\\n[Activities]'); + packageInfo.activities.value.forEach(function(activity) { + console.log(activity.name.value); + }); +}); +''', + + 'CUSTOM_API_LOGGER': ''' +Java.perform(function() { + // Add your custom API class/method hooks here + var targetClass = Java.use('com.example.api.ServiceClass'); + + targetClass.apiMethod.implementation = function() { + console.log('[+] API Call Intercepted'); + console.log('Arguments:', arguments); + var result = this.apiMethod.apply(this, arguments); + console.log('Result:', result); + return result; + }; +}); +''' +} \ No newline at end of file diff --git a/src/gui/__init__.py b/src/gui/__init__.py new file mode 100644 index 0000000..166ce0b --- /dev/null +++ b/src/gui/__init__.py @@ -0,0 +1 @@ +# Empty file to make the directory a Python package \ No newline at end of file diff --git a/src/gui/__pycache__/__init__.cpython-311.pyc b/src/gui/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..9f4b74e Binary files /dev/null and b/src/gui/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/gui/__pycache__/main_window.cpython-311.pyc b/src/gui/__pycache__/main_window.cpython-311.pyc new file mode 100644 index 0000000..8226c06 Binary files /dev/null and b/src/gui/__pycache__/main_window.cpython-311.pyc differ diff --git a/src/gui/main_window.py b/src/gui/main_window.py new file mode 100644 index 0000000..9714485 --- /dev/null +++ b/src/gui/main_window.py @@ -0,0 +1,811 @@ +from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QPushButton, QStackedWidget, QLabel, QListWidget, QTableWidget, QGroupBox, QCheckBox, QSpinBox, QMessageBox, QScrollArea, QGridLayout, QLineEdit, QTextEdit, QFrame, QDialog, QFileDialog) +from PyQt5.QtCore import Qt, QSize +import qtawesome as qta +from .widgets.device_panel import DevicePanel +from .widgets.process_panel import ProcessPanel +from .widgets.script_editor import ScriptEditorPanel +from .widgets.output_panel import OutputPanel +from .widgets.codeshare_browser import CodeShareBrowser +from .widgets.app_launcher import AppLauncher +from .widgets.process_monitor import ProcessMonitor +from .widgets.injection_panel import InjectionPanel +from .widgets.device_selector import DeviceSelector +from .widgets.history_page import HistoryPage +from core.history_manager import HistoryManager +from core.android_helper import AndroidHelper +import frida +import subprocess +import os +import json +import requests + +class FridaInjectorMainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Oliver Stankiewicz's | Frida Script Manager") + self.setMinimumSize(1400, 800) + self.history_manager = HistoryManager() + self.favorites = [] # Initialize favorites list + self.load_favorites() # Load favorites on startup + self.setup_ui() + + # Connect codeshare and favorites browsers + self.codeshare_browser.favorites_updated.connect(self.refresh_favorites) + + def setup_ui(self): + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # Main horizontal layout + layout = QHBoxLayout(central_widget) + + # Left sidebar for navigation + sidebar = self.create_sidebar() + layout.addWidget(sidebar) + + # Stacked widget for main content + self.stack = QStackedWidget() + layout.addWidget(self.stack) + + # Set layout ratio (1:4) + layout.setStretch(0, 1) + layout.setStretch(1, 4) + + # Initialize pages + self.init_pages() + + def create_sidebar(self): + sidebar = QWidget() + sidebar.setObjectName("sidebar") + sidebar.setStyleSheet(""" + QWidget#sidebar { + background-color: #2f3136; + border-right: 1px solid #202225; + min-width: 180px; + max-width: 180px; + } + QPushButton { + text-align: left; + padding: 6px 8px; + border: none; + border-radius: 4px; + margin: 1px 4px; + min-height: 32px; + max-height: 32px; + font-size: 13px; + } + QPushButton:hover { + background-color: #36393f; + } + QPushButton:checked { + background-color: #404249; + } + """) + + layout = QVBoxLayout(sidebar) + layout.setSpacing(1) + layout.setContentsMargins(0, 5, 0, 5) + + # Add navigation buttons + self.nav_buttons = {} + + nav_items = [ + ("home", "Home", "fa5s.home"), + ("inject", "Script Injection", "fa5s.syringe"), + ("codeshare", "CodeShare", "fa5s.cloud-download-alt"), + ("favorites", "Favorites", "fa5s.star"), + ("history", "History", "fa5s.history"), + ("monitor", "Process Monitor", "fa5s.desktop"), + ("settings", "Settings", "fa5s.cog") + ] + + for id_, text, icon in nav_items: + btn = QPushButton(qta.icon(icon, color='#b9bbbe'), f" {text}") + btn.setCheckable(True) + btn.clicked.connect(lambda checked, x=id_: self.switch_page(x)) + # Set icon size + btn.setIconSize(QSize(14, 14)) + self.nav_buttons[id_] = btn + layout.addWidget(btn) + + layout.addStretch() + + # Add status indicator at bottom + status_layout = QHBoxLayout() + status_layout.setContentsMargins(8, 4, 8, 4) + self.status_icon = QLabel() + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(8, 8)) + self.status_text = QLabel("Ready") + self.status_text.setStyleSheet("color: #b9bbbe; font-size: 12px;") + status_layout.addWidget(self.status_icon) + status_layout.addWidget(self.status_text) + layout.addLayout(status_layout) + + return sidebar + + def init_pages(self): + # Create pages + self.pages = { + 'home': self.create_home_page(), + 'inject': self.create_injection_page(), + 'codeshare': self.create_codeshare_page(), + 'favorites': self.create_favorites_page(), + 'history': self.create_history_page(), + 'monitor': self.create_monitor_page(), + 'settings': self.create_settings_page() + } + + # Add pages to stack + for page in self.pages.values(): + self.stack.addWidget(page) + + # Set initial page + self.switch_page('home') + + def switch_page(self, page_id): + # Update button states + for btn in self.nav_buttons.values(): + btn.setChecked(False) + self.nav_buttons[page_id].setChecked(True) + + # Switch to page + self.stack.setCurrentWidget(self.pages[page_id]) + + def create_home_page(self): + page = QWidget() + layout = QVBoxLayout(page) + layout.setSpacing(20) + + # Welcome header + header = QFrame() + header.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 10px; + padding: 20px; + } + QLabel { + color: white; + } + """) + header_layout = QVBoxLayout(header) + + title = QLabel("Welcome to Frida Script Manager") + title.setStyleSheet("font-size: 24px; font-weight: bold;") + + subtitle = QLabel("A powerful GUI tool for Frida script management and injection") + subtitle.setStyleSheet("font-size: 16px; color: #b9bbbe;") + + author = QLabel("Created by Oliver Stankiewicz") + author.setStyleSheet("font-size: 14px; color: #7289da;") + + header_layout.addWidget(title) + header_layout.addWidget(subtitle) + header_layout.addWidget(author) + + # Quick actions section + actions = QFrame() + actions.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 10px; + padding: 20px; + } + QLabel { + color: white; + } + QPushButton { + background-color: #7289da; + border-radius: 5px; + padding: 10px; + color: white; + text-align: left; + font-size: 14px; + } + QPushButton:hover { + background-color: #677bc4; + } + """) + actions_layout = QVBoxLayout(actions) + + actions_title = QLabel("Quick Actions") + actions_title.setStyleSheet("font-size: 18px; font-weight: bold; margin-bottom: 10px;") + + # Create action buttons + inject_btn = QPushButton(qta.icon('fa5s.syringe'), " Script Injection") + inject_btn.clicked.connect(lambda: self.switch_page('inject')) + + browse_btn = QPushButton(qta.icon('fa5s.cloud-download-alt'), " Browse CodeShare") + browse_btn.clicked.connect(lambda: self.switch_page('codeshare')) + + favorites_btn = QPushButton(qta.icon('fa5s.star'), " View Favorites") + favorites_btn.clicked.connect(lambda: self.switch_page('favorites')) + + monitor_btn = QPushButton(qta.icon('fa5s.desktop'), " Process Monitor") + monitor_btn.clicked.connect(lambda: self.switch_page('monitor')) + + actions_layout.addWidget(actions_title) + actions_layout.addWidget(inject_btn) + actions_layout.addWidget(browse_btn) + actions_layout.addWidget(favorites_btn) + actions_layout.addWidget(monitor_btn) + + # Add sections to main layout + layout.addWidget(header) + layout.addWidget(actions) + layout.addStretch() + + return page + + def create_injection_page(self): + page = QWidget() + layout = QVBoxLayout(page) + + # Add device selector + self.device_selector = DeviceSelector() + self.script_editor = ScriptEditorPanel() + self.injection_panel = InjectionPanel() + self.injection_panel.script_editor = self.script_editor + self.output_panel = OutputPanel() + + layout.addWidget(self.device_selector) + layout.addWidget(self.script_editor) + layout.addWidget(self.injection_panel) + layout.addWidget(self.output_panel) + + # Connect signals - ensure we're passing both device_id and pid + self.device_selector.process_selected.connect( + lambda device_id, pid: self.injection_panel.set_process(device_id, pid) + ) + self.injection_panel.injection_started.connect(self.inject_script) + self.injection_panel.injection_stopped.connect(self.stop_injection) + + return page + + def create_codeshare_page(self): + page = QWidget() + layout = QVBoxLayout(page) + + self.codeshare_browser = CodeShareBrowser() + # Connect codeshare signals here, after creating the browser + self.codeshare_browser.open_in_injector.connect(self.open_script_in_injector) + layout.addWidget(self.codeshare_browser) + + return page + + def create_favorites_page(self): + page = QWidget() + layout = QVBoxLayout(page) + + # Toolbar + toolbar = QHBoxLayout() + + # Search bar + search_input = QLineEdit() + search_input.setPlaceholderText("⌕ Search favorites...") + search_input.textChanged.connect(self.filter_favorites) + + # Upload button + upload_btn = QPushButton(qta.icon('fa5s.file-upload'), "Upload Script") + upload_btn.clicked.connect(self.upload_script) + + toolbar.addWidget(search_input) + toolbar.addWidget(upload_btn) + + # Grid for favorite scripts + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setStyleSheet(""" + QScrollArea { + border: none; + background-color: #36393f; + } + """) + + self.favorites_grid = QWidget() + self.favorites_grid_layout = QGridLayout(self.favorites_grid) + self.favorites_grid_layout.setSpacing(10) + scroll.setWidget(self.favorites_grid) + + # Add components to layout + layout.addLayout(toolbar) + layout.addWidget(scroll) + + # Initial population + self.refresh_favorites() + + return page + + def filter_favorites(self, text): + """Filter favorite scripts by search text""" + search_text = text.lower() + for i in range(self.favorites_grid_layout.count()): + widget = self.favorites_grid_layout.itemAt(i).widget() + if widget: + title = widget.findChild(QLabel).text().lower() + desc = widget.findChildren(QLabel)[-2].text().lower() + widget.setVisible(search_text in title or search_text in desc) + + def upload_script(self): + """Upload a custom script to favorites""" + file_path, _ = QFileDialog.getOpenFileName( + self, + "Upload Script", + "", + "JavaScript Files (*.js);;All Files (*.*)" + ) + + if file_path: + try: + with open(file_path, 'r') as f: + script_content = f.read() + + # Create script info + script_name = os.path.basename(file_path) + script_info = { + 'id': f"custom/{script_name}", + 'title': script_name, + 'author': 'Custom Script', + 'description': 'Uploaded custom script', + 'likes': 0, + 'seen': 0, + 'content': script_content + } + + # Add to favorites + self.add_to_favorites(script_info) + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to upload script: {str(e)}") + + def add_to_favorites(self, script_info): + """Add a script to favorites""" + # Add to favorites list if not already present + if not any(s['id'] == script_info['id'] for s in self.favorites): + self.favorites.append(script_info) + self.save_favorites() + + # Create card widget + card = self.create_favorite_card(script_info) + + # Add to grid + count = self.favorites_grid_layout.count() + row = count // 3 + col = count % 3 + self.favorites_grid_layout.addWidget(card, row, col) + + def create_favorite_card(self, script_info): + """Create a card widget for a favorite script""" + card = QFrame() + card.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 8px; + padding: 10px; + } + QFrame:hover { + background-color: #40444b; + } + """) + + layout = QVBoxLayout(card) + + # Title and metadata + title = QLabel(script_info['title']) + title.setStyleSheet("font-size: 14px; font-weight: bold; color: white;") + author = QLabel(f"by {script_info['author']}") + author.setStyleSheet("color: #b9bbbe;") + + # Description + desc = QLabel(script_info.get('description', '')[:100] + '...') + desc.setWordWrap(True) + desc.setStyleSheet("color: #b9bbbe;") + + # Action buttons + buttons = QHBoxLayout() + + view_btn = QPushButton("View") + view_btn.clicked.connect(lambda: self.view_favorite(script_info)) + + inject_btn = QPushButton("⚡ Inject") + inject_btn.clicked.connect(lambda: self.open_script_in_injector(script_info.get('content', ''))) + + remove_btn = QPushButton("✕ Remove") + remove_btn.clicked.connect(lambda: self.remove_from_favorites(script_info, card)) + + buttons.addWidget(view_btn) + buttons.addWidget(inject_btn) + buttons.addWidget(remove_btn) + buttons.addStretch() + + # Add all components + layout.addWidget(title) + layout.addWidget(author) + layout.addWidget(desc) + layout.addLayout(buttons) + + return card + + def view_favorite(self, script_info): + """View a favorite script's details""" + dialog = QDialog(self) + dialog.setWindowTitle(f"View Script - {script_info['title']}") + dialog.resize(800, 600) + + layout = QVBoxLayout(dialog) + + # Script content + content = QTextEdit() + content.setReadOnly(True) + content.setFont(QFont('Consolas', 11)) + content.setText(script_info.get('content', 'Script content not available')) + + # Action buttons + buttons = QHBoxLayout() + + copy_btn = QPushButton(" Copy") + copy_btn.clicked.connect(lambda: self.copy_to_clipboard(content.toPlainText())) + + inject_btn = QPushButton("⚡ Inject") + inject_btn.clicked.connect(lambda: self.open_script_in_injector(content.toPlainText())) + + buttons.addWidget(copy_btn) + buttons.addWidget(inject_btn) + buttons.addStretch() + + layout.addWidget(content) + layout.addLayout(buttons) + + dialog.exec_() + + def remove_from_favorites(self, script_info, card): + """Remove a script from favorites""" + reply = QMessageBox.question( + self, + "Remove Favorite", + f"Remove {script_info['title']} from favorites?", + QMessageBox.Yes | QMessageBox.No + ) + + if reply == QMessageBox.Yes: + # Remove from grid + card.setParent(None) + + # Remove from favorites list + if script_info['id'].startswith('custom/'): + self.favorites = [s for s in self.favorites if s['id'] != script_info['id']] + self.save_favorites() + elif hasattr(self.codeshare_browser, 'favorites'): + self.codeshare_browser.favorites.remove(script_info['id']) + self.codeshare_browser.save_favorites() + + # Refresh display + self.refresh_favorites() + + def copy_to_clipboard(self, text): + """Copy text to clipboard""" + QApplication.clipboard().setText(text) + QMessageBox.information(self, "✓ Success", "📋 Copied to clipboard!") + + def create_history_page(self): + page = QWidget() + layout = QVBoxLayout(page) + + self.history_page = HistoryPage(self.history_manager) + self.history_page.script_selected.connect(self.open_script_in_injector) + layout.addWidget(self.history_page) + + return page + + def create_monitor_page(self): + page = QWidget() + layout = QVBoxLayout(page) + + # Pass self (main window) to ProcessMonitor + self.process_monitor = ProcessMonitor(main_window=self) + layout.addWidget(self.process_monitor) + + return page + + def create_settings_page(self): + page = QWidget() + layout = QVBoxLayout(page) + + # Add settings categories + settings_categories = [ + ("General", [ + ("Auto-inject on launch", "checkbox"), + ("Save script history", "checkbox"), + ("Dark theme", "checkbox") + ]), + ("Script Editor", [ + ("Font size", "spinbox"), + ("Show line numbers", "checkbox"), + ("Auto-completion", "checkbox") + ]), + ("Monitoring", [ + ("Update interval", "spinbox"), + ("Show memory usage", "checkbox"), + ("Log to file", "checkbox") + ]) + ] + + for category, settings in settings_categories: + group = QGroupBox(category) + group_layout = QVBoxLayout() + + for setting_name, setting_type in settings: + setting_layout = QHBoxLayout() + setting_layout.addWidget(QLabel(setting_name)) + + if setting_type == "checkbox": + widget = QCheckBox() + elif setting_type == "spinbox": + widget = QSpinBox() + + setting_layout.addWidget(widget) + group_layout.addLayout(setting_layout) + + group.setLayout(group_layout) + layout.addWidget(group) + + layout.addStretch() + + return page + + def on_process_started(self, name, pid): + self.status_text.setText(f"Process started: {name} ({pid})") + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(12, 12)) + + def on_process_ended(self, name, pid): + self.status_text.setText(f"Process ended: {name} ({pid})") + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#f04747').pixmap(12, 12)) + + def on_memory_updated(self, pid, memory_mb): + # Update memory usage in process monitor + pass + + def inject_script(self, script_content, pid): + """Inject script into process""" + try: + if not script_content: + QMessageBox.warning(self, "Error", "No script to inject!") + return + + # Update status + self.status_text.setText(f"Injecting into PID: {pid}") + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#faa61a').pixmap(12, 12)) + + # Get device and process info + device_id = self.device_selector.current_device + process_info = self.device_selector.get_selected_process_info() + + if not process_info: + raise Exception("No process selected") + + device = frida.get_device(device_id) + + # Check if Android device needs frida-server + if device.type == 'usb': + if not AndroidHelper.is_frida_running(device_id): + self.output_panel.append_output("[*] Starting frida-server on device...") + if not AndroidHelper.start_frida_server(device_id): + raise Exception("Failed to start frida-server") + self.output_panel.append_output("[+] frida-server started") + # Re-get device after starting server + device = frida.get_device(device_id) + + try: + # Try to attach first + session = device.attach(pid) + self.output_panel.append_output(f"[+] Successfully attached to PID: {pid}") + except frida.ProcessNotFoundError: + # If attach fails, try to spawn + try: + if device.type == 'local': + # For local processes, use executable path + import psutil + process = psutil.Process(pid) + executable = process.exe() + pid = device.spawn([executable]) + self.output_panel.append_output(f"[+] Spawned process with PID: {pid}") + else: + # For Android/remote devices + if device.type == 'usb': + package_name = process_info['name'] + pid = device.spawn([package_name]) + self.output_panel.append_output(f"[+] Spawned Android app: {package_name}") + else: + pid = device.spawn([process_info['name']]) + + session = device.attach(pid) + device.resume(pid) + except Exception as e: + raise Exception(f"Failed to spawn process: {str(e)}") + + # Create and load script + script = session.create_script(script_content) + + def on_message(message, data): + if message['type'] == 'send': + self.output_panel.append_output(f"[*] {message['payload']}") + elif message['type'] == 'error': + self.output_panel.append_output(f"[!] {message['description']}") + + script.on('message', on_message) + script.load() + + # Update status on success + self.status_text.setText(f"Successfully injected into PID: {pid}") + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(12, 12)) + + # Store session and script + self.current_session = session + self.current_script = script + + # Show success message + self.output_panel.append_output(f"[+] Script loaded successfully") + + # Add history entry + self.history_manager.add_entry('script_injection', { + 'script': script_content, + 'pid': pid, + 'device': device_id, + 'status': 'success' + }) + + except Exception as e: + error_msg = f"Injection failed: {str(e)}" + self.output_panel.append_output(f"[-] {error_msg}") + QMessageBox.critical(self, "Error", error_msg) + + # Add history entry + self.history_manager.add_entry('script_injection', { + 'script': script_content, + 'pid': pid, + 'device': device_id, + 'status': 'failed', + 'error': str(e) + }) + + finally: + if hasattr(self, 'injection_panel'): + self.injection_panel.reset_ui() + + def stop_injection(self): + """Stop the current injection""" + try: + if hasattr(self, 'current_script') and self.current_script: + self.current_script.unload() + if hasattr(self, 'current_session') and self.current_session: + self.current_session.detach() + + self.current_script = None + self.current_session = None + + self.output_panel.append_output("[*] Script injection stopped") + self.status_text.setText("Ready") + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(12, 12)) + + except Exception as e: + error_msg = f"Error stopping injection: {str(e)}" + self.output_panel.append_output(f"[-] {error_msg}") + QMessageBox.critical(self, "Error", error_msg) + + def on_process_selected(self, device_id, pid): + self.current_device = device_id + self.current_pid = pid + self.status_text.setText(f"Selected PID: {pid} on device: {device_id}") + + def open_in_injector(self, device_id, pid): + """Open the selected process in the injector tab""" + # Switch to injector tab + self.switch_page('inject') + + # Select the device and process + self.device_selector.select_device(device_id) + self.device_selector.select_process(pid) + + def open_script_in_injector(self, code): + """Open a script in the injector page""" + # Switch to injector page + self.switch_page('inject') + + # Set the script content + self.script_editor.set_script(code) + + def load_favorites(self): + """Load favorites from file""" + try: + favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json') + if os.path.exists(favorites_file): + with open(favorites_file, 'r') as f: + data = json.load(f) + self.favorites = data.get('scripts', []) + except Exception as e: + print(f"Error loading favorites: {e}") + self.favorites = [] + + def save_favorites(self): + """Save favorites to file""" + try: + favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json') + os.makedirs(os.path.dirname(favorites_file), exist_ok=True) + with open(favorites_file, 'w') as f: + json.dump({'scripts': self.favorites}, f) + except Exception as e: + print(f"Error saving favorites: {e}") + + def refresh_favorites(self): + """Refresh the favorites page""" + # Clear existing grid + for i in reversed(range(self.favorites_grid_layout.count())): + widget = self.favorites_grid_layout.itemAt(i).widget() + if widget: + widget.setParent(None) + + # Get all favorites + try: + # Combine CodeShare favorites and custom scripts + all_favorites = [] + + # Add CodeShare favorites + if hasattr(self.codeshare_browser, 'favorites'): + response = requests.get(self.codeshare_browser.api_url) + codeshare_scripts = response.json() + for script in codeshare_scripts: + if script['id'] in self.codeshare_browser.favorites: + all_favorites.append(script) + + # Add custom scripts from our favorites + all_favorites.extend([s for s in self.favorites if s['id'].startswith('custom/')]) + + if all_favorites: + # Add scripts to grid + for idx, script_info in enumerate(all_favorites): + row = idx // 3 + col = idx % 3 + card = self.create_favorite_card(script_info) + self.favorites_grid_layout.addWidget(card, row, col) + else: + # Show message if no favorites + msg = QLabel("No favorite scripts yet.\nBrowse scripts and click the ★ to add favorites!") + msg.setAlignment(Qt.AlignCenter) + msg.setStyleSheet(""" + color: #b9bbbe; + font-size: 14px; + padding: 20px; + """) + self.favorites_grid_layout.addWidget(msg, 0, 0, 1, 3) + + except Exception as e: + error_msg = QLabel(f"Error loading favorites: {str(e)}") + error_msg.setStyleSheet("color: #ff4444;") + self.favorites_grid_layout.addWidget(error_msg, 0, 0, 1, 3) + + def cleanup(self): + """Clean up resources to prevent memory leaks""" + # Stop monitoring + if hasattr(self, 'process_monitor'): + self.process_monitor.stop_monitoring() + + # Clear history + if hasattr(self, 'history_manager'): + self.history_manager.save_history() + + # Clear device selector + if hasattr(self, 'device_selector'): + self.device_selector.cleanup() + + # Clear any running scripts + self.stop_injection() + + # Clear references + self.current_script = None + self.current_session = None + + def closeEvent(self, event): + """Handle window close event""" + self.cleanup() + event.accept() \ No newline at end of file diff --git a/src/gui/widgets/__init__.py b/src/gui/widgets/__init__.py new file mode 100644 index 0000000..166ce0b --- /dev/null +++ b/src/gui/widgets/__init__.py @@ -0,0 +1 @@ +# Empty file to make the directory a Python package \ No newline at end of file diff --git a/src/gui/widgets/__pycache__/__init__.cpython-311.pyc b/src/gui/widgets/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..6cb549e Binary files /dev/null and b/src/gui/widgets/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/app_launcher.cpython-311.pyc b/src/gui/widgets/__pycache__/app_launcher.cpython-311.pyc new file mode 100644 index 0000000..e3c9bb5 Binary files /dev/null and b/src/gui/widgets/__pycache__/app_launcher.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/codeshare_browser.cpython-311.pyc b/src/gui/widgets/__pycache__/codeshare_browser.cpython-311.pyc new file mode 100644 index 0000000..27b82b2 Binary files /dev/null and b/src/gui/widgets/__pycache__/codeshare_browser.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/device_panel.cpython-311.pyc b/src/gui/widgets/__pycache__/device_panel.cpython-311.pyc new file mode 100644 index 0000000..1a3dbcd Binary files /dev/null and b/src/gui/widgets/__pycache__/device_panel.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/device_selector.cpython-311.pyc b/src/gui/widgets/__pycache__/device_selector.cpython-311.pyc new file mode 100644 index 0000000..248fa6b Binary files /dev/null and b/src/gui/widgets/__pycache__/device_selector.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/history_page.cpython-311.pyc b/src/gui/widgets/__pycache__/history_page.cpython-311.pyc new file mode 100644 index 0000000..6b97a48 Binary files /dev/null and b/src/gui/widgets/__pycache__/history_page.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/injection_panel.cpython-311.pyc b/src/gui/widgets/__pycache__/injection_panel.cpython-311.pyc new file mode 100644 index 0000000..4883d17 Binary files /dev/null and b/src/gui/widgets/__pycache__/injection_panel.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/output_panel.cpython-311.pyc b/src/gui/widgets/__pycache__/output_panel.cpython-311.pyc new file mode 100644 index 0000000..108fb66 Binary files /dev/null and b/src/gui/widgets/__pycache__/output_panel.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/process_monitor.cpython-311.pyc b/src/gui/widgets/__pycache__/process_monitor.cpython-311.pyc new file mode 100644 index 0000000..3f50df9 Binary files /dev/null and b/src/gui/widgets/__pycache__/process_monitor.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/process_panel.cpython-311.pyc b/src/gui/widgets/__pycache__/process_panel.cpython-311.pyc new file mode 100644 index 0000000..6437ca7 Binary files /dev/null and b/src/gui/widgets/__pycache__/process_panel.cpython-311.pyc differ diff --git a/src/gui/widgets/__pycache__/script_editor.cpython-311.pyc b/src/gui/widgets/__pycache__/script_editor.cpython-311.pyc new file mode 100644 index 0000000..95c77df Binary files /dev/null and b/src/gui/widgets/__pycache__/script_editor.cpython-311.pyc differ diff --git a/src/gui/widgets/app_launcher.py b/src/gui/widgets/app_launcher.py new file mode 100644 index 0000000..1738069 --- /dev/null +++ b/src/gui/widgets/app_launcher.py @@ -0,0 +1,306 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QLineEdit, QComboBox, QLabel, QTableWidget, + QTableWidgetItem, QMenu, QAction, QCheckBox, + QFileDialog, QGroupBox) +from PyQt5.QtCore import pyqtSignal, Qt +import subprocess +import json +import os +import qtawesome as qta +import sys + +class AppLauncher(QWidget): + app_launched = pyqtSignal(str, int) # package_name, pid + script_selected = pyqtSignal(str) # script content + + def __init__(self): + super().__init__() + self.favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json') + self.scripts_dir = os.path.join(os.path.expanduser('~'), '.frida_gui', 'scripts') + self.load_favorites() + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Quick Launch Section + quick_launch_group = QGroupBox("Quick Launch") + quick_launch_layout = QVBoxLayout() + + # Package input + package_layout = QHBoxLayout() + self.package_input = QLineEdit() + self.package_input.setPlaceholderText("Enter package name or path...") + self.launch_button = QPushButton("Launch") + self.launch_button.setIcon(qta.icon('fa5s.play')) + self.launch_button.clicked.connect(self.launch_app) + package_layout.addWidget(self.package_input) + package_layout.addWidget(self.launch_button) + + # Script Selection + script_layout = QHBoxLayout() + self.script_input = QLineEdit() + self.script_input.setPlaceholderText("Select Frida script file...") + self.script_input.setReadOnly(True) + + self.browse_script_btn = QPushButton("Browse") + self.browse_script_btn.setIcon(qta.icon('fa5s.folder-open')) + self.browse_script_btn.clicked.connect(self.browse_script) + + self.edit_script_btn = QPushButton("Edit") + self.edit_script_btn.setIcon(qta.icon('fa5s.edit')) + self.edit_script_btn.clicked.connect(self.edit_script) + self.edit_script_btn.setEnabled(False) + + script_layout.addWidget(self.script_input) + script_layout.addWidget(self.browse_script_btn) + script_layout.addWidget(self.edit_script_btn) + + # Launch Options + options_layout = QHBoxLayout() + self.debug_check = QCheckBox("Debug Mode") + self.wait_check = QCheckBox("Wait for Debugger") + self.inject_check = QCheckBox("Auto-Inject Script") + self.inject_check.toggled.connect(self.toggle_script_selection) + + options_layout.addWidget(self.debug_check) + options_layout.addWidget(self.wait_check) + options_layout.addWidget(self.inject_check) + + quick_launch_layout.addLayout(package_layout) + quick_launch_layout.addLayout(script_layout) + quick_launch_layout.addLayout(options_layout) + quick_launch_group.setLayout(quick_launch_layout) + + # Favorites Section + favorites_group = QGroupBox("Favorites") + favorites_layout = QVBoxLayout() + + self.favorites_table = QTableWidget(0, 4) # Added column for script + self.favorites_table.setHorizontalHeaderLabels(["Name", "Package", "Script", "Actions"]) + self.favorites_table.horizontalHeader().setStretchLastSection(True) + self.favorites_table.setContextMenuPolicy(Qt.CustomContextMenu) + self.favorites_table.customContextMenuRequested.connect(self.show_context_menu) + + favorites_layout.addWidget(self.favorites_table) + favorites_group.setLayout(favorites_layout) + + # Recent Apps Section + recent_group = QGroupBox("Recent Apps") + recent_layout = QHBoxLayout() + self.recent_combo = QComboBox() + self.recent_combo.setPlaceholderText("Recent Apps") + recent_launch_btn = QPushButton("Launch Recent") + recent_launch_btn.clicked.connect(self.launch_recent) + + recent_layout.addWidget(self.recent_combo) + recent_layout.addWidget(recent_launch_btn) + recent_group.setLayout(recent_layout) + + # Add all sections to main layout + layout.addWidget(quick_launch_group) + layout.addWidget(favorites_group) + layout.addWidget(recent_group) + + # Populate favorites + self.update_favorites_table() + + def browse_script(self): + file_name, _ = QFileDialog.getOpenFileName( + self, + "Select Frida Script", + self.scripts_dir, + "JavaScript Files (*.js);;All Files (*.*)" + ) + + if file_name: + self.script_input.setText(file_name) + self.edit_script_btn.setEnabled(True) + + # Read script content + try: + with open(file_name, 'r') as f: + script_content = f.read() + self.script_selected.emit(script_content) + except Exception as e: + print(f"Error reading script: {str(e)}") + + def edit_script(self): + script_path = self.script_input.text() + if script_path and os.path.exists(script_path): + # You can implement your own script editor or use system default + if sys.platform == 'win32': + os.startfile(script_path) + elif sys.platform == 'darwin': + subprocess.run(['open', script_path]) + else: + subprocess.run(['xdg-open', script_path]) + + def toggle_script_selection(self, enabled): + self.script_input.setEnabled(enabled) + self.browse_script_btn.setEnabled(enabled) + self.edit_script_btn.setEnabled(enabled and bool(self.script_input.text())) + + def add_to_favorites(self, name, package, script_path=None): + self.favorites[name] = { + 'package': package, + 'script': script_path + } + self.save_favorites() + self.update_favorites_table() + + def update_favorites_table(self): + self.favorites_table.setRowCount(0) + for name, data in self.favorites.items(): + row = self.favorites_table.rowCount() + self.favorites_table.insertRow(row) + + name_item = QTableWidgetItem(name) + package_item = QTableWidgetItem(data['package']) + script_item = QTableWidgetItem(data.get('script', '')) + + launch_btn = QPushButton("Launch") + launch_btn.clicked.connect( + lambda checked, p=data['package'], s=data.get('script'): + self.launch_favorite(p, s) + ) + + self.favorites_table.setItem(row, 0, name_item) + self.favorites_table.setItem(row, 1, package_item) + self.favorites_table.setItem(row, 2, script_item) + self.favorites_table.setCellWidget(row, 3, launch_btn) + + def launch_favorite(self, package, script_path=None): + if script_path: + try: + with open(script_path, 'r') as f: + script_content = f.read() + self.script_selected.emit(script_content) + except Exception as e: + print(f"Error reading script: {str(e)}") + self.launch_app(package) + + def show_context_menu(self, position): + menu = QMenu() + remove_action = QAction("Remove from Favorites", self) + remove_action.triggered.connect(self.remove_selected_favorite) + + edit_script_action = QAction("Edit Script", self) + edit_script_action.triggered.connect(self.edit_selected_script) + + menu.addAction(remove_action) + menu.addAction(edit_script_action) + menu.exec_(self.favorites_table.mapToGlobal(position)) + + def edit_selected_script(self): + current_row = self.favorites_table.currentRow() + if current_row >= 0: + script_path = self.favorites_table.item(current_row, 2).text() + if script_path: + if sys.platform == 'win32': + os.startfile(script_path) + elif sys.platform == 'darwin': + subprocess.run(['open', script_path]) + else: + subprocess.run(['xdg-open', script_path]) + + def launch_app(self, package_name=None): + if not package_name: + package_name = self.package_input.text() + + try: + cmd = ['adb', 'shell', 'am', 'start'] + + if self.debug_check.isChecked(): + cmd.extend(['-D']) + + if self.wait_check.isChecked(): + cmd.extend(['-W']) + + cmd.extend(['-n', f'{package_name}/{package_name}.MainActivity']) + + process = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + stdout, stderr = process.communicate() + + if process.returncode == 0: + self.add_to_recent(package_name) + # Get PID of launched app + pid_cmd = ['adb', 'shell', 'pidof', package_name] + pid = subprocess.check_output(pid_cmd).decode().strip() + if pid: + self.app_launched.emit(package_name, int(pid)) + else: + raise Exception(stderr.decode()) + + except Exception as e: + print(f"Error launching app: {str(e)}") + + def add_to_favorites(self, name, package): + self.favorites[name] = package + self.save_favorites() + self.update_favorites_table() + + def remove_from_favorites(self, name): + if name in self.favorites: + del self.favorites[name] + self.save_favorites() + self.update_favorites_table() + + def load_favorites(self): + try: + if os.path.exists(self.favorites_file): + with open(self.favorites_file, 'r') as f: + self.favorites = json.load(f) + else: + self.favorites = {} + except: + self.favorites = {} + + def save_favorites(self): + os.makedirs(os.path.dirname(self.favorites_file), exist_ok=True) + with open(self.favorites_file, 'w') as f: + json.dump(self.favorites, f) + + def update_favorites_table(self): + self.favorites_table.setRowCount(0) + for name, package in self.favorites.items(): + row = self.favorites_table.rowCount() + self.favorites_table.insertRow(row) + + name_item = QTableWidgetItem(name) + package_item = QTableWidgetItem(package) + + launch_btn = QPushButton("Launch") + launch_btn.clicked.connect(lambda checked, p=package: self.launch_app(p)) + + self.favorites_table.setItem(row, 0, name_item) + self.favorites_table.setItem(row, 1, package_item) + self.favorites_table.setCellWidget(row, 2, launch_btn) + + def show_context_menu(self, position): + menu = QMenu() + remove_action = QAction("Remove from Favorites", self) + remove_action.triggered.connect(lambda: self.remove_selected_favorite()) + menu.addAction(remove_action) + menu.exec_(self.favorites_table.mapToGlobal(position)) + + def remove_selected_favorite(self): + current_row = self.favorites_table.currentRow() + if current_row >= 0: + name = self.favorites_table.item(current_row, 0).text() + self.remove_from_favorites(name) + + def add_to_recent(self, package_name): + current_text = self.recent_combo.currentText() + if current_text != package_name: + self.recent_combo.insertItem(0, package_name) + if self.recent_combo.count() > 10: + self.recent_combo.removeItem(10) + + def launch_recent(self): + package_name = self.recent_combo.currentText() + if package_name: + self.launch_app(package_name) \ No newline at end of file diff --git a/src/gui/widgets/codeshare_browser.py b/src/gui/widgets/codeshare_browser.py new file mode 100644 index 0000000..b195f4a --- /dev/null +++ b/src/gui/widgets/codeshare_browser.py @@ -0,0 +1,539 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, + QLineEdit, QPushButton, QListWidget, + QTextBrowser, QSplitter, QComboBox, + QLabel, QProgressBar, QMessageBox, QGroupBox, QDialog, QTabWidget, QMenu, QFrame, QTableWidget, QHeaderView, QFileDialog, QScrollArea, QGridLayout, QTextEdit) +from PyQt5.QtCore import pyqtSignal, Qt, QThread, QUrl +from PyQt5.QtGui import QFont, QDesktopServices, QIcon +import aiohttp +import asyncio +import qtawesome as qta +import json +import os +from bs4 import BeautifulSoup +from core.script_templates import SCRIPT_TEMPLATES +from core.script_history import ScriptHistory +import time +import requests +import threading +import re + +class CodeFetcher(QThread): + code_fetched = pyqtSignal(str) + error_occurred = pyqtSignal(str) + + def __init__(self, url): + super().__init__() + self.url = url + + def run(self): + try: + response = requests.get(self.url) + if response.status_code != 200: + self.error_occurred.emit(f"HTTP Error: {response.status_code}") + return + + # Find the script content in the Vue.js data + script_match = re.search(r'projectSource: "(.*?)",', response.text, re.DOTALL) + if script_match: + # Unescape the JavaScript string + code = script_match.group(1).encode().decode('unicode_escape') + self.code_fetched.emit(code) + else: + # Try alternative method - look for the editor content + soup = BeautifulSoup(response.text, 'html.parser') + editor_div = soup.find('div', {'id': 'editor'}) + if editor_div and editor_div.string: + self.code_fetched.emit(editor_div.string) + else: + self.error_occurred.emit("Could not find script content") + + except Exception as e: + self.error_occurred.emit(f"Error fetching script: {str(e)}") + +class CodeShareBrowser(QWidget): + script_selected = pyqtSignal(str) # For injector + open_in_injector = pyqtSignal(str) # New signal for opening in injector + favorites_updated = pyqtSignal() # New signal for favorites updates + + def __init__(self): + super().__init__() + self.scripts_cache = {} + self.api_url = "https://konsumer.js.org/frida-codeshare/codeshare.json" + self.favorites = [] # Initialize as empty list + self.load_favorites() + self.setup_ui() + + def load_favorites(self): + """Load favorites from file""" + try: + favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json') + if os.path.exists(favorites_file): + with open(favorites_file, 'r') as f: + data = json.load(f) + # Make sure we get a list, even if loading from a dict + if isinstance(data, dict): + self.favorites = data.get('favorites', []) + else: + self.favorites = data if isinstance(data, list) else [] + else: + self.favorites = [] + except Exception as e: + print(f"Error loading favorites: {e}") + self.favorites = [] + + def save_favorites(self): + """Save favorites to file""" + try: + favorites_file = os.path.join(os.path.expanduser('~'), '.frida_gui', 'favorites.json') + os.makedirs(os.path.dirname(favorites_file), exist_ok=True) + with open(favorites_file, 'w') as f: + # Save as a simple list + json.dump(self.favorites, f) + except Exception as e: + print(f"Error saving favorites: {e}") + + def is_favorite(self, script_id): + """Check if script is favorited""" + return script_id in self.favorites + + def toggle_favorite(self, script_info): + """Toggle favorite status of script""" + script_id = script_info['id'] + if script_id in self.favorites: + self.favorites.remove(script_id) + else: + self.favorites.append(script_id) + self.save_favorites() + self.refresh_favorites() + self.favorites_updated.emit() # Emit signal when favorites change + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Create tab widget + self.tab_widget = QTabWidget() + + # Create tabs + self.browse_tab = QWidget() + self.favorites_tab = QWidget() + + self.setup_browse_tab() + self.setup_favorites_tab() + + # Add tabs + self.tab_widget.addTab(self.browse_tab, "Browse") + self.tab_widget.addTab(self.favorites_tab, "★ Favorites") + + layout.addWidget(self.tab_widget) + + self.refresh_scripts() + + def setup_browse_tab(self): + """Setup the browse tab (existing functionality)""" + layout = QVBoxLayout(self.browse_tab) + + # Move existing toolbar and grid here + toolbar = QHBoxLayout() + + # Search bar + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("⌕ Search scripts...") + self.search_input.textChanged.connect(self.filter_scripts) + + # Category filter + self.category_combo = QComboBox() + self.category_combo.addItems(['All', 'Android', 'iOS', 'Windows', 'Linux', 'macOS']) + self.category_combo.currentTextChanged.connect(self.filter_scripts) + + # Sort options + self.sort_combo = QComboBox() + self.sort_combo.addItems(['★ Most Popular', '👁 Most Viewed', '⏲ Latest']) + self.sort_combo.currentTextChanged.connect(self.refresh_scripts) + + toolbar.addWidget(self.search_input) + toolbar.addWidget(self.category_combo) + toolbar.addWidget(self.sort_combo) + + # Grid layout for scripts + self.grid_widget = QWidget() + self.grid_layout = QGridLayout(self.grid_widget) + self.grid_layout.setSpacing(10) + + # Scroll area for grid + scroll = QScrollArea() + scroll.setWidget(self.grid_widget) + scroll.setWidgetResizable(True) + scroll.setStyleSheet(""" + QScrollArea { + border: none; + background-color: #36393f; + } + """) + + # Add all components + layout.addLayout(toolbar) + layout.addWidget(scroll) + + def setup_favorites_tab(self): + """Setup the favorites tab""" + layout = QVBoxLayout(self.favorites_tab) + + # Create grid for favorite scripts + self.favorites_grid = QWidget() + self.favorites_grid_layout = QGridLayout(self.favorites_grid) + self.favorites_grid_layout.setSpacing(10) + + # Scroll area for favorites + scroll = QScrollArea() + scroll.setWidget(self.favorites_grid) + scroll.setWidgetResizable(True) + scroll.setStyleSheet(""" + QScrollArea { + border: none; + background-color: #36393f; + } + """) + + # Add to layout + layout.addWidget(scroll) + + # Initial population of favorites + self.refresh_favorites() + + def refresh_favorites(self): + """Refresh the favorites grid""" + # Clear existing favorites grid + for i in reversed(range(self.favorites_grid_layout.count())): + widget = self.favorites_grid_layout.itemAt(i).widget() + if widget: + widget.setParent(None) + + # Get favorite scripts + try: + # Get all scripts + response = requests.get(self.api_url) + all_scripts = response.json() + + # Filter to only favorited scripts + favorite_scripts = [s for s in all_scripts if s['id'] in self.favorites] + + if favorite_scripts: + # Add scripts to grid + for idx, script_info in enumerate(favorite_scripts): + row = idx // 3 + col = idx % 3 + card = self.create_script_card(script_info) + self.favorites_grid_layout.addWidget(card, row, col) + else: + # Show message if no favorites + msg = QLabel("No favorite scripts yet.\nBrowse scripts and click the ★ to add favorites!") + msg.setAlignment(Qt.AlignCenter) + msg.setStyleSheet(""" + color: #b9bbbe; + font-size: 14px; + padding: 20px; + """) + self.favorites_grid_layout.addWidget(msg, 0, 0, 1, 3) + + except Exception as e: + print(f"Error refreshing favorites: {e}") + error_msg = QLabel(f"Error loading favorites: {str(e)}") + error_msg.setStyleSheet("color: #ff4444;") + self.favorites_grid_layout.addWidget(error_msg, 0, 0, 1, 3) + + def fetch_scripts(self): + """Fetch scripts from API""" + try: + response = requests.get(self.api_url) + scripts = response.json() + + # Sort scripts + sort_option = self.sort_combo.currentText() + if sort_option == 'Most Popular': + scripts.sort(key=lambda x: x.get('likes', 0), reverse=True) + elif sort_option == 'Most Viewed': + scripts.sort(key=lambda x: x.get('seen', 0), reverse=True) + + return scripts + except Exception as e: + print(f"Error fetching scripts: {e}") + return [] + + def create_script_card(self, script_info): + """Create a card widget for a script""" + card = QFrame() + card.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 8px; + padding: 10px; + } + QFrame:hover { + background-color: #40444b; + } + QLabel { + color: white; + } + """) + + layout = QVBoxLayout(card) + + # Title + title = QLabel(script_info['title']) + title.setStyleSheet("font-size: 14px; font-weight: bold;") + title.setWordWrap(True) + + # Author + author = QLabel(f"by {script_info['author']}") + author.setStyleSheet("color: #b9bbbe;") + + # Stats + stats = QHBoxLayout() + stars = QLabel(f"★ {script_info.get('likes', 0)}") + views = QLabel(f"👁 {script_info.get('seen', 0)}") + stats.addWidget(stars) + stats.addWidget(views) + + # Description + desc = QLabel(script_info.get('description', '')[:100] + '...') + desc.setWordWrap(True) + desc.setStyleSheet("color: #b9bbbe;") + + # Action buttons + buttons = QHBoxLayout() + + view_btn = QPushButton("View") + view_btn.clicked.connect(lambda: self.fetch_script_code(script_info)) + + fav_btn = QPushButton() + if self.is_favorite(script_info['id']): + fav_btn.setIcon(QIcon()) + fav_btn.setText("★") + else: + fav_btn.setIcon(QIcon()) + fav_btn.setText("☆") + fav_btn.setStyleSheet("color: #b9bbbe;") + fav_btn.clicked.connect(lambda: self.toggle_favorite_ui(script_info, fav_btn)) + + buttons.addWidget(view_btn) + buttons.addWidget(fav_btn) + buttons.addStretch() + + layout.addWidget(title) + layout.addWidget(author) + layout.addLayout(stats) + layout.addWidget(desc) + layout.addLayout(buttons) + + return card + + def fetch_script_code(self, script_info): + """Fetch and show script code""" + # Remove author name from ID if it's included + script_id = script_info['id'].replace(f"{script_info['author']}/", "") + url = f"https://codeshare.frida.re/@{script_info['author']}/{script_id}" + print(f"Fetching script from: {url}") # Debug print + + # Create preview dialog + dialog = QDialog(self) + dialog.setWindowTitle(f"Frida CodeShare - {script_info['title']}") + dialog.resize(1000, 800) + dialog.setStyleSheet(""" + QDialog { + background-color: #2f3136; + } + QLabel { + color: white; + } + QPushButton { + background-color: #7289da; + color: white; + padding: 8px 16px; + border-radius: 4px; + min-width: 100px; + } + QPushButton:hover { + background-color: #677bc4; + } + QTextEdit { + background-color: #36393f; + color: #dcddde; + border: none; + border-radius: 4px; + padding: 10px; + font-family: 'Consolas', monospace; + } + """) + + layout = QVBoxLayout(dialog) + layout.setSpacing(15) + + # Header + header = QHBoxLayout() + title = QLabel(script_info['title']) + title.setStyleSheet("font-size: 18px; font-weight: bold;") + author = QLabel(f"by {script_info['author']}") + author.setStyleSheet("color: #b9bbbe;") + header.addWidget(title) + header.addWidget(author) + header.addStretch() + + # Stats + stats = QHBoxLayout() + likes = QLabel(f"★ {script_info.get('likes', 0)}") + views = QLabel(f"👁 {script_info.get('seen', 0)}") + stats.addWidget(likes) + stats.addWidget(views) + stats.addStretch() + + # Description + desc = QLabel(script_info.get('description', '')) + desc.setWordWrap(True) + desc.setStyleSheet("color: #b9bbbe; padding: 10px;") + + # Code preview + code_view = QTextEdit() + code_view.setReadOnly(True) + code_view.setFont(QFont('Consolas', 11)) + code_view.setLineWrapMode(QTextEdit.NoWrap) + code_view.setText("Loading script...") + + # Usage instructions + usage = QLabel(f"Try this code out by running:\n$ frida --codeshare {script_info['author']}/{script_info['id']} -f YOUR_BINARY") + usage.setStyleSheet(""" + background-color: #202225; + padding: 10px; + border-radius: 4px; + font-family: 'Consolas', monospace; + """) + + # Action buttons + buttons = QHBoxLayout() + + copy_btn = QPushButton(qta.icon('fa5s.copy'), "⎘ Copy Code") + copy_btn.clicked.connect(lambda: self.copy_to_clipboard(code_view.toPlainText())) + + inject_btn = QPushButton(qta.icon('fa5s.syringe'), "⚡ Open in Injector") + inject_btn.clicked.connect(lambda: self.open_in_injector_page(code_view.toPlainText(), dialog)) + + download_btn = QPushButton(qta.icon('fa5s.download'), "⤓ Download") + download_btn.clicked.connect(lambda: self.download_script(script_info['title'], code_view.toPlainText())) + + open_btn = QPushButton(qta.icon('fa5s.external-link-alt'), "⧉ Open in Browser") + open_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url))) + + buttons.addWidget(copy_btn) + buttons.addWidget(inject_btn) + buttons.addWidget(download_btn) + buttons.addWidget(open_btn) + buttons.addStretch() + + # Add all components + layout.addLayout(header) + layout.addLayout(stats) + layout.addWidget(desc) + layout.addWidget(usage) + layout.addWidget(code_view) + layout.addLayout(buttons) + + dialog.show() + + # Create and start the code fetcher thread + self.code_fetcher = CodeFetcher(url) + self.code_fetcher.code_fetched.connect(code_view.setText) + self.code_fetcher.error_occurred.connect(lambda err: code_view.setText(f"Error loading script: {err}")) + self.code_fetcher.start() + + def refresh_scripts(self): + """Refresh scripts from API""" + # Clear existing grid + for i in reversed(range(self.grid_layout.count())): + self.grid_layout.itemAt(i).widget().setParent(None) + + # Fetch and sort scripts + scripts = self.fetch_scripts() + + # Add all scripts to grid + for idx, script_info in enumerate(scripts): + row = idx // 3 + col = idx % 3 + card = self.create_script_card(script_info) + self.grid_layout.addWidget(card, row, col) + + # Refresh favorites tab + self.refresh_favorites() + + def add_script(self, script_info): + """Add a script card to the grid""" + # Calculate grid position + count = self.grid_layout.count() + row = count // 3 + col = count % 3 + + # Create and add card + card = self.create_script_card(script_info) + self.grid_layout.addWidget(card, row, col) + + # Cache the script + self.scripts_cache[script_info['id']] = script_info + + def filter_scripts(self): + """Filter visible scripts based on search and category""" + search_text = self.search_input.text().lower() + category = self.category_combo.currentText() + + # Show/hide cards based on filters + for i in range(self.grid_layout.count()): + widget = self.grid_layout.itemAt(i).widget() + if widget: + title = widget.findChild(QLabel).text().lower() + desc = widget.findChildren(QLabel)[-2].text().lower() # Description label + + show = True + if search_text and search_text not in title and search_text not in desc: + show = False + if category != 'All' and category not in desc: + show = False + + widget.setVisible(show) + + def copy_to_clipboard(self, text): + """Copy text to clipboard""" + clipboard = QApplication.clipboard() + clipboard.setText(text) + QMessageBox.information(self, "✓ Success", "⎘ Copied to clipboard!") + + def download_script(self, title, code): + """Download script to file""" + filename = f"{title.lower().replace(' ', '_')}.js" + file_path, _ = QFileDialog.getSaveFileName( + self, "⤓ Save Script", filename, "JavaScript Files (*.js)" + ) + + if file_path: + try: + with open(file_path, 'w') as f: + f.write(code) + QMessageBox.information(self, "✓ Success", "⤓ Script downloaded successfully!") + except Exception as e: + QMessageBox.critical(self, "✗ Error", f"Failed to save script: {str(e)}") + + def toggle_favorite_ui(self, script_info, button): + """Toggle favorite status and update UI""" + self.toggle_favorite(script_info) + if self.is_favorite(script_info['id']): + button.setIcon(QIcon()) + button.setText("★") + else: + button.setIcon(QIcon()) + button.setText("☆") + button.setStyleSheet("color: #b9bbbe;") + + # Refresh favorites tab when status changes + self.refresh_favorites() + + def open_in_injector_page(self, code, dialog=None): + """Open the script in the injector page""" + self.open_in_injector.emit(code) # Emit signal to main window + if dialog: + dialog.close() # Close the preview dialog \ No newline at end of file diff --git a/src/gui/widgets/data_visualizer.py b/src/gui/widgets/data_visualizer.py new file mode 100644 index 0000000..578fd96 --- /dev/null +++ b/src/gui/widgets/data_visualizer.py @@ -0,0 +1,44 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout +from PyQt5.QtChart import QChart, QChartView, QLineSeries +from PyQt5.QtCore import Qt, QTimer +import json + +class DataVisualizer(QWidget): + def __init__(self): + super().__init__() + self.setup_ui() + self.api_calls = [] + self.setup_timer() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Create chart + self.chart = QChart() + self.chart.setTitle("API Calls Over Time") + self.chart.setAnimationOptions(QChart.SeriesAnimations) + + self.series = QLineSeries() + self.chart.addSeries(self.series) + + chart_view = QChartView(self.chart) + chart_view.setRenderHint(QPainter.Antialiasing) + + layout.addWidget(chart_view) + + def setup_timer(self): + self.timer = QTimer() + self.timer.timeout.connect(self.update_chart) + self.timer.start(1000) # Update every second + + def add_api_call(self, call_data): + self.api_calls.append({ + 'timestamp': time.time(), + 'data': call_data + }) + + def update_chart(self): + # Update chart with new data + self.series.clear() + for i, call in enumerate(self.api_calls[-50:]): # Show last 50 calls + self.series.append(i, len(call['data'])) \ No newline at end of file diff --git a/src/gui/widgets/device_panel.py b/src/gui/widgets/device_panel.py new file mode 100644 index 0000000..16947c4 --- /dev/null +++ b/src/gui/widgets/device_panel.py @@ -0,0 +1,46 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, + QComboBox, QPushButton, QLabel) +from PyQt5.QtCore import pyqtSignal +import frida + +class DevicePanel(QWidget): + device_selected = pyqtSignal(str) + + def __init__(self): + super().__init__() + self.setup_ui() + + def setup_ui(self): + layout = QHBoxLayout(self) + + # Device selection combo box + self.device_combo = QComboBox() + self.scan_button = QPushButton("Scan Devices") + + layout.addWidget(QLabel("Select Device:")) + layout.addWidget(self.device_combo) + layout.addWidget(self.scan_button) + + # Connect signals + self.scan_button.clicked.connect(self.scan_devices) + self.device_combo.currentIndexChanged.connect(self._on_device_selected) + + # Initial scan + self.scan_devices() + + def scan_devices(self): + try: + self.device_combo.clear() + devices = frida.enumerate_devices() + for device in devices: + if device.type in ['usb', 'remote']: + self.device_combo.addItem(f"{device.name} (ADB - {device.type})", device.id) + elif device.type == 'local': + self.device_combo.addItem(f"{device.name} (Local)", device.id) + except Exception as e: + print(f"Error scanning devices: {str(e)}") + + def _on_device_selected(self): + device_id = self.device_combo.currentData() + if device_id: + self.device_selected.emit(device_id) \ No newline at end of file diff --git a/src/gui/widgets/device_selector.py b/src/gui/widgets/device_selector.py new file mode 100644 index 0000000..d80fbe0 --- /dev/null +++ b/src/gui/widgets/device_selector.py @@ -0,0 +1,313 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QComboBox, + QPushButton, QLabel, QFrame, QLineEdit, QMessageBox, + QApplication) +from PyQt5.QtCore import pyqtSignal, QSize +import frida +import subprocess +import qtawesome as qta +import psutil +import sys +from pathlib import Path + +# Add project root to Python path +sys.path.append(str(Path(__file__).parent.parent.parent)) +from core.android_helper import AndroidHelper + +class DeviceSelector(QWidget): + process_selected = pyqtSignal(str, int) # device_id, pid + + def __init__(self): + super().__init__() + self.current_device = None + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + # Create main frame + frame = QFrame() + frame.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 8px; + padding: 10px; + } + QComboBox { + background-color: #36393f; + border: none; + border-radius: 4px; + padding: 8px; + color: white; + min-width: 200px; + } + QComboBox::drop-down { + border: none; + padding-right: 10px; + } + QComboBox::down-arrow { + image: url(down-arrow.png); + } + """) + + frame_layout = QVBoxLayout(frame) + + # Device selection + device_layout = QHBoxLayout() + + self.device_combo = QComboBox() + self.device_combo.setPlaceholderText("Select Device...") + self.device_combo.currentIndexChanged.connect(self.on_device_changed) + + refresh_btn = QPushButton(qta.icon('fa5s.sync'), "") + refresh_btn.setToolTip("Refresh Devices") + refresh_btn.clicked.connect(self.refresh_devices) + + device_layout.addWidget(QLabel("Device:")) + device_layout.addWidget(self.device_combo, 1) + device_layout.addWidget(refresh_btn) + + # Process selection + process_layout = QHBoxLayout() + + # Add filter input + self.process_filter = QLineEdit() + self.process_filter.setPlaceholderText("Filter processes...") + self.process_filter.textChanged.connect(self.filter_processes) + + self.process_combo = QComboBox() + self.process_combo.setPlaceholderText("Select Process...") + self.process_combo.currentIndexChanged.connect(self.on_process_changed) + self.process_combo.setMaxVisibleItems(20) # Show more items in dropdown + self.process_combo.setStyleSheet(""" + QComboBox QListView { + min-width: 300px; + } + """) + + # Add refresh button + refresh_btn = QPushButton(qta.icon('fa5s.sync'), "") + refresh_btn.setToolTip("Refresh Processes") + refresh_btn.clicked.connect(self.refresh_processes) + + process_layout.addWidget(QLabel("Process:")) + process_layout.addWidget(self.process_filter) + process_layout.addWidget(self.process_combo, 1) + process_layout.addWidget(refresh_btn) + + # Add layouts to frame + frame_layout.addLayout(device_layout) + frame_layout.addLayout(process_layout) + + # Add frame to main layout + layout.addWidget(frame) + + # Initial device scan + self.refresh_devices() + + def refresh_devices(self): + self.device_combo.clear() + try: + devices = frida.enumerate_devices() + for device in devices: + if device.type == 'usb': + self.device_combo.addItem(f"📱 {device.name} (USB)", device.id) + elif device.type == 'remote': + self.device_combo.addItem(f"🌐 {device.name} (Remote)", device.id) + elif device.type == 'local': + self.device_combo.addItem(f"💻 {device.name} (Local)", device.id) + except Exception as e: + print(f"Error enumerating devices: {e}") + + def on_device_changed(self, index): + if index < 0: + return + + device_id = self.device_combo.currentData() + self.current_device = device_id + self.refresh_processes() + + def refresh_processes(self): + self.process_combo.clear() + if not self.current_device: + return + + try: + device = frida.get_device(self.current_device) + + if device.type == 'usb': + # Show loading message + self.process_combo.addItem("Checking device status...") + QApplication.processEvents() + + # Check frida-server for Android devices + if not AndroidHelper.is_device_connected(self.current_device): + raise Exception(f"Device {self.current_device} not connected") + + if not AndroidHelper.is_frida_running(self.current_device): + # Show installing message + msg = QMessageBox() + msg.setIcon(QMessageBox.Information) + msg.setText("Installing Frida Server") + msg.setInformativeText("Please wait while Frida server is being installed on the device...") + msg.setWindowTitle("Installing Frida") + msg.show() + QApplication.processEvents() + + success = AndroidHelper.start_frida_server(self.current_device) + msg.close() + + if not success: + error_msg = QMessageBox() + error_msg.setIcon(QMessageBox.Critical) + error_msg.setText("Frida Installation Failed") + error_msg.setInformativeText("Failed to install and start Frida server on the device. Please check your device connection and try again.") + error_msg.setWindowTitle("Installation Error") + error_msg.exec_() + return + + # Show success message + success_msg = QMessageBox() + success_msg.setIcon(QMessageBox.Information) + success_msg.setText("Frida Server Installed") + success_msg.setInformativeText("Frida server has been successfully installed and started on the device.") + success_msg.setWindowTitle("Installation Complete") + success_msg.exec_() + + # Clear loading message and get processes + self.process_combo.clear() + + try: + # Get Android processes using frida-ps + processes = device.enumerate_processes() + for process in processes: + if process.pid > 0: + name = process.name + pid = process.pid + # Only add user apps (filter out system processes) + if '.' in name: # Simple check for app package names + self.process_combo.addItem( + f"{name} (PID: {pid})", + pid + ) + except Exception as e: + print(f"Error getting processes: {str(e)}") + raise Exception("Failed to get process list from device") + + elif device.type == 'local': + # Handle local device processes + processes = device.enumerate_processes() + for process in processes: + if process.pid > 0: + self.process_combo.addItem( + f"{process.name} (PID: {process.pid})", + process.pid + ) + + except Exception as e: + error_msg = QMessageBox() + error_msg.setIcon(QMessageBox.Critical) + error_msg.setText("Error") + error_msg.setInformativeText(f"Failed to refresh processes: {str(e)}") + error_msg.setWindowTitle("Process List Error") + error_msg.exec_() + + self.process_combo.clear() + self.process_combo.addItem("Error loading processes") + + def filter_processes(self, text): + """Filter processes in combo box""" + text = text.lower() + self.process_combo.clear() + + try: + device = frida.get_device(self.current_device) + + if device.type == 'local': + processes = device.enumerate_processes() + for process in processes: + try: + if process.pid > 0 and process.name and text in process.name.lower(): + pid = int(process.pid) + name = str(process.name) + self.process_combo.addItem( + f"{name} (PID: {pid})", + pid + ) + except (ValueError, AttributeError) as e: + continue + else: + # For Android devices + processes = device.enumerate_processes() + for process in processes: + if process.pid > 0 and text in process.name.lower(): + if '.' in process.name: # Only show Android apps + self.process_combo.addItem( + f"{process.name} (PID: {process.pid})", + process.pid + ) + + except Exception as e: + print(f"Error filtering processes: {e}") + + def on_process_changed(self, index): + if index < 0: + return + + try: + device_id = self.device_combo.currentData() + pid = self.process_combo.currentData() + + # Debug output + print(f"Process changed - device_id: {device_id}, pid: {pid} ({type(pid)})") + + # Only emit if we have valid data + if device_id and isinstance(pid, int) and pid > 0: + self.process_selected.emit(device_id, pid) + else: + print(f"Skipping invalid process selection - device_id: {device_id}, pid: {pid}") + + except Exception as e: + print(f"Error in process selection: {e}") + + def get_selected_process_info(self): + """Get info about selected process""" + try: + index = self.process_combo.currentIndex() + if index >= 0: + device_id = self.device_combo.currentData() + pid = self.process_combo.currentData() + name = self.process_combo.currentText().split('(')[0].strip() + + # Debug output + print(f"Selected process - PID: {pid} ({type(pid)}), Name: {name}") + + if device_id and pid: + return { + 'device_id': device_id, + 'pid': pid, + 'name': name + } + return None + except Exception as e: + print(f"Error getting process info: {e}") + return None + + def select_device(self, device_id): + """Select a device by its ID""" + index = self.device_combo.findData(device_id) + if index >= 0: + self.device_combo.setCurrentIndex(index) + + def select_process(self, pid): + """Select a process by its PID""" + for i in range(self.process_combo.count()): + if str(pid) in self.process_combo.itemText(i): + self.process_combo.setCurrentIndex(i) + break + + def cleanup(self): + """Clean up resources""" + self.process_combo.clear() + self.device_combo.clear() + self.current_device = None \ No newline at end of file diff --git a/src/gui/widgets/history_page.py b/src/gui/widgets/history_page.py new file mode 100644 index 0000000..1e84f44 --- /dev/null +++ b/src/gui/widgets/history_page.py @@ -0,0 +1,148 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QLabel, QTableWidget, QTableWidgetItem, QHeaderView, + QMenu, QMessageBox) +from PyQt5.QtCore import Qt, pyqtSignal +import qtawesome as qta +from datetime import datetime + +class HistoryPage(QWidget): + script_selected = pyqtSignal(str) # For opening scripts in injector + + def __init__(self, history_manager): + super().__init__() + self.history_manager = history_manager + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Header with title and clear button + header_layout = QHBoxLayout() + title = QLabel("Action History") + title.setStyleSheet("font-size: 18px; font-weight: bold; color: white;") + + clear_btn = QPushButton(qta.icon('fa5s.trash'), "Clear History") + clear_btn.clicked.connect(self.clear_history) + + header_layout.addWidget(title) + header_layout.addStretch() + header_layout.addWidget(clear_btn) + + # History table + self.table = QTableWidget() + self.table.setColumnCount(4) + self.table.setHorizontalHeaderLabels(["Time", "Action", "Details", "Actions"]) + + # Style the table + self.table.setStyleSheet(""" + QTableWidget { + background-color: #36393f; + border: none; + border-radius: 8px; + } + QTableWidget::item { + padding: 8px; + border-bottom: 1px solid #2f3136; + } + QHeaderView::section { + background-color: #2f3136; + padding: 8px; + border: none; + color: white; + font-weight: bold; + } + """) + + # Set column stretching + table_header = self.table.horizontalHeader() + table_header.setSectionResizeMode(0, QHeaderView.Fixed) # Time + table_header.setSectionResizeMode(1, QHeaderView.Fixed) # Action + table_header.setSectionResizeMode(2, QHeaderView.Stretch) # Details + table_header.setSectionResizeMode(3, QHeaderView.Fixed) # Actions + + self.table.setColumnWidth(0, 180) # Time + self.table.setColumnWidth(1, 120) # Action + self.table.setColumnWidth(3, 100) # Actions + + # Context menu + self.table.setContextMenuPolicy(Qt.CustomContextMenu) + self.table.customContextMenuRequested.connect(self.show_context_menu) + + # Add components to layout + layout.addLayout(header_layout) + layout.addWidget(self.table) + + self.refresh_history() + + def refresh_history(self): + self.table.setRowCount(0) + + for entry in self.history_manager.history: + row = self.table.rowCount() + self.table.insertRow(row) + + # Time + time_item = QTableWidgetItem( + datetime.fromisoformat(entry['timestamp']).strftime('%Y-%m-%d %H:%M:%S') + ) + + # Action + action_item = QTableWidgetItem(entry['type']) + + # Details + details = entry['details'] + if isinstance(details, dict): + details_text = "\n".join(f"{k}: {v}" for k, v in details.items()) + else: + details_text = str(details) + details_item = QTableWidgetItem(details_text) + + # Action buttons + action_widget = QWidget() + action_layout = QHBoxLayout(action_widget) + action_layout.setContentsMargins(4, 4, 4, 4) + + if 'script' in entry['details']: + inject_btn = QPushButton(qta.icon('fa5s.syringe'), "") + inject_btn.clicked.connect( + lambda x, s=entry['details']['script']: self.script_selected.emit(s) + ) + action_layout.addWidget(inject_btn) + + action_layout.addStretch() + + # Add items to row + self.table.setItem(row, 0, time_item) + self.table.setItem(row, 1, action_item) + self.table.setItem(row, 2, details_item) + self.table.setCellWidget(row, 3, action_widget) + + def clear_history(self): + reply = QMessageBox.question( + self, + "Clear History", + "Are you sure you want to clear all history?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + self.history_manager.clear_history() + self.refresh_history() + + def show_context_menu(self, position): + menu = QMenu() + + copy_action = menu.addAction("Copy Details") + copy_action.triggered.connect( + lambda: self.copy_details(self.table.currentRow()) + ) + + menu.exec_(self.table.viewport().mapToGlobal(position)) + + def copy_details(self, row): + if row >= 0: + details_item = self.table.item(row, 2) + if details_item: + clipboard = QApplication.clipboard() + clipboard.setText(details_item.text()) \ No newline at end of file diff --git a/src/gui/widgets/injection_panel.py b/src/gui/widgets/injection_panel.py new file mode 100644 index 0000000..187054d --- /dev/null +++ b/src/gui/widgets/injection_panel.py @@ -0,0 +1,220 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QLabel, QProgressBar, QFrame, QMessageBox, QFileDialog) +from PyQt5.QtCore import Qt, pyqtSignal +import qtawesome as qta +import os + +class InjectionPanel(QWidget): + injection_started = pyqtSignal(str, int) # script, pid + injection_completed = pyqtSignal(bool, str) # success, message + injection_stopped = pyqtSignal() # Signal to stop injection + + def __init__(self): + super().__init__() + self.current_pid = None + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Status panel + status_frame = QFrame() + status_frame.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 8px; + padding: 10px; + } + """) + status_layout = QHBoxLayout(status_frame) + + self.status_icon = QLabel() + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#99aab5').pixmap(16, 16)) + self.status_label = QLabel("No process selected") + self.status_label.setStyleSheet("color: #99aab5;") + + status_layout.addWidget(self.status_icon) + status_layout.addWidget(self.status_label) + status_layout.addStretch() + + # Action buttons + button_layout = QHBoxLayout() + + self.load_btn = QPushButton(qta.icon('fa5s.folder-open'), "Load Script") + self.load_btn.setStyleSheet(""" + QPushButton { + background-color: #7289da; + color: white; + padding: 8px 16px; + border-radius: 4px; + } + QPushButton:hover { + background-color: #677bc4; + } + """) + self.load_btn.clicked.connect(self.load_script_file) + + # Attach button (for running processes) + self.attach_btn = QPushButton(qta.icon('fa5s.link'), "Attach") + self.attach_btn.setStyleSheet(""" + QPushButton { + background-color: #43b581; + color: white; + padding: 8px 16px; + border-radius: 4px; + } + QPushButton:hover { + background-color: #3ca374; + } + QPushButton:disabled { + background-color: #2f3136; + color: #72767d; + } + """) + self.attach_btn.clicked.connect(lambda: self.start_injection(mode="attach")) + self.attach_btn.setEnabled(False) + + # Launch button (for spawning new process) + self.launch_btn = QPushButton(qta.icon('fa5s.play'), "Launch") + self.launch_btn.setStyleSheet(self.attach_btn.styleSheet()) + self.launch_btn.clicked.connect(lambda: self.start_injection(mode="launch")) + self.launch_btn.setEnabled(False) + + self.stop_btn = QPushButton(qta.icon('fa5s.stop'), "Stop") + self.stop_btn.setStyleSheet(""" + QPushButton { + background-color: #f04747; + color: white; + padding: 8px 16px; + border-radius: 4px; + } + QPushButton:hover { + background-color: #d84040; + } + """) + self.stop_btn.clicked.connect(self.stop_injection) + self.stop_btn.setEnabled(False) + + button_layout.addWidget(self.load_btn) + button_layout.addWidget(self.attach_btn) + button_layout.addWidget(self.launch_btn) + button_layout.addWidget(self.stop_btn) + button_layout.addStretch() + + # Progress bar + self.progress_bar = QProgressBar() + self.progress_bar.setTextVisible(False) + self.progress_bar.setStyleSheet(""" + QProgressBar { + border: none; + background-color: #2f3136; + border-radius: 4px; + height: 8px; + } + QProgressBar::chunk { + background-color: #7289da; + border-radius: 4px; + } + """) + self.progress_bar.hide() + + # Add all components + layout.addWidget(status_frame) + layout.addLayout(button_layout) + layout.addWidget(self.progress_bar) + + def set_process(self, device_id, pid): + """Called when a process is selected""" + try: + # Ensure pid is an integer + if not isinstance(pid, int): + print(f"Warning: PID is not an integer: {pid} ({type(pid)})") + pid = int(pid) + + if pid <= 0: + raise ValueError(f"Invalid PID value: {pid}") + + self.current_pid = pid + self.status_label.setText(f"Selected PID: {self.current_pid}") + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(16, 16)) + self.attach_btn.setEnabled(True) + self.launch_btn.setEnabled(True) + + except (ValueError, TypeError) as e: + print(f"Error setting process: {e}") + self.status_label.setText("Invalid PID") + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#f04747').pixmap(16, 16)) + self.attach_btn.setEnabled(False) + self.launch_btn.setEnabled(False) + + def load_script_file(self): + """Load script from file""" + file_name, _ = QFileDialog.getOpenFileName( + self, + "Load Frida Script", + "", + "JavaScript Files (*.js);;All Files (*.*)" + ) + + if file_name: + try: + with open(file_name, 'r') as f: + script_content = f.read() + self.script_editor.set_script(script_content) + self.status_label.setText(f"Loaded script: {os.path.basename(file_name)}") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to load script: {str(e)}") + + def start_injection(self, mode="attach"): + """Start the injection process""" + if not self.current_pid or not isinstance(self.current_pid, int) or self.current_pid <= 0: + QMessageBox.warning(self, "Error", "Invalid PID!") + return + + script_content = self.script_editor.get_script() + if not script_content: + QMessageBox.warning(self, "Error", "No script to inject!") + return + + # Debug output + print(f"Starting injection - PID: {self.current_pid} ({type(self.current_pid)}), Mode: {mode}") + + # Update UI + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#faa61a').pixmap(16, 16)) + self.status_label.setText(f"{'Attaching to' if mode == 'attach' else 'Launching'} process...") + self.attach_btn.setEnabled(False) + self.launch_btn.setEnabled(False) + self.stop_btn.setEnabled(True) + self.progress_bar.show() + self.progress_bar.setRange(0, 0) + + try: + self.injection_started.emit(script_content, self.current_pid) + except Exception as e: + self.injection_failed(str(e)) + + def injection_succeeded(self): + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#43b581').pixmap(16, 16)) + self.status_label.setText("Injection successful!") + self.reset_ui() + QMessageBox.information(self, "Success", "Script injected successfully!") + + def injection_failed(self, error): + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#f04747').pixmap(16, 16)) + self.status_label.setText("Injection failed!") + self.reset_ui() + QMessageBox.critical(self, "Error", f"Injection failed: {error}") + + def reset_ui(self): + self.attach_btn.setEnabled(True) + self.launch_btn.setEnabled(True) + self.stop_btn.setEnabled(False) + self.progress_bar.hide() + + def stop_injection(self): + """Stop the current injection""" + self.injection_stopped.emit() + self.reset_ui() + self.status_label.setText("Injection stopped") + self.status_icon.setPixmap(qta.icon('fa5s.circle', color='#faa61a').pixmap(16, 16)) + \ No newline at end of file diff --git a/src/gui/widgets/output_panel.py b/src/gui/widgets/output_panel.py new file mode 100644 index 0000000..93c065c --- /dev/null +++ b/src/gui/widgets/output_panel.py @@ -0,0 +1,21 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit + +class OutputPanel(QWidget): + def __init__(self): + super().__init__() + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + self.output_area = QTextEdit() + self.output_area.setReadOnly(True) + self.output_area.setPlaceholderText("Output will appear here...") + + layout.addWidget(self.output_area) + + def append_output(self, text): + self.output_area.append(text) + + def clear_output(self): + self.output_area.clear() \ No newline at end of file diff --git a/src/gui/widgets/process_manager.py b/src/gui/widgets/process_manager.py new file mode 100644 index 0000000..9bd380e --- /dev/null +++ b/src/gui/widgets/process_manager.py @@ -0,0 +1,307 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QLabel, QLineEdit, QTableWidget, QTableWidgetItem, + QMenu, QAction, QComboBox, QCheckBox, QFrame, + QTableWidgetSelectionRange, QHeaderView) +from PyQt5.QtCore import Qt, pyqtSignal, QTimer +from PyQt5.QtGui import QColor, QFont +import qtawesome as qta +import re +import psutil + +class ProcessManager(QWidget): + process_selected = pyqtSignal(int) # pid + + def __init__(self): + super().__init__() + self.processes = {} + self.filters = { + 'name': '', + 'pid': '', + 'cpu': 0, + 'memory': 0, + 'show_system': False + } + self.setup_ui() + self.start_monitoring() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Filter bar + filter_frame = QFrame() + filter_frame.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 8px; + padding: 10px; + } + """) + filter_layout = QHBoxLayout(filter_frame) + + # Search with regex support + search_container = QFrame() + search_layout = QHBoxLayout(search_container) + + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Filter processes (supports regex)") + self.search_input.textChanged.connect(self.apply_filters) + + self.regex_check = QCheckBox("Regex") + self.regex_check.toggled.connect(self.apply_filters) + + search_layout.addWidget(self.search_input) + search_layout.addWidget(self.regex_check) + + # Advanced filters + self.filter_combo = QComboBox() + self.filter_combo.addItems(['All', 'User', 'System', 'Android Apps']) + self.filter_combo.currentTextChanged.connect(self.apply_filters) + + # Resource thresholds + self.cpu_threshold = QSpinBox() + self.cpu_threshold.setSuffix("% CPU") + self.cpu_threshold.valueChanged.connect(self.apply_filters) + + self.memory_threshold = QSpinBox() + self.memory_threshold.setSuffix("MB") + self.memory_threshold.setMaximum(32000) + self.memory_threshold.valueChanged.connect(self.apply_filters) + + filter_layout.addWidget(search_container) + filter_layout.addWidget(self.filter_combo) + filter_layout.addWidget(self.cpu_threshold) + filter_layout.addWidget(self.memory_threshold) + + # Process table + self.process_table = QTableWidget() + self.process_table.setColumnCount(6) + self.process_table.setHorizontalHeaderLabels([ + "PID", "Name", "CPU %", "Memory", "Status", "Path" + ]) + + # Style the table + self.process_table.setStyleSheet(""" + QTableWidget { + background-color: #36393f; + border: none; + border-radius: 8px; + gridline-color: #2f3136; + } + QTableWidget::item { + padding: 5px; + border-bottom: 1px solid #2f3136; + } + QTableWidget::item:selected { + background-color: #7289da; + } + """) + + # Set column widths + header = self.process_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.Fixed) # PID + header.setSectionResizeMode(1, QHeaderView.Stretch) # Name + header.setSectionResizeMode(2, QHeaderView.Fixed) # CPU + header.setSectionResizeMode(3, QHeaderView.Fixed) # Memory + header.setSectionResizeMode(4, QHeaderView.Fixed) # Status + header.setSectionResizeMode(5, QHeaderView.Stretch) # Path + + self.process_table.setColumnWidth(0, 70) # PID + self.process_table.setColumnWidth(2, 80) # CPU + self.process_table.setColumnWidth(3, 100) # Memory + self.process_table.setColumnWidth(4, 100) # Status + + # Context menu + self.process_table.setContextMenuPolicy(Qt.CustomContextMenu) + self.process_table.customContextMenuRequested.connect(self.show_context_menu) + + # Quick action buttons + action_layout = QHBoxLayout() + + self.refresh_btn = QPushButton(qta.icon('fa5s.sync'), "Refresh") + self.refresh_btn.clicked.connect(self.refresh_processes) + + self.kill_btn = QPushButton(qta.icon('fa5s.stop'), "Kill") + self.kill_btn.clicked.connect(self.kill_selected_process) + + self.inject_btn = QPushButton(qta.icon('fa5s.syringe'), "Inject") + self.inject_btn.clicked.connect(self.inject_into_selected) + + action_layout.addWidget(self.refresh_btn) + action_layout.addWidget(self.kill_btn) + action_layout.addWidget(self.inject_btn) + action_layout.addStretch() + + # Status bar + status_bar = QFrame() + status_bar.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 4px; + padding: 5px; + } + """) + status_layout = QHBoxLayout(status_bar) + + self.process_count = QLabel("0 processes") + self.cpu_usage = QLabel("CPU: 0%") + self.memory_usage = QLabel("Memory: 0 MB") + + status_layout.addWidget(self.process_count) + status_layout.addStretch() + status_layout.addWidget(self.cpu_usage) + status_layout.addWidget(self.memory_usage) + + # Add all components + layout.addWidget(filter_frame) + layout.addWidget(self.process_table) + layout.addLayout(action_layout) + layout.addWidget(status_bar) + + def start_monitoring(self): + self.update_timer = QTimer() + self.update_timer.timeout.connect(self.refresh_processes) + self.update_timer.start(2000) # Update every 2 seconds + + def refresh_processes(self): + self.processes.clear() + total_cpu = 0 + total_memory = 0 + + for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_info', 'status', 'exe']): + try: + info = proc.info + memory_mb = info['memory_info'].rss / 1024 / 1024 + self.processes[info['pid']] = { + 'name': info['name'], + 'cpu': info['cpu_percent'], + 'memory': memory_mb, + 'status': info['status'], + 'path': info['exe'] or '' + } + total_cpu += info['cpu_percent'] + total_memory += memory_mb + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + self.update_table() + self.update_stats(total_cpu, total_memory) + + def update_table(self): + self.process_table.setSortingEnabled(False) + self.process_table.setRowCount(0) + + filtered_processes = self.filter_processes() + + for pid, info in filtered_processes.items(): + row = self.process_table.rowCount() + self.process_table.insertRow(row) + + # PID + pid_item = QTableWidgetItem(str(pid)) + pid_item.setTextAlignment(Qt.AlignCenter) + + # Name + name_item = QTableWidgetItem(info['name']) + + # CPU + cpu_item = QTableWidgetItem(f"{info['cpu']:.1f}%") + cpu_item.setTextAlignment(Qt.AlignCenter) + + # Memory + memory_item = QTableWidgetItem(f"{info['memory']:.1f} MB") + memory_item.setTextAlignment(Qt.AlignCenter) + + # Status + status_item = QTableWidgetItem(info['status']) + status_item.setTextAlignment(Qt.AlignCenter) + + # Path + path_item = QTableWidgetItem(info['path']) + + # Set items + self.process_table.setItem(row, 0, pid_item) + self.process_table.setItem(row, 1, name_item) + self.process_table.setItem(row, 2, cpu_item) + self.process_table.setItem(row, 3, memory_item) + self.process_table.setItem(row, 4, status_item) + self.process_table.setItem(row, 5, path_item) + + # Color coding based on resource usage + if info['cpu'] > 50: + self.color_row(row, QColor(240, 71, 71, 50)) # Red + elif info['memory'] > 1000: + self.color_row(row, QColor(250, 166, 26, 50)) # Orange + + self.process_table.setSortingEnabled(True) + + def filter_processes(self): + filtered = {} + search_text = self.search_input.text().lower() + + for pid, info in self.processes.items(): + # Apply regex/text filter + if self.regex_check.isChecked(): + try: + if not re.search(search_text, info['name'].lower()): + continue + except re.error: + continue + elif search_text and search_text not in info['name'].lower(): + continue + + # Apply type filter + if self.filter_combo.currentText() == 'User' and pid < 1000: + continue + elif self.filter_combo.currentText() == 'System' and pid >= 1000: + continue + elif self.filter_combo.currentText() == 'Android Apps' and not info['name'].startswith('com.'): + continue + + # Apply resource thresholds + if info['cpu'] < self.cpu_threshold.value(): + continue + if info['memory'] < self.memory_threshold.value(): + continue + + filtered[pid] = info + + return filtered + + def color_row(self, row, color): + for col in range(self.process_table.columnCount()): + item = self.process_table.item(row, col) + item.setBackground(color) + + def update_stats(self, total_cpu, total_memory): + self.process_count.setText(f"{len(self.processes)} processes") + self.cpu_usage.setText(f"CPU: {total_cpu:.1f}%") + self.memory_usage.setText(f"Memory: {total_memory:.0f} MB") + + def show_context_menu(self, position): + menu = QMenu() + + kill_action = QAction("Kill Process", self) + kill_action.triggered.connect(self.kill_selected_process) + + inject_action = QAction("Inject Script", self) + inject_action.triggered.connect(self.inject_into_selected) + + menu.addAction(kill_action) + menu.addAction(inject_action) + menu.exec_(self.process_table.mapToGlobal(position)) + + def kill_selected_process(self): + selected = self.process_table.selectedItems() + if selected: + pid = int(self.process_table.item(selected[0].row(), 0).text()) + try: + psutil.Process(pid).terminate() + self.refresh_processes() + except psutil.NoSuchProcess: + pass + + def inject_into_selected(self): + selected = self.process_table.selectedItems() + if selected: + pid = int(self.process_table.item(selected[0].row(), 0).text()) + self.process_selected.emit(pid) \ No newline at end of file diff --git a/src/gui/widgets/process_monitor.py b/src/gui/widgets/process_monitor.py new file mode 100644 index 0000000..fe5ae7e --- /dev/null +++ b/src/gui/widgets/process_monitor.py @@ -0,0 +1,335 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QLabel, QLineEdit, QTableWidget, QTableWidgetItem, + QMenu, QAction, QComboBox, QCheckBox, QFrame, + QHeaderView, QStyle, QStyledItemDelegate, QToolButton, QMessageBox) +from PyQt5.QtCore import Qt, pyqtSignal, QTimer, QSize +from PyQt5.QtGui import QColor, QFont, QIcon +import psutil +import frida +import re +import qtawesome as qta +from datetime import datetime +import subprocess + +class ProcessInfoDelegate(QStyledItemDelegate): + def paint(self, painter, option, index): + if index.column() in [2, 3]: # CPU and Memory columns + value = float(index.data().replace('%', '').replace('MB', '')) + if value > 80: + option.backgroundBrush = QColor('#f04747') + elif value > 50: + option.backgroundBrush = QColor('#faa61a') + super().paint(painter, option, index) + +class ProcessMonitor(QWidget): + def __init__(self, main_window=None): + QWidget.__init__(self) + self.processes = {} + self.current_device = None + self.main_window = main_window + self.setup_ui() + self.start_monitoring() + + def start_monitoring(self): + self.update_timer = QTimer() + self.update_timer.timeout.connect(self.refresh_processes) + self.update_timer.start(2000) # Update every 2 seconds + + def stop_monitoring(self): + if hasattr(self, 'update_timer'): + self.update_timer.stop() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Device selection + device_frame = QFrame() + device_frame.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 8px; + padding: 10px; + } + """) + device_layout = QHBoxLayout(device_frame) + + self.device_combo = QComboBox() + self.device_combo.currentIndexChanged.connect(self.on_device_changed) + + refresh_devices_btn = QPushButton(qta.icon('fa5s.sync'), "Refresh Devices") + refresh_devices_btn.clicked.connect(self.refresh_devices) + + device_layout.addWidget(QLabel("Device:")) + device_layout.addWidget(self.device_combo) + device_layout.addWidget(refresh_devices_btn) + + # Search and Filter Bar + filter_frame = QFrame() + filter_frame.setStyleSheet(""" + QFrame { + background-color: #2f3136; + border-radius: 8px; + padding: 10px; + } + """) + filter_layout = QHBoxLayout(filter_frame) + + # Process search with regex toggle + search_container = QFrame() + search_layout = QHBoxLayout(search_container) + + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Filter processes (supports regex)") + self.search_input.textChanged.connect(self.apply_filters) + + self.regex_check = QCheckBox("Regex") + self.regex_check.toggled.connect(self.apply_filters) + + search_layout.addWidget(self.search_input) + search_layout.addWidget(self.regex_check) + + # Advanced filters + self.filter_combo = QComboBox() + self.filter_combo.addItems(['All', 'User', 'System', 'Android Apps', 'High CPU', 'High Memory']) + self.filter_combo.currentTextChanged.connect(self.apply_filters) + + filter_layout.addWidget(search_container) + filter_layout.addWidget(self.filter_combo) + + # Process Table + self.process_table = QTableWidget() + self.process_table.setColumnCount(8) + self.process_table.setHorizontalHeaderLabels([ + "PID", "Name", "CPU %", "Memory", "Status", "User", "Started", "Command Line" + ]) + + # Context menu + self.process_table.setContextMenuPolicy(Qt.CustomContextMenu) + self.process_table.customContextMenuRequested.connect(self.show_context_menu) + + # Action buttons + action_layout = QHBoxLayout() + + self.refresh_btn = QPushButton(qta.icon('fa5s.sync'), "Refresh") + self.refresh_btn.clicked.connect(self.refresh_processes) + + self.kill_btn = QPushButton(qta.icon('fa5s.stop'), "Kill") + self.kill_btn.clicked.connect(self.kill_selected_process) + + self.inject_btn = QPushButton(qta.icon('fa5s.syringe'), "Open in Injector") + self.inject_btn.clicked.connect(self.open_in_injector_clicked) + + action_layout.addWidget(self.refresh_btn) + action_layout.addWidget(self.kill_btn) + action_layout.addWidget(self.inject_btn) + action_layout.addStretch() + + # Add all components + layout.addWidget(device_frame) + layout.addWidget(filter_frame) + layout.addWidget(self.process_table) + layout.addLayout(action_layout) + + # Initial device scan + self.refresh_devices() + + def refresh_devices(self): + self.device_combo.clear() + try: + devices = frida.enumerate_devices() + for device in devices: + if device.type == 'usb': + self.device_combo.addItem(f"📱 {device.name} (USB)", device.id) + elif device.type == 'remote': + self.device_combo.addItem(f"🌐 {device.name} (Remote)", device.id) + elif device.type == 'local': + self.device_combo.addItem(f"💻 {device.name} (Local)", device.id) + except Exception as e: + print(f"Error enumerating devices: {e}") + + def on_device_changed(self, index): + if index >= 0: + self.current_device = self.device_combo.currentData() + self.refresh_processes() + + def show_context_menu(self, position): + menu = QMenu() + + kill_action = QAction("Kill Process", self) + kill_action.triggered.connect(self.kill_selected_process) + + inject_action = QAction("Open in Injector", self) + inject_action.triggered.connect(self.open_in_injector_clicked) + + details_action = QAction("Process Details", self) + details_action.triggered.connect(self.show_process_details) + + menu.addAction(kill_action) + menu.addAction(inject_action) + menu.addAction(details_action) + menu.exec_(self.process_table.mapToGlobal(position)) + + def open_in_injector_clicked(self): + """Handle click on 'Open in Injector' button""" + if not self.main_window: + return + + selected = self.process_table.selectedItems() + if selected: + row = selected[0].row() + pid = int(self.process_table.item(row, 0).text()) + if self.current_device: + self.main_window.open_in_injector(self.current_device, pid) + else: + QMessageBox.warning(self, "Error", "No device selected!") + + def refresh_processes(self): + self.process_table.setRowCount(0) + if not self.current_device: + return + + try: + device = frida.get_device(self.current_device) + + if device.type == 'local': + # For local processes, use psutil for more reliable info + for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_info', 'status', 'username', 'create_time', 'cmdline']): + try: + row = self.process_table.rowCount() + self.process_table.insertRow(row) + + # Get process info + pid = proc.pid + name = proc.name() + cpu = proc.cpu_percent() + memory = proc.memory_info().rss / 1024 / 1024 # Convert to MB + status = proc.status() + user = proc.username() + started = datetime.fromtimestamp(proc.create_time()).strftime('%Y-%m-%d %H:%M:%S') + cmdline = ' '.join(proc.cmdline()) + + # Create items + items = [ + QTableWidgetItem(str(pid)), + QTableWidgetItem(name), + QTableWidgetItem(f"{cpu:.1f}%"), + QTableWidgetItem(f"{memory:.1f} MB"), + QTableWidgetItem(status), + QTableWidgetItem(user), + QTableWidgetItem(started), + QTableWidgetItem(cmdline) + ] + + # Set alignment + items[0].setTextAlignment(Qt.AlignCenter) + items[2].setTextAlignment(Qt.AlignCenter) + items[3].setTextAlignment(Qt.AlignCenter) + items[4].setTextAlignment(Qt.AlignCenter) + + # Add items to row + for col, item in enumerate(items): + self.process_table.setItem(row, col, item) + + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + else: + # For ADB devices + try: + adb_output = subprocess.check_output( + ['adb', '-s', self.current_device, 'shell', 'ps'], + text=True + ).strip().split('\n') + + for line in adb_output[1:]: # Skip header + parts = line.split() + if len(parts) >= 9: + row = self.process_table.rowCount() + self.process_table.insertRow(row) + + pid = parts[1] + name = parts[-1] + + items = [ + QTableWidgetItem(pid), + QTableWidgetItem(name), + QTableWidgetItem("N/A"), + QTableWidgetItem("N/A"), + QTableWidgetItem(parts[7]), + QTableWidgetItem(parts[0]), + QTableWidgetItem("N/A"), + QTableWidgetItem("N/A") + ] + + for col, item in enumerate(items): + self.process_table.setItem(row, col, item) + + except subprocess.CalledProcessError as e: + print(f"ADB error: {e}") + + except Exception as e: + print(f"Error refreshing processes: {e}") + + def apply_filters(self): + search_text = self.search_input.text().lower() + filter_type = self.filter_combo.currentText() + use_regex = self.regex_check.isChecked() + + for row in range(self.process_table.rowCount()): + show_row = True + name = self.process_table.item(row, 1).text().lower() + pid = int(self.process_table.item(row, 0).text()) + + # Apply text filter + if search_text: + if use_regex: + try: + if not re.search(search_text, name): + show_row = False + except re.error: + show_row = False + elif search_text not in name: + show_row = False + + # Apply type filter + if filter_type == 'User' and pid < 1000: + show_row = False + elif filter_type == 'System' and pid >= 1000: + show_row = False + elif filter_type == 'Android Apps' and not name.startswith('com.'): + show_row = False + elif filter_type == 'High CPU': + cpu = float(self.process_table.item(row, 2).text().replace('%', '')) + if cpu < 50: + show_row = False + elif filter_type == 'High Memory': + memory = float(self.process_table.item(row, 3).text().replace('MB', '')) + if memory < 500: + show_row = False + + self.process_table.setRowHidden(row, not show_row) + + def kill_selected_process(self): + selected = self.process_table.selectedItems() + if selected: + row = selected[0].row() + pid = int(self.process_table.item(row, 0).text()) + try: + if self.current_device == 'local': + psutil.Process(pid).terminate() + else: + subprocess.run(['adb', '-s', self.current_device, 'shell', 'kill', str(pid)]) + self.refresh_processes() + except Exception as e: + print(f"Error killing process: {e}") + + def show_process_details(self): + selected = self.process_table.selectedItems() + if selected: + row = selected[0].row() + details = "\n".join([ + f"{self.process_table.horizontalHeaderItem(col).text()}: " + f"{self.process_table.item(row, col).text()}" + for col in range(self.process_table.columnCount()) + ]) + QMessageBox.information(self, "Process Details", details) \ No newline at end of file diff --git a/src/gui/widgets/process_panel.py b/src/gui/widgets/process_panel.py new file mode 100644 index 0000000..12aea20 --- /dev/null +++ b/src/gui/widgets/process_panel.py @@ -0,0 +1,59 @@ +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, + QComboBox, QPushButton, QLabel) +import frida +import subprocess + +class ProcessPanel(QWidget): + def __init__(self): + super().__init__() + self.setup_ui() + self.current_device_id = None + + def setup_ui(self): + layout = QHBoxLayout(self) + + self.process_combo = QComboBox() + self.refresh_button = QPushButton("Refresh Processes") + + layout.addWidget(QLabel("Select Process:")) + layout.addWidget(self.process_combo) + layout.addWidget(self.refresh_button) + + self.refresh_button.clicked.connect(self.refresh_processes) + + def update_device(self, device_id): + self.current_device_id = device_id + self.refresh_processes() + + def refresh_processes(self): + if not self.current_device_id: + return + + self.process_combo.clear() + try: + device = frida.get_device(self.current_device_id) + if device.type == 'local': + processes = device.enumerate_processes() + for process in processes: + self.process_combo.addItem( + f"{process.name} (PID: {process.pid})", + process.pid + ) + else: + # For ADB devices + output = subprocess.check_output( + ['adb', '-s', self.current_device_id, 'shell', 'ps'], + text=True + ).strip().split('\n') + + for line in output[1:]: # Skip header + parts = line.split() + if len(parts) >= 9: + pid = parts[1] + process_name = parts[-1] + self.process_combo.addItem( + f"{process_name} (PID: {pid})", + pid + ) + except Exception as e: + print(f"Error refreshing processes: {str(e)}") \ No newline at end of file diff --git a/src/gui/widgets/script_editor.py b/src/gui/widgets/script_editor.py new file mode 100644 index 0000000..813ebc6 --- /dev/null +++ b/src/gui/widgets/script_editor.py @@ -0,0 +1,25 @@ +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTextEdit + +class ScriptEditorPanel(QWidget): + def __init__(self): + super().__init__() + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + self.editor = QTextEdit() + self.editor.setPlaceholderText("Enter your Frida script here...") + + # Set default script template + self.editor.setPlainText('''Java.perform(function() { + console.log("Script loaded!"); +});''') + + layout.addWidget(self.editor) + + def get_script(self): + return self.editor.toPlainText() + + def set_script(self, script): + self.editor.setPlainText(script) \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..0fffbf7 --- /dev/null +++ b/src/main.py @@ -0,0 +1,70 @@ +import sys +import os +from pathlib import Path + +def setup_qt_environment(): + """Setup Qt environment variables and paths""" + try: + # Get the PyQt5 location + import PyQt5 + pyqt_path = Path(PyQt5.__file__).parent + + # Set environment variables + os.environ['QT_DEBUG_PLUGINS'] = '1' + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = str(pyqt_path / 'Qt5' / 'plugins') + + # Print debug info + print(f"PyQt5 path: {pyqt_path}") + print(f"Plugin path: {os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']}") + + # Verify plugin exists + cocoa_path = pyqt_path / 'Qt5' / 'plugins' / 'platforms' / 'libqcocoa.dylib' + if cocoa_path.exists(): + print(f"Found cocoa plugin at: {cocoa_path}") + else: + print(f"Warning: Could not find cocoa plugin at: {cocoa_path}") + + # Try alternate locations + alt_paths = [ + pyqt_path / 'Qt' / 'plugins' / 'platforms' / 'libqcocoa.dylib', + Path('/opt/anaconda3/plugins/platforms/libqcocoa.dylib'), + Path('/opt/anaconda3/lib/python3.11/site-packages/PyQt5/Qt/plugins/platforms/libqcocoa.dylib') + ] + + for path in alt_paths: + if path.exists(): + print(f"Found cocoa plugin in alternate location: {path}") + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = str(path.parent.parent) + break + + except Exception as e: + print(f"Error setting up Qt environment: {e}") + +# Add project root to Python path +project_root = Path(__file__).parent +sys.path.append(str(project_root)) + +# Setup Qt environment before importing PyQt +setup_qt_environment() + +from PyQt5.QtWidgets import QApplication, QMessageBox +from gui.main_window import FridaInjectorMainWindow +from utils.themes import set_application_style + +def main(): + try: + # Create application + app = QApplication(sys.argv) + set_application_style(app) + + window = FridaInjectorMainWindow() + window.show() + + sys.exit(app.exec_()) + except Exception as e: + print(f"Error starting application: {e}") + QMessageBox.critical(None, "Error", f"Application failed to start: {str(e)}") + sys.exit(1) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/src/utils/__pycache__/themes.cpython-311.pyc b/src/utils/__pycache__/themes.cpython-311.pyc new file mode 100644 index 0000000..74eb79d Binary files /dev/null and b/src/utils/__pycache__/themes.cpython-311.pyc differ diff --git a/src/utils/themes.py b/src/utils/themes.py new file mode 100644 index 0000000..4c808e4 --- /dev/null +++ b/src/utils/themes.py @@ -0,0 +1,172 @@ +from PyQt5.QtWidgets import QStyleFactory +from PyQt5.QtGui import QPalette, QColor +from PyQt5.QtCore import Qt + +# Discord-inspired color scheme +DISCORD_COLORS = { + 'background': '#36393f', + 'secondary_bg': '#2f3136', + 'tertiary_bg': '#202225', + 'text': '#dcddde', + 'secondary_text': '#96989d', + 'accent': '#ec695c', + 'accent_hover': '#4752c4', + 'red': '#ed4245', + 'green': '#3ba55c' +} + +STYLE_SHEET = """ +QMainWindow, QWidget { + background-color: """ + DISCORD_COLORS['background'] + """; + color: """ + DISCORD_COLORS['text'] + """; + font-family: 'Segoe UI', Arial, sans-serif; +} + +QTabWidget::pane { + border: none; + background-color: """ + DISCORD_COLORS['background'] + """; +} + +QTabWidget::tab-bar { + alignment: left; +} + +QTabBar::tab { + background-color: """ + DISCORD_COLORS['tertiary_bg'] + """; + color: """ + DISCORD_COLORS['secondary_text'] + """; + padding: 8px 16px; + border: none; + min-width: 100px; +} + +QTabBar::tab:selected { + background-color: """ + DISCORD_COLORS['background'] + """; + color: """ + DISCORD_COLORS['text'] + """; +} + +QTabBar::tab:hover:!selected { + background-color: """ + DISCORD_COLORS['secondary_bg'] + """; +} + +QPushButton { + background-color: """ + DISCORD_COLORS['accent'] + """; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; +} + +QPushButton:hover { + background-color: """ + DISCORD_COLORS['accent_hover'] + """; +} + +QPushButton:pressed { + background-color: """ + DISCORD_COLORS['accent'] + """; +} + +QComboBox { + background-color: """ + DISCORD_COLORS['tertiary_bg'] + """; + border: none; + border-radius: 4px; + padding: 6px 12px; + color: """ + DISCORD_COLORS['text'] + """; + min-width: 150px; +} + +QComboBox::drop-down { + border: none; + width: 20px; +} + +QComboBox::down-arrow { + image: none; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid """ + DISCORD_COLORS['text'] + """; + margin-right: 8px; +} + +QTextEdit { + background-color: """ + DISCORD_COLORS['tertiary_bg'] + """; + border: none; + border-radius: 4px; + padding: 8px; + color: """ + DISCORD_COLORS['text'] + """; + font-family: 'Consolas', 'Courier New', monospace; +} + +QLabel { + color: """ + DISCORD_COLORS['text'] + """; + font-weight: bold; +} + +QScrollBar:vertical { + border: none; + background-color: """ + DISCORD_COLORS['tertiary_bg'] + """; + width: 14px; + margin: 0; +} + +QScrollBar::handle:vertical { + background-color: """ + DISCORD_COLORS['secondary_bg'] + """; + min-height: 30px; + border-radius: 7px; +} + +QScrollBar::handle:vertical:hover { + background-color: """ + DISCORD_COLORS['accent'] + """; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical, +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + border: none; + background: none; + color: none; +} + +QListWidget { + background-color: """ + DISCORD_COLORS['tertiary_bg'] + """; + border: none; + border-radius: 4px; + padding: 4px; +} + +QListWidget::item { + padding: 8px; + border-radius: 4px; +} + +QListWidget::item:hover { + background-color: """ + DISCORD_COLORS['secondary_bg'] + """; +} + +QListWidget::item:selected { + background-color: """ + DISCORD_COLORS['accent'] + """; + color: white; +} +""" + +def set_application_style(app): + app.setStyle(QStyleFactory.create("Fusion")) + + # Set the custom style sheet + app.setStyleSheet(STYLE_SHEET) + + # Set up dark palette for system dialogs + dark_palette = QPalette() + dark_palette.setColor(QPalette.Window, QColor(DISCORD_COLORS['background'])) + dark_palette.setColor(QPalette.WindowText, QColor(DISCORD_COLORS['text'])) + dark_palette.setColor(QPalette.Base, QColor(DISCORD_COLORS['tertiary_bg'])) + dark_palette.setColor(QPalette.AlternateBase, QColor(DISCORD_COLORS['secondary_bg'])) + dark_palette.setColor(QPalette.ToolTipBase, QColor(DISCORD_COLORS['text'])) + dark_palette.setColor(QPalette.ToolTipText, QColor(DISCORD_COLORS['text'])) + dark_palette.setColor(QPalette.Text, QColor(DISCORD_COLORS['text'])) + dark_palette.setColor(QPalette.Button, QColor(DISCORD_COLORS['accent'])) + dark_palette.setColor(QPalette.ButtonText, Qt.white) + dark_palette.setColor(QPalette.BrightText, Qt.red) + dark_palette.setColor(QPalette.Link, QColor(DISCORD_COLORS['accent'])) + dark_palette.setColor(QPalette.Highlight, QColor(DISCORD_COLORS['accent'])) + dark_palette.setColor(QPalette.HighlightedText, Qt.white) + + app.setPalette(dark_palette) \ No newline at end of file