From 5408de21e5208a4ff62e8dac09765a64a7660e15 Mon Sep 17 00:00:00 2001 From: jiqiu2021 Date: Sat, 5 Oct 2024 15:26:16 +0800 Subject: [PATCH] add --- .gitattributes | 4 + .github/workflows/build.yml | 27 + .gitignore | 14 + LICENSE | 21 + README.md | 21 + README.zh-CN.md | 19 + build.gradle | 35 + gradle.properties | 19 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++ gradlew.bat | 84 ++ module.gradle | 9 + module/.gitignore | 3 + module/build.gradle | 107 +++ module/src/main/AndroidManifest.xml | 1 + module/src/main/cpp/CMakeLists.txt | 45 + module/src/main/cpp/game.h | 10 + module/src/main/cpp/hack.cpp | 264 ++++++ module/src/main/cpp/hack.h | 12 + module/src/main/cpp/log.h | 16 + module/src/main/cpp/main.cpp | 81 ++ module/src/main/cpp/xdl/include/xdl.h | 92 ++ module/src/main/cpp/xdl/xdl.c | 869 ++++++++++++++++++ module/src/main/cpp/xdl/xdl_iterate.c | 297 ++++++ module/src/main/cpp/xdl/xdl_iterate.h | 43 + module/src/main/cpp/xdl/xdl_linker.c | 187 ++++ module/src/main/cpp/xdl/xdl_linker.h | 40 + module/src/main/cpp/xdl/xdl_lzma.c | 181 ++++ module/src/main/cpp/xdl/xdl_lzma.h | 40 + module/src/main/cpp/xdl/xdl_util.c | 95 ++ module/src/main/cpp/xdl/xdl_util.h | 71 ++ module/src/main/cpp/zygisk.hpp | 326 +++++++ settings.gradle | 5 + .../META-INF/com/google/android/update-binary | 33 + .../com/google/android/updater-script | 1 + template/magisk_module/module.prop | 6 + 37 files changed, 3256 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.zh-CN.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 module.gradle create mode 100644 module/.gitignore create mode 100644 module/build.gradle create mode 100644 module/src/main/AndroidManifest.xml create mode 100644 module/src/main/cpp/CMakeLists.txt create mode 100644 module/src/main/cpp/game.h create mode 100644 module/src/main/cpp/hack.cpp create mode 100644 module/src/main/cpp/hack.h create mode 100644 module/src/main/cpp/log.h create mode 100644 module/src/main/cpp/main.cpp create mode 100644 module/src/main/cpp/xdl/include/xdl.h create mode 100644 module/src/main/cpp/xdl/xdl.c create mode 100644 module/src/main/cpp/xdl/xdl_iterate.c create mode 100644 module/src/main/cpp/xdl/xdl_iterate.h create mode 100644 module/src/main/cpp/xdl/xdl_linker.c create mode 100644 module/src/main/cpp/xdl/xdl_linker.h create mode 100644 module/src/main/cpp/xdl/xdl_lzma.c create mode 100644 module/src/main/cpp/xdl/xdl_lzma.h create mode 100644 module/src/main/cpp/xdl/xdl_util.c create mode 100644 module/src/main/cpp/xdl/xdl_util.h create mode 100644 module/src/main/cpp/zygisk.hpp create mode 100644 settings.gradle create mode 100644 template/magisk_module/META-INF/com/google/android/update-binary create mode 100644 template/magisk_module/META-INF/com/google/android/updater-script create mode 100644 template/magisk_module/module.prop diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..27fc7c2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.jar binary \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ec41728 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: Build +on: + workflow_dispatch: + inputs: + package_name: + description: "Package name of the game:" + required: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 11 + cache: gradle + - run: | + chmod +x ./gradlew + sed -i 's/moduleDescription = "/moduleDescription = "(${{ github.event.inputs.package_name }}) /g' module.gradle + sed -i "s/com.game.packagename/${{ github.event.inputs.package_name }}/g" module/src/main/cpp/game.h + ./gradlew :module:assembleRelease + - uses: actions/upload-artifact@v3 + with: + name: zygisk-il2cppdumper + path: out/magisk_module_release/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc00840 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +.idea +/.idea/caches/build_file_checksums.ser +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +/out +.externalNativeBuild +.cxx \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..39817e7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Rikka + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4ed2d4 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Zygisk-Il2CppDumper +Il2CppDumper with Zygisk, dump il2cpp data at runtime, can bypass protection, encryption and obfuscation. + +中文说明请戳[这里](README.zh-CN.md) + +## How to use +1. Install [Magisk](https://github.com/topjohnwu/Magisk) v24 or later and enable Zygisk +2. Build module + - GitHub Actions + 1. Fork this repo + 2. Go to the **Actions** tab in your forked repo + 3. In the left sidebar, click the **Build** workflow. + 4. Above the list of workflow runs, select **Run workflow** + 5. Input the game package name and click **Run workflow** + 6. Wait for the action to complete and download the artifact + - Android Studio + 1. Download the source code + 2. Edit `game.h`, modify `AimPackageName` to the game package name + 3. Use Android Studio to run the gradle task `:module:assembleRelease` to compile, the zip package will be generated in the `out` folder +3. Install module in Magisk +4. Start the game, `dump.cs` will be generated in the `/data/data/AimPackageName/files/` directory \ No newline at end of file diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..f8b2012 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,19 @@ +# Zygisk-Il2CppDumper +Zygisk版Il2CppDumper,在游戏运行时dump il2cpp数据,可以绕过保护,加密以及混淆。 + +## 如何食用 +1. 安装[Magisk](https://github.com/topjohnwu/Magisk) v24以上版本并开启Zygisk +2. 生成模块 + - GitHub Actions + 1. Fork这个项目 + 2. 在你fork的项目中选择**Actions**选项卡 + 3. 在左边的侧边栏中,单击**Build** + 4. 选择**Run workflow** + 5. 输入游戏包名并点击**Run workflow** + 6. 等待操作完成并下载 + - Android Studio + 1. 下载源码 + 2. 编辑`game.h`, 修改`AimPackageName`为游戏包名 + 3. 使用Android Studio运行gradle任务`:module:assembleRelease`编译,zip包会生成在`out`文件夹下 +3. 在Magisk里安装模块 +4. 启动游戏,会在`/data/data/AimPackageName/files/`目录下生成`dump.cs` \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..5e02054 --- /dev/null +++ b/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'idea' + +idea.module { + excludeDirs += file('out') + resourceDirs += file('template') + resourceDirs += file('scripts') +} + +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.4.2' + } +} + +allprojects { + repositories { + mavenCentral() + google() + } +} + +ext { + minSdkVersion = 23 + targetSdkVersion = 32 + + outDir = file("$rootDir/out") +} + +task clean(type: Delete) { + delete rootProject.buildDir, outDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..01b80d7 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2giqr}t zFG7D6)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^S&A^X^U}h20jpS zQsdeaA#WIE*<8KG*oXc~$izYilTc#z{5xhpXmdT-YUnGh9v4c#lrHG6X82F2-t35} zB`jo$HjKe~E*W$=g|j&P>70_cI`GnOQ;Jp*JK#CT zuEGCn{8A@bC)~0%wsEv?O^hSZF*iqjO~_h|>xv>PO+?525Nw2472(yqS>(#R)D7O( zg)Zrj9n9$}=~b00=Wjf?E418qP-@8%MQ%PBiCTX=$B)e5cHFDu$LnOeJ~NC;xmOk# z>z&TbsK>Qzk)!88lNI8fOE2$Uxso^j*1fz>6Ot49y@=po)j4hbTIcVR`ePHpuJSfp zxaD^Dn3X}Na3@<_Pc>a;-|^Pon(>|ytG_+U^8j_JxP=_d>L$Hj?|0lz>_qQ#a|$+( z(x=Lipuc8p4^}1EQhI|TubffZvB~lu$zz9ao%T?%ZLyV5S9}cLeT?c} z>yCN9<04NRi~1oR)CiBakoNhY9BPnv)kw%*iv8vdr&&VgLGIs(-FbJ?d_gfbL2={- zBk4lkdPk~7+jIxd4{M(-W1AC_WcN&Oza@jZoj zaE*9Y;g83#m(OhA!w~LNfUJNUuRz*H-=$s*z+q+;snKPRm9EptejugC-@7-a-}Tz0 z@KHra#Y@OXK+KsaSN9WiGf?&jlZ!V7L||%KHP;SLksMFfjkeIMf<1e~t?!G3{n)H8 zQAlFY#QwfKuj;l@<$YDATAk;%PtD%B(0<|8>rXU< zJ66rkAVW_~Dj!7JGdGGi4NFuE?7ZafdMxIh65Sz7yQoA7fBZCE@WwysB=+`kT^LFX zz8#FlSA5)6FG9(qL3~A24mpzL@@2D#>0J7mMS1T*9UJ zvOq!!a(%IYY69+h45CE?(&v9H4FCr>gK0>mK~F}5RdOuH2{4|}k@5XpsX7+LZo^Qa4sH5`eUj>iffoBVm+ zz4Mtf`h?NW$*q1yr|}E&eNl)J``SZvTf6Qr*&S%tVv_OBpbjnA0&Vz#(;QmGiq-k! zgS0br4I&+^2mgA15*~Cd00cXLYOLA#Ep}_)eED>m+K@JTPr_|lSN}(OzFXQSBc6fM z@f-%2;1@BzhZa*LFV z-LrLmkmB%<<&jEURBEW>soaZ*rSIJNwaV%-RSaCZi4X)qYy^PxZ=oL?6N-5OGOMD2 z;q_JK?zkwQ@b3~ln&sDtT5SpW9a0q+5Gm|fpVY2|zqlNYBR}E5+ahgdj!CvK$Tlk0 z9g$5N;aar=CqMsudQV>yb4l@hN(9Jcc=1(|OHsqH6|g=K-WBd8GxZ`AkT?OO z-z_Ued-??Z*R4~L7jwJ%-`s~FK|qNAJ;EmIVDVpk{Lr7T4l{}vL)|GuUuswe9c5F| zv*5%u01hlv08?00Vpwyk*Q&&fY8k6MjOfpZfKa@F-^6d=Zv|0@&4_544RP5(s|4VPVP-f>%u(J@23BHqo2=zJ#v9g=F!cP((h zpt0|(s++ej?|$;2PE%+kc6JMmJjDW)3BXvBK!h!E`8Y&*7hS{c_Z?4SFP&Y<3evqf z9-ke+bSj$%Pk{CJlJbWwlBg^mEC^@%Ou?o>*|O)rl&`KIbHrjcpqsc$Zqt0^^F-gU2O=BusO+(Op}!jNzLMc zT;0YT%$@ClS%V+6lMTfhuzzxomoat=1H?1$5Ei7&M|gxo`~{UiV5w64Np6xV zVK^nL$)#^tjhCpTQMspXI({TW^U5h&Wi1Jl8g?P1YCV4=%ZYyjSo#5$SX&`r&1PyC zzc;uzCd)VTIih|8eNqFNeBMe#j_FS6rq81b>5?aXg+E#&$m++Gz9<+2)h=K(xtn}F ziV{rmu+Y>A)qvF}ms}4X^Isy!M&1%$E!rTO~5(p+8{U6#hWu>(Ll1}eD64Xa>~73A*538wry?v$vW z>^O#FRdbj(k0Nr&)U`Tl(4PI*%IV~;ZcI2z&rmq=(k^}zGOYZF3b2~Klpzd2eZJl> zB=MOLwI1{$RxQ7Y4e30&yOx?BvAvDkTBvWPpl4V8B7o>4SJn*+h1Ms&fHso%XLN5j z-zEwT%dTefp~)J_C8;Q6i$t!dnlh-!%haR1X_NuYUuP-)`IGWjwzAvp!9@h`kPZhf zwLwFk{m3arCdx8rD~K2`42mIN4}m%OQ|f)4kf%pL?Af5Ul<3M2fv>;nlhEPR8b)u} zIV*2-wyyD%%) zl$G@KrC#cUwoL?YdQyf9WH)@gWB{jd5w4evI& zOFF)p_D8>;3-N1z6mES!OPe>B^<;9xsh)){Cw$Vs-ez5nXS95NOr3s$IU;>VZSzKn zBvub8_J~I%(DozZW@{)Vp37-zevxMRZ8$8iRfwHmYvyjOxIOAF2FUngKj289!(uxY zaClWm!%x&teKmr^ABrvZ(ikx{{I-lEzw5&4t3P0eX%M~>$wG0ZjA4Mb&op+0$#SO_ z--R`>X!aqFu^F|a!{Up-iF(K+alKB{MNMs>e(i@Tpy+7Z-dK%IEjQFO(G+2mOb@BO zP>WHlS#fSQm0et)bG8^ZDScGnh-qRKIFz zfUdnk=m){ej0i(VBd@RLtRq3Ep=>&2zZ2%&vvf?Iex01hx1X!8U+?>ER;yJlR-2q4 z;Y@hzhEC=d+Le%=esE>OQ!Q|E%6yG3V_2*uh&_nguPcZ{q?DNq8h_2ahaP6=pP-+x zK!(ve(yfoYC+n(_+chiJ6N(ZaN+XSZ{|H{TR1J_s8x4jpis-Z-rlRvRK#U%SMJ(`C z?T2 zF(NNfO_&W%2roEC2j#v*(nRgl1X)V-USp-H|CwFNs?n@&vpRcj@W@xCJwR6@T!jt377?XjZ06=`d*MFyTdyvW!`mQm~t3luzYzvh^F zM|V}rO>IlBjZc}9Z zd$&!tthvr>5)m;5;96LWiAV0?t)7suqdh0cZis`^Pyg@?t>Ms~7{nCU;z`Xl+raSr zXpp=W1oHB*98s!Tpw=R5C)O{{Inl>9l7M*kq%#w9a$6N~v?BY2GKOVRkXYCgg*d

<5G2M1WZP5 zzqSuO91lJod(SBDDw<*sX(+F6Uq~YAeYV#2A;XQu_p=N5X+#cmu19Qk>QAnV=k!?wbk5I;tDWgFc}0NkvC*G=V+Yh1cyeJVq~9czZiDXe+S=VfL2g`LWo8om z$Y~FQc6MFjV-t1Y`^D9XMwY*U_re2R?&(O~68T&D4S{X`6JYU-pz=}ew-)V0AOUT1 zVOkHAB-8uBcRjLvz<9HS#a@X*Kc@|W)nyiSgi|u5$Md|P()%2(?olGg@ypoJwp6>m z*dnfjjWC>?_1p;%1brqZyDRR;8EntVA92EJ3ByOxj6a+bhPl z;a?m4rQAV1@QU^#M1HX)0+}A<7TCO`ZR_RzF}X9-M>cRLyN4C+lCk2)kT^3gN^`IT zNP~fAm(wyIoR+l^lQDA(e1Yv}&$I!n?&*p6?lZcQ+vGLLd~fM)qt}wsbf3r=tmVYe zl)ntf#E!P7wlakP9MXS7m0nsAmqxZ*)#j;M&0De`oNmFgi$ov#!`6^4)iQyxg5Iuj zjLAhzQ)r`^hf7`*1`Rh`X;LVBtDSz@0T?kkT1o!ijeyTGt5vc^Cd*tmNgiNo^EaWvaC8$e+nb_{W01j3%=1Y&92YacjCi>eNbwk%-gPQ@H-+4xskQ}f_c=jg^S-# zYFBDf)2?@5cy@^@FHK5$YdAK9cI;!?Jgd}25lOW%xbCJ>By3=HiK@1EM+I46A)Lsd zeT|ZH;KlCml=@;5+hfYf>QNOr^XNH%J-lvev)$Omy8MZ`!{`j>(J5cG&ZXXgv)TaF zg;cz99i$4CX_@3MIb?GL0s*8J=3`#P(jXF(_(6DXZjc@(@h&=M&JG)9&Te1?(^XMW zjjC_70|b=9hB6pKQi`S^Ls7JyJw^@P>Ko^&q8F&?>6i;#CbxUiLz1ZH4lNyd@QACd zu>{!sqjB!2Dg}pbAXD>d!3jW}=5aN0b;rw*W>*PAxm7D)aw(c*RX2@bTGEI|RRp}vw7;NR2wa;rXN{L{Q#=Fa z$x@ms6pqb>!8AuV(prv>|aU8oWV={C&$c zMa=p=CDNOC2tISZcd8~18GN5oTbKY+Vrq;3_obJlfSKRMk;Hdp1`y`&LNSOqeauR_ z^j*Ojl3Ohzb5-a49A8s|UnM*NM8tg}BJXdci5%h&;$afbmRpN0&~9rCnBA`#lG!p zc{(9Y?A0Y9yo?wSYn>iigf~KP$0*@bGZ>*YM4&D;@{<%Gg5^uUJGRrV4 z(aZOGB&{_0f*O=Oi0k{@8vN^BU>s3jJRS&CJOl3o|BE{FAA&a#2YYiX3pZz@|Go-F z|Fly;7eX2OTs>R}<`4RwpHFs9nwh)B28*o5qK1Ge=_^w0m`uJOv!=&!tzt#Save(C zgKU=Bsgql|`ui(e1KVxR`?>Dx>(rD1$iWp&m`v)3A!j5(6vBm*z|aKm*T*)mo(W;R zNGo2`KM!^SS7+*9YxTm6YMm_oSrLceqN*nDOAtagULuZl5Q<7mOnB@Hq&P|#9y{5B z!2x+2s<%Cv2Aa0+u{bjZXS);#IFPk(Ph-K7K?3i|4ro> zRbqJoiOEYo(Im^((r}U4b8nvo_>4<`)ut`24?ILnglT;Pd&U}$lV3U$F9#PD(O=yV zgNNA=GW|(E=&m_1;uaNmipQe?pon4{T=zK!N!2_CJL0E*R^XXIKf*wi!>@l}3_P9Z zF~JyMbW!+n-+>!u=A1ESxzkJy$DRuG+$oioG7(@Et|xVbJ#BCt;J43Nvj@MKvTxzy zMmjNuc#LXBxFAwIGZJk~^!q$*`FME}yKE8d1f5Mp}KHNq(@=Z8YxV}0@;YS~|SpGg$_jG7>_8WWYcVx#4SxpzlV9N4aO>K{c z$P?a_fyDzGX$Of3@ykvedGd<@-R;M^Shlj*SswJLD+j@hi_&_>6WZ}#AYLR0iWMK|A zH_NBeu(tMyG=6VO-=Pb>-Q#$F*or}KmEGg*-n?vWQREURdB#+6AvOj*I%!R-4E_2$ zU5n9m>RWs|Wr;h2DaO&mFBdDb-Z{APGQx$(L`if?C|njd*fC=rTS%{o69U|meRvu?N;Z|Y zbT|ojL>j;q*?xXmnHH#3R4O-59NV1j=uapkK7}6@Wo*^Nd#(;$iuGsb;H315xh3pl zHaJ>h-_$hdNl{+|Zb%DZH%ES;*P*v0#}g|vrKm9;j-9e1M4qX@zkl&5OiwnCz=tb6 zz<6HXD+rGIVpGtkb{Q^LIgExOm zz?I|oO9)!BOLW#krLmWvX5(k!h{i>ots*EhpvAE;06K|u_c~y{#b|UxQ*O@Ks=bca z^_F0a@61j3I(Ziv{xLb8AXQj3;R{f_l6a#H5ukg5rxwF9A$?Qp-Mo54`N-SKc}fWp z0T)-L@V$$&my;l#Ha{O@!fK4-FSA)L&3<${Hcwa7ue`=f&YsXY(NgeDU#sRlT3+9J z6;(^(sjSK@3?oMo$%L-nqy*E;3pb0nZLx6 z;h5)T$y8GXK1DS-F@bGun8|J(v-9o=42&nLJy#}M5D0T^5VWBNn$RpC zZzG6Bt66VY4_?W=PX$DMpKAI!d`INr) zkMB{XPQ<52rvWVQqgI0OL_NWxoe`xxw&X8yVftdODPj5|t}S6*VMqN$-h9)1MBe0N zYq?g0+e8fJCoAksr0af1)FYtz?Me!Cxn`gUx&|T;)695GG6HF7!Kg1zzRf_{VWv^bo81v4$?F6u2g|wxHc6eJQAg&V z#%0DnWm2Rmu71rPJ8#xFUNFC*V{+N_qqFH@gYRLZ6C?GAcVRi>^n3zQxORPG)$-B~ z%_oB?-%Zf7d*Fe;cf%tQwcGv2S?rD$Z&>QC2X^vwYjnr5pa5u#38cHCt4G3|efuci z@3z=#A13`+ztmp;%zjXwPY_aq-;isu*hecWWX_=Z8paSqq7;XYnUjK*T>c4~PR4W7 z#C*%_H&tfGx`Y$w7`dXvVhmovDnT>btmy~SLf>>~84jkoQ%cv=MMb+a{JV&t0+1`I z32g_Y@yDhKe|K^PevP~MiiVl{Ou7^Mt9{lOnXEQ`xY^6L8D$705GON{!1?1&YJEl#fTf5Z)da=yiEQ zGgtC-soFGOEBEB~ZF_{7b(76En>d}mI~XIwNw{e>=Fv)sgcw@qOsykWr?+qAOZSVrQfg}TNI ztKNG)1SRrAt6#Q?(me%)>&A_^DM`pL>J{2xu>xa$3d@90xR61TQDl@fu%_85DuUUA za9tn64?At;{`BAW6oykwntxHeDpXsV#{tmt5RqdN7LtcF4vR~_kZNT|wqyR#z^Xcd zFdymVRZvyLfTpBT>w9<)Ozv@;Yk@dOSVWbbtm^y@@C>?flP^EgQPAwsy75bveo=}T zFxl(f)s)j(0#N_>Or(xEuV(n$M+`#;Pc$1@OjXEJZumkaekVqgP_i}p`oTx;terTx zZpT+0dpUya2hqlf`SpXN{}>PfhajNk_J0`H|2<5E;U5Vh4F8er z;RxLSFgpGhkU>W?IwdW~NZTyOBrQ84H7_?gviIf71l`EETodG9a1!8e{jW?DpwjL? zGEM&eCzwoZt^P*8KHZ$B<%{I}>46IT%jJ3AnnB5P%D2E2Z_ z1M!vr#8r}1|KTqWA4%67ZdbMW2YJ81b(KF&SQ2L1Qn(y-=J${p?xLMx3W7*MK;LFQ z6Z`aU;;mTL4XrrE;HY*Rkh6N%?qviUGNAKiCB~!P}Z->IpO6E(gGd7I#eDuT7j|?nZ zK}I(EJ>$Kb&@338M~O+em9(L!+=0zBR;JAQesx|3?Ok90)D1aS9P?yTh6Poh8Cr4X zk3zc=f2rE7jj+aP7nUsr@~?^EGP>Q>h#NHS?F{Cn`g-gD<8F&dqOh-0sa%pfL`b+1 zUsF*4a~)KGb4te&K0}bE>z3yb8% zibb5Q%Sfiv7feb1r0tfmiMv z@^4XYwg@KZI=;`wC)`1jUA9Kv{HKe2t$WmRcR4y8)VAFjRi zaz&O7Y2tDmc5+SX(bj6yGHYk$dBkWc96u3u&F)2yEE~*i0F%t9Kg^L6MJSb&?wrXi zGSc;_rln$!^ybwYBeacEFRsVGq-&4uC{F)*Y;<0y7~USXswMo>j4?~5%Zm!m@i@-> zXzi82sa-vpU{6MFRktJy+E0j#w`f`>Lbog{zP|9~hg(r{RCa!uGe>Yl536cn$;ouH za#@8XMvS-kddc1`!1LVq;h57~zV`7IYR}pp3u!JtE6Q67 zq3H9ZUcWPm2V4IukS}MCHSdF0qg2@~ufNx9+VMjQP&exiG_u9TZAeAEj*jw($G)zL zq9%#v{wVyOAC4A~AF=dPX|M}MZV)s(qI9@aIK?Pe+~ch|>QYb+78lDF*Nxz2-vpRbtQ*F4$0fDbvNM#CCatgQ@z1+EZWrt z2dZfywXkiW=no5jus-92>gXn5rFQ-COvKyegmL=4+NPzw6o@a?wGE-1Bt;pCHe;34K%Z z-FnOb%!nH;)gX+!a3nCk?5(f1HaWZBMmmC@lc({dUah+E;NOros{?ui1zPC-Q0);w zEbJmdE$oU$AVGQPdm{?xxI_0CKNG$LbY*i?YRQ$(&;NiA#h@DCxC(U@AJ$Yt}}^xt-EC_ z4!;QlLkjvSOhdx!bR~W|Ezmuf6A#@T`2tsjkr>TvW*lFCMY>Na_v8+{Y|=MCu1P8y z89vPiH5+CKcG-5lzk0oY>~aJC_0+4rS@c@ZVKLAp`G-sJB$$)^4*A!B zmcf}lIw|VxV9NSoJ8Ag3CwN&d7`|@>&B|l9G8tXT^BDHOUPrtC70NgwN4${$k~d_4 zJ@eo6%YQnOgq$th?0{h`KnqYa$Nz@vlHw<%!C5du6<*j1nwquk=uY}B8r7f|lY+v7 zm|JU$US08ugor8E$h3wH$c&i~;guC|3-tqJy#T;v(g( zBZtPMSyv%jzf->435yM(-UfyHq_D=6;ouL4!ZoD+xI5uCM5ay2m)RPmm$I}h>()hS zO!0gzMxc`BPkUZ)WXaXam%1;)gedA7SM8~8yIy@6TPg!hR0=T>4$Zxd)j&P-pXeSF z9W`lg6@~YDhd19B9ETv(%er^Xp8Yj@AuFVR_8t*KS;6VHkEDKI#!@l!l3v6`W1`1~ zP{C@keuV4Q`Rjc08lx?zmT$e$!3esc9&$XZf4nRL(Z*@keUbk!GZi(2Bmyq*saOD? z3Q$V<*P-X1p2}aQmuMw9nSMbOzuASsxten7DKd6A@ftZ=NhJ(0IM|Jr<91uAul4JR zADqY^AOVT3a(NIxg|U;fyc#ZnSzw2cr}#a5lZ38>nP{05D)7~ad7JPhw!LqOwATXtRhK!w0X4HgS1i<%AxbFmGJx9?sEURV+S{k~g zGYF$IWSlQonq6}e;B(X(sIH|;52+(LYW}v_gBcp|x%rEAVB`5LXg_d5{Q5tMDu0_2 z|LOm$@K2?lrLNF=mr%YP|U-t)~9bqd+wHb4KuPmNK<}PK6e@aosGZK57=Zt+kcszVOSbe;`E^dN! ze7`ha3WUUU7(nS0{?@!}{0+-VO4A{7+nL~UOPW9_P(6^GL0h${SLtqG!} zKl~Ng5#@Sy?65wk9z*3SA`Dpd4b4T^@C8Fhd8O)k_4%0RZL5?#b~jmgU+0|DB%0Z) zql-cPC>A9HPjdOTpPC` zQwvF}uB5kG$Xr4XnaH#ruSjM*xG?_hT7y3G+8Ox`flzU^QIgb_>2&-f+XB6MDr-na zSi#S+c!ToK84<&m6sCiGTd^8pNdXo+$3^l3FL_E`0 z>8it5YIDxtTp2Tm(?}FX^w{fbfgh7>^8mtvN>9fWgFN_*a1P`Gz*dyOZF{OV7BC#j zQV=FQM5m>47xXgapI$WbPM5V`V<7J9tD)oz@d~MDoM`R^Y6-Na(lO~uvZlpu?;zw6 zVO1faor3dg#JEb5Q*gz4<W8tgC3nE2BG2jeIQs1)<{In&7hJ39x=;ih;CJDy)>0S1at*7n?Wr0ahYCpFjZ|@u91Zl7( zv;CSBRC65-6f+*JPf4p1UZ)k=XivKTX6_bWT~7V#rq0Xjas6hMO!HJN8GdpBKg_$B zwDHJF6;z?h<;GXFZan8W{XFNPpOj!(&I1`&kWO86p?Xz`a$`7qV7Xqev|7nn_lQuX ziGpU1MMYt&5dE2A62iX3;*0WzNB9*nSTzI%62A+N?f?;S>N@8M=|ef3gtQTIA*=yq zQAAjOqa!CkHOQo4?TsqrrsJLclXcP?dlAVv?v`}YUjo1Htt;6djP@NPFH+&p1I+f_ z)Y279{7OWomY8baT(4TAOlz1OyD{4P?(DGv3XyJTA2IXe=kqD)^h(@*E3{I~w;ws8 z)ZWv7E)pbEM zd3MOXRH3mQhks9 zv6{s;k0y5vrcjXaVfw8^>YyPo=oIqd5IGI{)+TZq5Z5O&hXAw%ZlL}^6FugH;-%vP zAaKFtt3i^ag226=f0YjzdPn6|4(C2sC5wHFX{7QF!tG1E-JFA`>eZ`}$ymcRJK?0c zN363o{&ir)QySOFY0vcu6)kX#;l??|7o{HBDVJN+17rt|w3;(C_1b>d;g9Gp=8YVl zYTtA52@!7AUEkTm@P&h#eg+F*lR zQ7iotZTcMR1frJ0*V@Hw__~CL>_~2H2cCtuzYIUD24=Cv!1j6s{QS!v=PzwQ(a0HS zBKx04KA}-Ue+%9d`?PG*hIij@54RDSQpA7|>qYVIrK_G6%6;#ZkR}NjUgmGju)2F`>|WJoljo)DJgZr4eo1k1i1+o z1D{>^RlpIY8OUaOEf5EBu%a&~c5aWnqM zxBpJq98f=%M^{4mm~5`CWl%)nFR64U{(chmST&2jp+-r z3675V<;Qi-kJud%oWnCLdaU-)xTnMM%rx%Jw6v@=J|Ir=4n-1Z23r-EVf91CGMGNz zb~wyv4V{H-hkr3j3WbGnComiqmS0vn?n?5v2`Vi>{Ip3OZUEPN7N8XeUtF)Ry6>y> zvn0BTLCiqGroFu|m2zG-;Xb6;W`UyLw)@v}H&(M}XCEVXZQoWF=Ykr5lX3XWwyNyF z#jHv)A*L~2BZ4lX?AlN3X#axMwOC)PoVy^6lCGse9bkGjb=qz%kDa6}MOmSwK`cVO zt(e*MW-x}XtU?GY5}9{MKhRhYOlLhJE5=ca+-RmO04^ z66z{40J=s=ey9OCdc(RCzy zd7Zr1%!y3}MG(D=wM_ebhXnJ@MLi7cImDkhm0y{d-Vm81j`0mbi4lF=eirlr)oW~a zCd?26&j^m4AeXEsIUXiTal)+SPM4)HX%%YWF1?(FV47BaA`h9m67S9x>hWMVHx~Hg z1meUYoLL(p@b3?x|9DgWeI|AJ`Ia84*P{Mb%H$ZRROouR4wZhOPX15=KiBMHl!^JnCt$Az`KiH^_d>cev&f zaG2>cWf$=A@&GP~DubsgYb|L~o)cn5h%2`i^!2)bzOTw2UR!>q5^r&2Vy}JaWFUQE04v>2;Z@ZPwXr?y&G(B^@&y zsd6kC=hHdKV>!NDLIj+3rgZJ|dF`%N$DNd;B)9BbiT9Ju^Wt%%u}SvfM^=|q-nxDG zuWCQG9e#~Q5cyf8@y76#kkR^}{c<_KnZ0QsZcAT|YLRo~&tU|N@BjxOuy`#>`X~Q< z?R?-Gsk$$!oo(BveQLlUrcL#eirhgBLh`qHEMg`+sR1`A=1QX7)ZLMRT+GBy?&mM8 zQG^z-!Oa&J-k7I(3_2#Q6Bg=NX<|@X&+YMIOzfEO2$6Mnh}YV!m!e^__{W@-CTprr zbdh3f=BeCD$gHwCrmwgM3LAv3!Mh$wM)~KWzp^w)Cu6roO7uUG5z*}i0_0j47}pK; ztN530`ScGatLOL06~zO)Qmuv`h!gq5l#wx(EliKe&rz-5qH(hb1*fB#B+q`9=jLp@ zOa2)>JTl7ovxMbrif`Xe9;+fqB1K#l=Dv!iT;xF zdkCvS>C5q|O;}ns3AgoE({Ua-zNT-9_5|P0iANmC6O76Sq_(AN?UeEQJ>#b54fi3k zFmh+P%b1x3^)0M;QxXLP!BZ^h|AhOde*{9A=f3|Xq*JAs^Y{eViF|=EBfS6L%k4ip zk+7M$gEKI3?bQg?H3zaE@;cyv9kv;cqK$VxQbFEsy^iM{XXW0@2|DOu$!-k zSFl}Y=jt-VaT>Cx*KQnHTyXt}f9XswFB9ibYh+k2J!ofO+nD?1iw@mwtrqI4_i?nE zhLkPp41ED62me}J<`3RN80#vjW;wt`pP?%oQ!oqy7`miL>d-35a=qotK$p{IzeSk# ze_$CFYp_zIkrPFVaW^s#U4xT1lI^A0IBe~Y<4uS%zSV=wcuLr%gQT=&5$&K*bwqx| zWzCMiz>7t^Et@9CRUm9E+@hy~sBpm9fri$sE1zgLU((1?Yg{N1Sars=DiW&~Zw=3I zi7y)&oTC?UWD2w97xQ&5vx zRXEBGeJ(I?Y}eR0_O{$~)bMJRTsNUPIfR!xU9PE7A>AMNr_wbrFK>&vVw=Y;RH zO$mlpmMsQ}-FQ2cSj7s7GpC+~^Q~dC?y>M}%!-3kq(F3hGWo9B-Gn02AwUgJ>Z-pKOaj zysJBQx{1>Va=*e@sLb2z&RmQ7ira;aBijM-xQ&cpR>X3wP^foXM~u1>sv9xOjzZpX z0K;EGouSYD~oQ&lAafj3~EaXfFShC+>VsRlEMa9cg9i zFxhCKO}K0ax6g4@DEA?dg{mo>s+~RPI^ybb^u--^nTF>**0l5R9pocwB?_K)BG_)S zyLb&k%XZhBVr7U$wlhMqwL)_r&&n%*N$}~qijbkfM|dIWP{MyLx}X&}ES?}7i;9bW zmTVK@zR)7kE2+L42Q`n4m0VVg5l5(W`SC9HsfrLZ=v%lpef=Gj)W59VTLe+Z$8T8i z4V%5+T0t8LnM&H>Rsm5C%qpWBFqgTwL{=_4mE{S3EnBXknM&u8n}A^IIM4$s3m(Rd z>zq=CP-!9p9es2C*)_hoL@tDYABn+o#*l;6@7;knWIyDrt5EuakO99S$}n((Fj4y} zD!VvuRzghcE{!s;jC*<_H$y6!6QpePo2A3ZbX*ZzRnQq*b%KK^NF^z96CHaWmzU@f z#j;y?X=UP&+YS3kZx7;{ zDA{9(wfz7GF`1A6iB6fnXu0?&d|^p|6)%3$aG0Uor~8o? z*e}u#qz7Ri?8Uxp4m_u{a@%bztvz-BzewR6bh*1Xp+G=tQGpcy|4V_&*aOqu|32CM zz3r*E8o8SNea2hYJpLQ-_}R&M9^%@AMx&`1H8aDx4j%-gE+baf2+9zI*+Pmt+v{39 zDZ3Ix_vPYSc;Y;yn68kW4CG>PE5RoaV0n@#eVmk?p$u&Fy&KDTy!f^Hy6&^-H*)#u zdrSCTJPJw?(hLf56%2;_3n|ujUSJOU8VPOTlDULwt0jS@j^t1WS z!n7dZIoT+|O9hFUUMbID4Ec$!cc($DuQWkocVRcYSikFeM&RZ=?BW)mG4?fh#)KVG zcJ!<=-8{&MdE)+}?C8s{k@l49I|Zwswy^ZN3;E!FKyglY~Aq?4m74P-0)sMTGXqd5(S<-(DjjM z&7dL-Mr8jhUCAG$5^mI<|%`;JI5FVUnNj!VO2?Jiqa|c2;4^n!R z`5KK0hyB*F4w%cJ@Un6GC{mY&r%g`OX|1w2$B7wxu97%<@~9>NlXYd9RMF2UM>(z0 zouu4*+u+1*k;+nFPk%ly!nuMBgH4sL5Z`@Rok&?Ef=JrTmvBAS1h?C0)ty5+yEFRz zY$G=coQtNmT@1O5uk#_MQM1&bPPnspy5#>=_7%WcEL*n$;sSAZcXxMpcXxLe;_mLA z5F_paad+bGZV*oh@8h0(|D2P!q# zTHjmiphJ=AazSeKQPkGOR-D8``LjzToyx{lfK-1CDD6M7?pMZOdLKFtjZaZMPk4}k zW)97Fh(Z+_Fqv(Q_CMH-YYi?fR5fBnz7KOt0*t^cxmDoIokc=+`o# zrud|^h_?KW=Gv%byo~(Ln@({?3gnd?DUf-j2J}|$Mk>mOB+1{ZQ8HgY#SA8END(Zw z3T+W)a&;OO54~m}ffemh^oZ!Vv;!O&yhL0~hs(p^(Yv=(3c+PzPXlS5W79Er8B1o* z`c`NyS{Zj_mKChj+q=w)B}K za*zzPhs?c^`EQ;keH{-OXdXJet1EsQ)7;{3eF!-t^4_Srg4(Ot7M*E~91gwnfhqaM zNR7dFaWm7MlDYWS*m}CH${o?+YgHiPC|4?X?`vV+ws&Hf1ZO-w@OGG^o4|`b{bLZj z&9l=aA-Y(L11!EvRjc3Zpxk7lc@yH1e$a}8$_-r$)5++`_eUr1+dTb@ zU~2P1HM#W8qiNN3b*=f+FfG1!rFxnNlGx{15}BTIHgxO>Cq4 z;#9H9YjH%>Z2frJDJ8=xq>Z@H%GxXosS@Z>cY9ppF+)e~t_hWXYlrO6)0p7NBMa`+ z^L>-#GTh;k_XnE)Cgy|0Dw;(c0* zSzW14ZXozu)|I@5mRFF1eO%JM=f~R1dkNpZM+Jh(?&Zje3NgM{2ezg1N`AQg5%+3Y z64PZ0rPq6;_)Pj-hyIOgH_Gh`1$j1!jhml7ksHA1`CH3FDKiHLz+~=^u@kUM{ilI5 z^FPiJ7mSrzBs9{HXi2{sFhl5AyqwUnU{sPcUD{3+l-ZHAQ)C;c$=g1bdoxeG(5N01 zZy=t8i{*w9m?Y>V;uE&Uy~iY{pY4AV3_N;RL_jT_QtLFx^KjcUy~q9KcLE3$QJ{!)@$@En{UGG7&}lc*5Kuc^780;7Bj;)X?1CSy*^^ zPP^M)Pr5R>mvp3_hmCtS?5;W^e@5BjE>Cs<`lHDxj<|gtOK4De?Sf0YuK5GX9G93i zMYB{8X|hw|T6HqCf7Cv&r8A$S@AcgG1cF&iJ5=%+x;3yB`!lQ}2Hr(DE8=LuNb~Vs z=FO&2pdc16nD$1QL7j+!U^XWTI?2qQKt3H8=beVTdHHa9=MiJ&tM1RRQ-=+vy!~iz zj3O{pyRhCQ+b(>jC*H)J)%Wq}p>;?@W*Eut@P&?VU+Sdw^4kE8lvX|6czf{l*~L;J zFm*V~UC;3oQY(ytD|D*%*uVrBB}BbAfjK&%S;z;7$w68(8PV_whC~yvkZmX)xD^s6 z{$1Q}q;99W?*YkD2*;)tRCS{q2s@JzlO~<8x9}X<0?hCD5vpydvOw#Z$2;$@cZkYrp83J0PsS~!CFtY%BP=yxG?<@#{7%2sy zOc&^FJxsUYN36kSY)d7W=*1-{7ghPAQAXwT7z+NlESlkUH&8ODlpc8iC*iQ^MAe(B z?*xO4i{zFz^G=^G#9MsLKIN64rRJykiuIVX5~0#vAyDWc9-=6BDNT_aggS2G{B>dD ze-B%d3b6iCfc5{@yz$>=@1kdK^tX9qh0=ocv@9$ai``a_ofxT=>X7_Y0`X}a^M?d# z%EG)4@`^Ej_=%0_J-{ga!gFtji_byY&Vk@T1c|ucNAr(JNr@)nCWj?QnCyvXg&?FW;S-VOmNL6^km_dqiVjJuIASVGSFEos@EVF7St$WE&Z%)`Q##+0 zjaZ=JI1G@0!?l|^+-ZrNd$WrHBi)DA0-Eke>dp=_XpV<%CO_Wf5kQx}5e<90dt>8k zAi00d0rQ821nA>B4JHN7U8Zz=0;9&U6LOTKOaC1FC8GgO&kc=_wHIOGycL@c*$`ce703t%>S}mvxEnD-V!;6c`2(p74V7D0No1Xxt`urE66$0(ThaAZ1YVG#QP$ zy~NN%kB*zhZ2Y!kjn826pw4bh)75*e!dse+2Db(;bN34Uq7bLpr47XTX{8UEeC?2i z*{$`3dP}32${8pF$!$2Vq^gY|#w+VA_|o(oWmQX8^iw#n_crb(K3{69*iU?<%C-%H zuKi)3M1BhJ@3VW>JA`M>L~5*_bxH@Euy@niFrI$82C1}fwR$p2E&ZYnu?jlS}u7W9AyfdXh2pM>78bIt3 z)JBh&XE@zA!kyCDfvZ1qN^np20c1u#%P6;6tU&dx0phT1l=(mw7`u!-0e=PxEjDds z9E}{E!7f9>jaCQhw)&2TtG-qiD)lD(4jQ!q{`x|8l&nmtHkdul# zy+CIF8lKbp9_w{;oR+jSLtTfE+B@tOd6h=QePP>rh4@~!8c;Hlg9m%%&?e`*Z?qz5-zLEWfi>`ord5uHF-s{^bexKAoMEV@9nU z^5nA{f{dW&g$)BAGfkq@r5D)jr%!Ven~Q58c!Kr;*Li#`4Bu_?BU0`Y`nVQGhNZk@ z!>Yr$+nB=`z#o2nR0)V3M7-eVLuY`z@6CT#OTUXKnxZn$fNLPv7w1y7eGE=Qv@Hey`n;`U=xEl|q@CCV^#l)s0ZfT+mUf z^(j5r4)L5i2jnHW4+!6Si3q_LdOLQi<^fu?6WdohIkn79=jf%Fs3JkeXwF(?_tcF? z?z#j6iXEd(wJy4|p6v?xNk-)iIf2oX5^^Y3q3ziw16p9C6B;{COXul%)`>nuUoM*q zzmr|NJ5n)+sF$!yH5zwp=iM1#ZR`O%L83tyog-qh1I z0%dcj{NUs?{myT~33H^(%0QOM>-$hGFeP;U$puxoJ>>o-%Lk*8X^rx1>j|LtH$*)>1C!Pv&gd16%`qw5LdOIUbkNhaBBTo}5iuE%K&ZV^ zAr_)kkeNKNYJRgjsR%vexa~&8qMrQYY}+RbZ)egRg9_$vkoyV|Nc&MH@8L)`&rpqd zXnVaI@~A;Z^c3+{x=xgdhnocA&OP6^rr@rTvCnhG6^tMox$ulw2U7NgUtW%|-5VeH z_qyd47}1?IbuKtqNbNx$HR`*+9o=8`%vM8&SIKbkX9&%TS++x z5|&6P<%=F$C?owUI`%uvUq^yW0>`>yz!|WjzsoB9dT;2Dx8iSuK%%_XPgy0dTD4kd zDXF@&O_vBVVKQq(9YTClUPM30Sk7B!v7nOyV`XC!BA;BIVwphh+c)?5VJ^(C;GoQ$ zvBxr7_p*k$T%I1ke}`U&)$uf}I_T~#3XTi53OX)PoXVgxEcLJgZG^i47U&>LY(l%_ z;9vVDEtuMCyu2fqZeez|RbbIE7@)UtJvgAcVwVZNLccswxm+*L&w`&t=ttT=sv6Aq z!HouSc-24Y9;0q$>jX<1DnnGmAsP))- z^F~o99gHZw`S&Aw7e4id6Lg7kMk-e)B~=tZ!kE7sGTOJ)8@q}np@j7&7Sy{2`D^FH zI7aX%06vKsfJ168QnCM2=l|i>{I{%@gcr>ExM0Dw{PX6ozEuqFYEt z087%MKC;wVsMV}kIiuu9Zz9~H!21d!;Cu#b;hMDIP7nw3xSX~#?5#SSjyyg+Y@xh| z%(~fv3`0j#5CA2D8!M2TrG=8{%>YFr(j)I0DYlcz(2~92?G*?DeuoadkcjmZszH5& zKI@Lis%;RPJ8mNsbrxH@?J8Y2LaVjUIhRUiO-oqjy<&{2X~*f|)YxnUc6OU&5iac= z*^0qwD~L%FKiPmlzi&~a*9sk2$u<7Al=_`Ox^o2*kEv?p`#G(p(&i|ot8}T;8KLk- zPVf_4A9R`5^e`Om2LV*cK59EshYXse&IoByj}4WZaBomoHAPKqxRKbPcD`lMBI)g- zeMRY{gFaUuecSD6q!+b5(?vAnf>c`Z(8@RJy%Ulf?W~xB1dFAjw?CjSn$ph>st5bc zUac1aD_m6{l|$#g_v6;=32(mwpveQDWhmjR7{|B=$oBhz`7_g7qNp)n20|^^op3 zSfTdWV#Q>cb{CMKlWk91^;mHap{mk)o?udk$^Q^^u@&jd zfZ;)saW6{e*yoL6#0}oVPb2!}r{pAUYtn4{P~ES9tTfC5hXZnM{HrC8^=Pof{G4%Bh#8 ze~?C9m*|fd8MK;{L^!+wMy>=f^8b&y?yr6KnTq28$pFMBW9Oy7!oV5z|VM$s-cZ{I|Xf@}-)1=$V&x7e;9v81eiTi4O5-vs?^5pCKy2l>q);!MA zS!}M48l$scB~+Umz}7NbwyTn=rqt@`YtuwiQSMvCMFk2$83k50Q>OK5&fe*xCddIm)3D0I6vBU<+!3=6?(OhkO|b4fE_-j zimOzyfBB_*7*p8AmZi~X2bgVhyPy>KyGLAnOpou~sx9)S9%r)5dE%ADs4v%fFybDa_w*0?+>PsEHTbhKK^G=pFz z@IxLTCROWiKy*)cV3y%0FwrDvf53Ob_XuA1#tHbyn%Ko!1D#sdhBo`;VC*e1YlhrC z?*y3rp86m#qI|qeo8)_xH*G4q@70aXN|SP+6MQ!fJQqo1kwO_v7zqvUfU=Gwx`CR@ zRFb*O8+54%_8tS(ADh}-hUJzE`s*8wLI>1c4b@$al)l}^%GuIXjzBK!EWFO8W`>F^ ze7y#qPS0NI7*aU)g$_ziF(1ft;2<}6Hfz10cR8P}67FD=+}MfhrpOkF3hFhQu;Q1y zu%=jJHTr;0;oC94Hi@LAF5quAQ(rJG(uo%BiRQ@8U;nhX)j0i?0SL2g-A*YeAqF>RVCBOTrn{0R27vu}_S zS>tX4!#&U4W;ikTE!eFH+PKw%p+B(MR2I%n#+m0{#?qRP_tR@zpgCb=4rcrL!F=;A zh%EIF8m6%JG+qb&mEfuFTLHSxUAZEvC-+kvZKyX~SA3Umt`k}}c!5dy?-sLIM{h@> z!2=C)@nx>`;c9DdwZ&zeUc(7t<21D7qBj!|1^Mp1eZ6)PuvHx+poKSDCSBMFF{bKy z;9*&EyKitD99N}%mK8431rvbT+^%|O|HV23{;RhmS{$5tf!bIPoH9RKps`-EtoW5h zo6H_!s)Dl}2gCeGF6>aZtah9iLuGd19^z0*OryPNt{70RvJSM<#Ox9?HxGg04}b^f zrVEPceD%)#0)v5$YDE?f`73bQ6TA6wV;b^x*u2Ofe|S}+q{s5gr&m~4qGd!wOu|cZ||#h_u=k*fB;R6&k?FoM+c&J;ISg70h!J7*xGus)ta4veTdW)S^@sU@ z4$OBS=a~@F*V0ECic;ht4@?Jw<9kpjBgHfr2FDPykCCz|v2)`JxTH55?b3IM={@DU z!^|9nVO-R#s{`VHypWyH0%cs;0GO3E;It6W@0gX6wZ%W|Dzz&O%m17pa19db(er}C zUId1a4#I+Ou8E1MU$g=zo%g7K(=0Pn$)Rk z<4T2u<0rD)*j+tcy2XvY+0 z0d2pqm4)4lDewsAGThQi{2Kc3&C=|OQF!vOd#WB_`4gG3@inh-4>BoL!&#ij8bw7? zqjFRDaQz!J-YGitV4}$*$hg`vv%N)@#UdzHFI2E<&_@0Uw@h_ZHf}7)G;_NUD3@18 zH5;EtugNT0*RXVK*by>WS>jaDDfe!A61Da=VpIK?mcp^W?!1S2oah^wowRnrYjl~`lgP-mv$?yb6{{S55CCu{R z$9;`dyf0Y>uM1=XSl_$01Lc1Iy68IosWN8Q9Op=~I(F<0+_kKfgC*JggjxNgK6 z-3gQm6;sm?J&;bYe&(dx4BEjvq}b`OT^RqF$J4enP1YkeBK#>l1@-K`ajbn05`0J?0daOtnzh@l3^=BkedW1EahZlRp;`j*CaT;-21&f2wU z+Nh-gc4I36Cw+;3UAc<%ySb`#+c@5y ze~en&bYV|kn?Cn|@fqmGxgfz}U!98$=drjAkMi`43I4R%&H0GKEgx-=7PF}y`+j>r zg&JF`jomnu2G{%QV~Gf_-1gx<3Ky=Md9Q3VnK=;;u0lyTBCuf^aUi?+1+`4lLE6ZK zT#(Bf`5rmr(tgTbIt?yA@y`(Ar=f>-aZ}T~>G32EM%XyFvhn&@PWCm#-<&ApLDCXT zD#(9m|V(OOo7PmE@`vD4$S5;+9IQm19dd zvMEU`)E1_F+0o0-z>YCWqg0u8ciIknU#{q02{~YX)gc_u;8;i233D66pf(IkTDxeN zL=4z2)?S$TV9=ORVr&AkZMl<4tTh(v;Ix1{`pPVqI3n2ci&4Dg+W|N8TBUfZ*WeLF zqCH_1Q0W&f9T$lx3CFJ$o@Lz$99 zW!G&@zFHxTaP!o#z^~xgF|(vrHz8R_r9eo;TX9}2ZyjslrtH=%6O)?1?cL&BT(Amp zTGFU1%%#xl&6sH-UIJk_PGk_McFn7=%yd6tAjm|lnmr8bE2le3I~L{0(ffo}TQjyo zHZZI{-}{E4ohYTlZaS$blB!h$Jq^Rf#(ch}@S+Ww&$b);8+>g84IJcLU%B-W?+IY& zslcZIR>+U4v3O9RFEW;8NpCM0w1ROG84=WpKxQ^R`{=0MZCubg3st z48AyJNEvyxn-jCPTlTwp4EKvyEwD3e%kpdY?^BH0!3n6Eb57_L%J1=a*3>|k68A}v zaW`*4YitylfD}ua8V)vb79)N_Ixw_mpp}yJGbNu+5YYOP9K-7nf*jA1#<^rb4#AcS zKg%zCI)7cotx}L&J8Bqo8O1b0q;B1J#B5N5Z$Zq=wX~nQFgUfAE{@u0+EnmK{1hg> zC{vMfFLD;L8b4L+B51&LCm|scVLPe6h02rws@kGv@R+#IqE8>Xn8i|vRq_Z`V;x6F zNeot$1Zsu`lLS92QlLWF54za6vOEKGYQMdX($0JN*cjG7HP&qZ#3+bEN$8O_PfeAb z0R5;=zXac2IZ?fxu59?Nka;1lKm|;0)6|#RxkD05P5qz;*AL@ig!+f=lW5^Jbag%2 z%9@iM0ph$WFlxS!`p31t92z~TB}P-*CS+1Oo_g;7`6k(Jyj8m8U|Q3Sh7o-Icp4kV zK}%qri5>?%IPfamXIZ8pXbm-#{ytiam<{a5A+3dVP^xz!Pvirsq7Btv?*d7eYgx7q zWFxrzb3-%^lDgMc=Vl7^={=VDEKabTG?VWqOngE`Kt7hs236QKidsoeeUQ_^FzsXjprCDd@pW25rNx#6x&L6ZEpoX9Ffzv@olnH3rGOSW( zG-D|cV0Q~qJ>-L}NIyT?T-+x+wU%;+_GY{>t(l9dI%Ximm+Kmwhee;FK$%{dnF;C% zFjM2&$W68Sz#d*wtfX?*WIOXwT;P6NUw}IHdk|)fw*YnGa0rHx#paG!m=Y6GkS4VX zX`T$4eW9k1W!=q8!(#8A9h67fw))k_G)Q9~Q1e3f`aV@kbcSv7!priDUN}gX(iXTy zr$|kU0Vn%*ylmyDCO&G0Z3g>%JeEPFAW!5*H2Ydl>39w3W+gEUjL&vrRs(xGP{(ze zy7EMWF14@Qh>X>st8_029||TP0>7SG9on_xxeR2Iam3G~Em$}aGsNt$iES9zFa<3W zxtOF*!G@=PhfHO!=9pVPXMUVi30WmkPoy$02w}&6A7mF)G6-`~EVq5CwD2`9Zu`kd)52``#V zNSb`9dG~8(dooi1*-aSMf!fun7Sc`-C$-E(3BoSC$2kKrVcI!&yC*+ff2+C-@!AT_ zsvlAIV+%bRDfd{R*TMF><1&_a%@yZ0G0lg2K;F>7b+7A6pv3-S7qWIgx+Z?dt8}|S z>Qbb6x(+^aoV7FQ!Ph8|RUA6vXWQH*1$GJC+wXLXizNIc9p2yLzw9 z0=MdQ!{NnOwIICJc8!+Jp!zG}**r#E!<}&Te&}|B4q;U57$+pQI^}{qj669zMMe_I z&z0uUCqG%YwtUc8HVN7?0GHpu=bL7&{C>hcd5d(iFV{I5c~jpX&!(a{yS*4MEoYXh z*X4|Y@RVfn;piRm-C%b@{0R;aXrjBtvx^HO;6(>i*RnoG0Rtcd25BT6edxTNOgUAOjn zJ2)l{ipj8IP$KID2}*#F=M%^n&=bA0tY98@+2I+7~A&T-tw%W#3GV>GTmkHaqftl)#+E zMU*P(Rjo>8%P@_@#UNq(_L{}j(&-@1iY0TRizhiATJrnvwSH0v>lYfCI2ex^><3$q znzZgpW0JlQx?JB#0^^s-Js1}}wKh6f>(e%NrMwS`Q(FhazkZb|uyB@d%_9)_xb$6T zS*#-Bn)9gmobhAtvBmL+9H-+0_0US?g6^TOvE8f3v=z3o%NcPjOaf{5EMRnn(_z8- z$|m0D$FTU zDy;21v-#0i)9%_bZ7eo6B9@Q@&XprR&oKl4m>zIj-fiRy4Dqy@VVVs?rscG| zmzaDQ%>AQTi<^vYCmv#KOTd@l7#2VIpsj?nm_WfRZzJako`^uU%Nt3e;cU*y*|$7W zLm%fX#i_*HoUXu!NI$ey>BA<5HQB=|nRAwK!$L#n-Qz;~`zACig0PhAq#^5QS<8L2 zS3A+8%vbVMa7LOtTEM?55apt(DcWh#L}R^P2AY*c8B}Cx=6OFAdMPj1f>k3#^#+Hk z6uW1WJW&RlBRh*1DLb7mJ+KO>!t^t8hX1#_Wk`gjDio9)9IGbyCAGI4DJ~orK+YRv znjxRMtshZQHc$#Y-<-JOV6g^Cr@odj&Xw5B(FmI)*qJ9NHmIz_r{t)TxyB`L-%q5l ztzHgD;S6cw?7Atg*6E1!c6*gPRCb%t7D%z<(xm+K{%EJNiI2N0l8ud0Ch@_av_RW? zIr!nO4dL5466WslE6MsfMss7<)-S!e)2@r2o=7_W)OO`~CwklRWzHTfpB)_HYwgz=BzLhgZ9S<{nLBOwOIgJU=94uj6r!m>Xyn9>&xP+=5!zG_*yEoRgM0`aYts z^)&8(>z5C-QQ*o_s(8E4*?AX#S^0)aqB)OTyX>4BMy8h(cHjA8ji1PRlox@jB*1n? zDIfyDjzeg91Ao(;Q;KE@zei$}>EnrF6I}q&Xd=~&$WdDsyH0H7fJX|E+O~%LS*7^Q zYzZ4`pBdY{b7u72gZm6^5~O-57HwzwAz{)NvVaowo`X02tL3PpgLjwA`^i9F^vSpN zAqH3mRjG8VeJNHZ(1{%!XqC+)Z%D}58Qel{_weSEHoygT9pN@i zi=G;!Vj6XQk2tuJC>lza%ywz|`f7TIz*EN2Gdt!s199Dr4Tfd_%~fu8gXo~|ogt5Q zlEy_CXEe^BgsYM^o@L?s33WM14}7^T(kqohOX_iN@U?u;$l|rAvn{rwy>!yfZw13U zB@X9)qt&4;(C6dP?yRsoTMI!j-f1KC!<%~i1}u7yLXYn)(#a;Z6~r>hp~kfP));mi zcG%kdaB9H)z9M=H!f>kM->fTjRVOELNwh1amgKQT=I8J66kI)u_?0@$$~5f`u%;zl zC?pkr^p2Fe=J~WK%4ItSzKA+QHqJ@~m|Cduv=Q&-P8I5rQ-#G@bYH}YJr zUS(~(w|vKyU(T(*py}jTUp%I%{2!W!K(i$uvotcPjVddW z8_5HKY!oBCwGZcs-q`4Yt`Zk~>K?mcxg51wkZlX5e#B08I75F7#dgn5yf&Hrp`*%$ zQ;_Qg>TYRzBe$x=T(@WI9SC!ReSas9vDm(yslQjBJZde5z8GDU``r|N(MHcxNopGr z_}u39W_zwWDL*XYYt>#Xo!9kL#97|EAGyGBcRXtLTd59x%m=3i zL^9joWYA)HfL15l9%H?q`$mY27!<9$7GH(kxb%MV>`}hR4a?+*LH6aR{dzrX@?6X4 z3e`9L;cjqYb`cJmophbm(OX0b)!AFG?5`c#zLagzMW~o)?-!@e80lvk!p#&CD8u5_r&wp4O0zQ>y!k5U$h_K;rWGk=U)zX!#@Q%|9g*A zWx)qS1?fq6X<$mQTB$#3g;;5tHOYuAh;YKSBz%il3Ui6fPRv#v62SsrCdMRTav)Sg zTq1WOu&@v$Ey;@^+_!)cf|w_X<@RC>!=~+A1-65O0bOFYiH-)abINwZvFB;hJjL_$ z(9iScmUdMp2O$WW!520Hd0Q^Yj?DK%YgJD^ez$Z^?@9@Ab-=KgW@n8nC&88)TDC+E zlJM)L3r+ZJfZW_T$;Imq*#2<(j+FIk8ls7)WJ6CjUu#r5PoXxQs4b)mZza<8=v{o)VlLRM<9yw^0En#tXAj`Sylxvki{<1DPe^ zhjHwx^;c8tb?Vr$6ZB;$Ff$+3(*oinbwpN-#F)bTsXq@Sm?43MC#jQ~`F|twI=7oC zH4TJtu#;ngRA|Y~w5N=UfMZi?s0%ZmKUFTAye&6Y*y-%c1oD3yQ%IF2q2385Zl+=> zfz=o`Bedy|U;oxbyb^rB9ixG{Gb-{h$U0hVe`J;{ql!s_OJ_>>eoQn(G6h7+b^P48 zG<=Wg2;xGD-+d@UMZ!c;0>#3nws$9kIDkK13IfloGT@s14AY>&>>^#>`PT7GV$2Hp zN<{bN*ztlZu_%W=&3+=#3bE(mka6VoHEs~0BjZ$+=0`a@R$iaW)6>wp2w)=v2@|2d z%?34!+iOc5S@;AAC4hELWLH56RGxo4jw8MDMU0Wk2k_G}=Vo(>eRFo(g3@HjG|`H3 zm8b*dK=moM*oB<)*A$M9!!5o~4U``e)wxavm@O_R(`P|u%9^LGi(_%IF<6o;NLp*0 zKsfZ0#24GT8(G`i4UvoMh$^;kOhl?`0yNiyrC#HJH=tqOH^T_d<2Z+ zeN>Y9Zn!X4*DMCK^o75Zk2621bdmV7Rx@AX^alBG4%~;G_vUoxhfhFRlR&+3WwF^T zaL)8xPq|wCZoNT^>3J0K?e{J-kl+hu2rZI>CUv#-z&u@`hjeb+bBZ>bcciQVZ{SbW zez04s9oFEgc8Z+Kp{XFX`MVf-s&w9*dx7wLen(_@y34}Qz@&`$2+osqfxz4&d}{Ql z*g1ag00Gu+$C`0avds{Q65BfGsu9`_`dML*rX~hyWIe$T>CsPRoLIr%MTk3pJ^2zH1qub1MBzPG}PO;Wmav9w%F7?%l=xIf#LlP`! z_Nw;xBQY9anH5-c8A4mME}?{iewjz(Sq-29r{fV;Fc>fv%0!W@(+{={Xl-sJ6aMoc z)9Q+$bchoTGTyWU_oI19!)bD=IG&OImfy;VxNXoIO2hYEfO~MkE#IXTK(~?Z&!ae! zl8z{D&2PC$Q*OBC(rS~-*-GHNJ6AC$@eve>LB@Iq;jbBZj`wk4|LGogE||Ie=M5g= z9d`uYQ1^Sr_q2wmZE>w2WG)!F%^KiqyaDtIAct?}D~JP4shTJy5Bg+-(EA8aXaxbd~BKMtTf2iQ69jD1o* zZF9*S3!v-TdqwK$%&?91Sh2=e63;X0Lci@n7y3XOu2ofyL9^-I767eHESAq{m+@*r zbVDx!FQ|AjT;!bYsXv8ilQjy~Chiu&HNhFXt3R_6kMC8~ChEFqG@MWu#1Q1#=~#ix zrkHpJre_?#r=N0wv`-7cHHqU`phJX2M_^{H0~{VP79Dv{6YP)oA1&TSfKPEPZn2)G z9o{U1huZBLL;Tp_0OYw@+9z(jkrwIGdUrOhKJUbwy?WBt zlIK)*K0lQCY0qZ!$%1?3A#-S70F#YyUnmJF*`xx?aH5;gE5pe-15w)EB#nuf6B*c~ z8Z25NtY%6Wlb)bUA$w%HKs5$!Z*W?YKV-lE0@w^{4vw;J>=rn?u!rv$&eM+rpU6rc=j9>N2Op+C{D^mospMCjF2ZGhe4eADA#skp2EA26%p3Ex9wHW8l&Y@HX z$Qv)mHM}4*@M*#*ll5^hE9M^=q~eyWEai*P;4z<9ZYy!SlNE5nlc7gm;M&Q zKhKE4d*%A>^m0R?{N}y|i6i^k>^n4(wzKvlQeHq{l&JuFD~sTsdhs`(?lFK@Q{pU~ zb!M3c@*3IwN1RUOVjY5>uT+s-2QLWY z4T2>fiSn>>Fob+%B868-v9D@AfWr#M8eM6w#eAlhc#zk6jkLxGBGk`E3$!A@*am!R zy>29&ptYK6>cvP`b!syNp)Q$0UOW|-O@)8!?94GOYF_}+zlW%fCEl|Tep_zx05g6q z>tp47e-&R*hSNe{6{H!mL?+j$c^TXT{C&@T-xIaesNCl05 z9SLb@q&mSb)I{VXMaiWa3PWj=Ed!>*GwUe;^|uk=Pz$njNnfFY^MM>E?zqhf6^{}0 zx&~~dA5#}1ig~7HvOQ#;d9JZBeEQ+}-~v$at`m!(ai z$w(H&mWCC~;PQ1$%iuz3`>dWeb3_p}X>L2LK%2l59Tyc}4m0>9A!8rhoU3m>i2+hl zx?*qs*c^j}+WPs>&v1%1Ko8_ivAGIn@QK7A`hDz-Emkcgv2@wTbYhkiwX2l=xz*XG zaiNg+j4F-I>9v+LjosI-QECrtKjp&0T@xIMKVr+&)gyb4@b3y?2CA?=ooN zT#;rU86WLh(e@#mF*rk(NV-qSIZyr z$6!ZUmzD)%yO-ot`rw3rp6?*_l*@Z*IB0xn4|BGPWHNc-1ZUnNSMWmDh=EzWJRP`) zl%d%J613oXzh5;VY^XWJi{lB`f#u+ThvtP7 zq(HK<4>tw(=yzSBWtYO}XI`S1pMBe3!jFxBHIuwJ(@%zdQFi1Q_hU2eDuHqXte7Ki zOV55H2D6u#4oTfr7|u*3p75KF&jaLEDpxk!4*bhPc%mpfj)Us3XIG3 zIKMX^s^1wt8YK7Ky^UOG=w!o5e7W-<&c|fw2{;Q11vm@J{)@N3-p1U>!0~sKWHaL= zWV(0}1IIyt1p%=_-Fe5Kfzc71wg}`RDDntVZv;4!=&XXF-$48jS0Sc;eDy@Sg;+{A zFStc{dXT}kcIjMXb4F7MbX~2%i;UrBxm%qmLKb|2=?uPr00-$MEUIGR5+JG2l2Nq` zkM{{1RO_R)+8oQ6x&-^kCj)W8Z}TJjS*Wm4>hf+4#VJP)OBaDF%3pms7DclusBUw} z{ND#!*I6h85g6DzNvdAmnwWY{&+!KZM4DGzeHI?MR@+~|su0{y-5-nICz_MIT_#FE zm<5f3zlaKq!XyvY3H`9s&T};z!cK}G%;~!rpzk9-6L}4Rg7vXtKFsl}@sT#U#7)x- z7UWue5sa$R>N&b{J61&gvKcKlozH*;OjoDR+elkh|4bJ!_3AZNMOu?n9&|L>OTD78 z^i->ah_Mqc|Ev)KNDzfu1P3grBIM#%`QZqj5W{qu(HocQhjyS;UINoP`{J+DvV?|1 z_sw6Yr3z6%e7JKVDY<$P=M)dbk@~Yw9|2!Cw!io3%j92wTD!c^e9Vj+7VqXo3>u#= zv#M{HHJ=e$X5vQ>>ML?E8#UlmvJgTnb73{PSPTf*0)mcj6C z{KsfUbDK|F$E(k;ER%8HMdDi`=BfpZzP3cl5yJHu;v^o2FkHNk;cXc17tL8T!CsYI zfeZ6sw@;8ia|mY_AXjCS?kUfxdjDB28)~Tz1dGE|{VfBS9`0m2!m1yG?hR})er^pl4c@9Aq+|}ZlDaHL)K$O| z%9Jp-imI-Id0|(d5{v~w6mx)tUKfbuVD`xNt04Mry%M+jXzE>4(TBsx#&=@wT2Vh) z1yeEY&~17>0%P(eHP0HB^|7C+WJxQBTG$uyOWY@iDloRIb-Cf!p<{WQHR!422#F34 zG`v|#CJ^G}y9U*7jgTlD{D&y$Iv{6&PYG>{Ixg$pGk?lWrE#PJ8KunQC@}^6OP!|< zS;}p3to{S|uZz%kKe|;A0bL0XxPB&Q{J(9PyX`+Kr`k~r2}yP^ND{8!v7Q1&vtk& z2Y}l@J@{|2`oA%sxvM9i0V+8IXrZ4;tey)d;LZI70Kbim<4=WoTPZy=Yd|34v#$Kh zx|#YJ8s`J>W&jt#GcMpx84w2Z3ur-rK7gf-p5cE)=w1R2*|0mj12hvapuUWM0b~dG zMg9p8FmAZI@i{q~0@QuY44&mMUNXd7z>U58shA3o`p5eVLpq>+{(<3->DWuSFVZwC zxd50Uz(w~LxC4}bgag#q#NNokK@yNc+Q|Ap!u>Ddy+df>v;j@I12CDNN9do+0^n8p zMQs7X#+FVF0C5muGfN{r0|Nkql%BQT|K(DDNdR2pzM=_ea5+GO|J67`05AV92t@4l z0Qno0078PIHdaQGHZ~Scw!dzgqjK~3B7kf>BcP__&lLyU(cu3B^uLo%{j|Mb0NR)tkeT7Hcwp4O# z)yzu>cvG(d9~0a^)eZ;;%3ksk@F&1eEBje~ zW+-_s)&RgiweQc!otF>4%vbXKaOU41{!hw?|2`Ld3I8$&#WOsq>EG)1ANb!{N4z9@ zsU!bPG-~-bqCeIDzo^Q;gnucB{tRzm{ZH^Orphm2U+REA!*<*J6YQV83@&xoDl%#wnl5qcBqCcAF-vX5{30}(oJrnSH z{RY85hylK2dMOh2%oO1J8%)0?8TOL%rS8)+CsDv}aQ>4D)Jv+DLK)9gI^n-T^$)Tc zFPUD75qJm!Y-KBqj;JP4dV4 z`X{lGmn<)1IGz330}s}Jrjtf{(lnuuNHe5(ezA(pYa=1|Ff-LhPFK8 zyJh_b{yzu0yll6ZkpRzRjezyYivjyjW7QwO;@6X`m;2Apn2EK2!~7S}-*=;5*7K$B z`x(=!^?zgj(-`&ApZJXI09aDLXaT@<;CH=?fBOY5d|b~wBA@@p^K#nxr`)?i?SqTupI_PJ(A3cx`z~9mX_*)>L F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2ab173e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon May 22 11:22:38 CST 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/module.gradle b/module.gradle new file mode 100644 index 0000000..b294adf --- /dev/null +++ b/module.gradle @@ -0,0 +1,9 @@ +ext { + moduleLibraryName = "myinjector" + magiskModuleId = "zygisk_myinjector" + moduleName = "myinjector" + moduleAuthor = "jiqiu2021" + moduleDescription = "注入任意SO到指定APP内" + moduleVersion = "v0.01" + moduleVersionCode = 1 +} diff --git a/module/.gitignore b/module/.gitignore new file mode 100644 index 0000000..9833d4b --- /dev/null +++ b/module/.gitignore @@ -0,0 +1,3 @@ +/.externalNativeBuild +/build +/release \ No newline at end of file diff --git a/module/build.gradle b/module/build.gradle new file mode 100644 index 0000000..915645c --- /dev/null +++ b/module/build.gradle @@ -0,0 +1,107 @@ +import org.apache.tools.ant.filters.FixCrLfFilter + +import java.nio.file.Paths +import java.nio.file.Files + +apply plugin: 'com.android.library' +apply from: file(rootProject.file('module.gradle')) + +android { + compileSdkVersion rootProject.ext.targetSdkVersion + ndkVersion '25.2.9519653' + defaultConfig { + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + externalNativeBuild { + cmake { + arguments "-DMODULE_NAME:STRING=$moduleLibraryName" + } + } + } + buildFeatures { + prefab true + } + externalNativeBuild { + cmake { + path "src/main/cpp/CMakeLists.txt" + version "3.22.1" + } + } +} + +repositories { + mavenLocal() +} + +afterEvaluate { + android.libraryVariants.forEach { variant -> + def variantCapped = variant.name.capitalize() + def variantLowered = variant.name.toLowerCase() + + def zipName = "${magiskModuleId.replace('_', '-')}-${moduleVersion}-${variantLowered}.zip" + def magiskDir = file("$outDir/magisk_module_$variantLowered") + + task("prepareMagiskFiles${variantCapped}", type: Sync) { + dependsOn("assemble$variantCapped") + + def templatePath = "$rootDir/template/magisk_module" + + into magiskDir + from(templatePath) { + exclude 'module.prop' + } + from(templatePath) { + include 'module.prop' + expand([ + id : magiskModuleId, + name : moduleName, + version : moduleVersion, + versionCode: moduleVersionCode.toString(), + author : moduleAuthor, + description: moduleDescription, + ]) + filter(FixCrLfFilter.class, + eol: FixCrLfFilter.CrLf.newInstance("lf")) + } + from("$buildDir/intermediates/stripped_native_libs/$variantLowered/out/lib") { + into 'lib' + } + doLast { + file("$magiskDir/zygisk").mkdir() + fileTree("$magiskDir/lib").visit { f -> + if (!f.directory) return + def srcPath = Paths.get("${f.file.absolutePath}/lib${moduleLibraryName}.so") + def dstPath = Paths.get("$magiskDir/zygisk/${f.path}.so") + Files.move(srcPath, dstPath) + } + new File("$magiskDir/lib").deleteDir() + } + } + + task("zip${variantCapped}", type: Zip) { + dependsOn("prepareMagiskFiles${variantCapped}") + from magiskDir + archiveFileName.set(zipName) + destinationDirectory.set(outDir) + } + + task("push${variantCapped}", type: Exec) { + dependsOn("zip${variantCapped}") + workingDir outDir + commandLine android.adbExecutable, "push", zipName, "/data/local/tmp/" + } + + task("flash${variantCapped}", type: Exec) { + dependsOn("push${variantCapped}") + commandLine android.adbExecutable, "shell", "su", "-c", + "magisk --install-module /data/local/tmp/${zipName}" + } + + task("flashAndReboot${variantCapped}", type: Exec) { + dependsOn("flash${variantCapped}") + commandLine android.adbExecutable, "shell", "reboot" + } + + variant.assembleProvider.get().finalizedBy("zip${variantCapped}") + } +} diff --git a/module/src/main/AndroidManifest.xml b/module/src/main/AndroidManifest.xml new file mode 100644 index 0000000..762b99d --- /dev/null +++ b/module/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/module/src/main/cpp/CMakeLists.txt b/module/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..4549504 --- /dev/null +++ b/module/src/main/cpp/CMakeLists.txt @@ -0,0 +1,45 @@ +cmake_minimum_required(VERSION 3.18.1) + +if (NOT DEFINED MODULE_NAME) + message(FATAL_ERROR "MODULE_NAME is not set") +else () + project(${MODULE_NAME}) +endif () + +message("Build type: ${CMAKE_BUILD_TYPE}") + +set(CMAKE_CXX_STANDARD 20) + +set(LINKER_FLAGS "-ffixed-x18 -Wl,--hash-style=both") +set(C_FLAGS "-Werror=format -fdata-sections -ffunction-sections") +set(CXX_FLAGS "${CXX_FLAGS} -fno-exceptions -fno-rtti") + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + set(C_FLAGS "${C_FLAGS} -O2 -fvisibility=hidden -fvisibility-inlines-hidden") + set(LINKER_FLAGS "${LINKER_FLAGS} -Wl,-exclude-libs,ALL -Wl,--gc-sections -Wl,--strip-all") +else () + set(C_FLAGS "${C_FLAGS} -O0") +endif () + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${C_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${C_FLAGS} ${CXX_FLAGS}") + +set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}") +set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${LINKER_FLAGS}") + +include_directories( + xdl/include +) + +aux_source_directory(xdl xdl-src) + +add_library(${MODULE_NAME} SHARED + main.cpp + hack.cpp + ${xdl-src}) +target_link_libraries(${MODULE_NAME} log) + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + add_custom_command(TARGET ${MODULE_NAME} POST_BUILD + COMMAND ${CMAKE_STRIP} --strip-all --remove-section=.comment "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${MODULE_NAME}.so") +endif () diff --git a/module/src/main/cpp/game.h b/module/src/main/cpp/game.h new file mode 100644 index 0000000..64378e7 --- /dev/null +++ b/module/src/main/cpp/game.h @@ -0,0 +1,10 @@ +// +// Created by Perfare on 2020/7/4. +// + +#ifndef ZYGISK_IL2CPPDUMPER_GAME_H +#define ZYGISK_IL2CPPDUMPER_GAME_H + +#define AimPackageName "com.example.testdlopen" + +#endif //ZYGISK_IL2CPPDUMPER_GAME_H diff --git a/module/src/main/cpp/hack.cpp b/module/src/main/cpp/hack.cpp new file mode 100644 index 0000000..3bcf9a0 --- /dev/null +++ b/module/src/main/cpp/hack.cpp @@ -0,0 +1,264 @@ +// +// Created by Perfare on 2020/7/4. +// + +#include "hack.h" +#include "log.h" +#include "xdl.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +//#include +#include + +void hack_start(const char *game_data_dir,JavaVM *vm) { + bool load = false; + LOGI("hack_start %s", game_data_dir); + // 构建新文件路径 + char new_so_path[256]; + snprintf(new_so_path, sizeof(new_so_path), "%s/files/%s.so", game_data_dir, "test"); + + // 复制 /sdcard/test.so 到 game_data_dir 并重命名 + const char *src_path = "/data/local/tmp/test.so"; + int src_fd = open(src_path, O_RDONLY); + if (src_fd < 0) { + LOGE("Failed to open %s: %s (errno: %d)", src_path, strerror(errno), errno); + return; + } + + int dest_fd = open(new_so_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (dest_fd < 0) { + LOGE("Failed to open %s", new_so_path); + close(src_fd); + return; + } + // 复制文件内容 + char buffer[4096]; + ssize_t bytes; + while ((bytes = read(src_fd, buffer, sizeof(buffer))) > 0) { + if (write(dest_fd, buffer, bytes) != bytes) { + LOGE("Failed to write to %s", new_so_path); + close(src_fd); + close(dest_fd); + return; + } + } + + close(src_fd); + close(dest_fd); + if (chmod(new_so_path, 0755) != 0) { + LOGE("Failed to change permissions on %s: %s (errno: %d)", new_so_path, strerror(errno), errno); + return; + } else { + LOGI("Successfully changed permissions to 755 on %s", new_so_path); + } + void * handle; + // 使用 xdl_open 打开新复制的 so 文件 + for (int i = 0; i < 10; i++) { +// void *handle = xdl_open(new_so_path, 0); + handle = dlopen(new_so_path, RTLD_NOW | RTLD_LOCAL); + if (handle) { + LOGI("Successfully loaded %s", new_so_path); + load = true; + break; + } else { + LOGE("Failed to load %s: %s", new_so_path, dlerror()); + sleep(1); + } + } + if (!load) { + LOGI("test.so not found in thread %d", gettid()); + } + void (*JNI_OnLoad)(JavaVM *, void *); + *(void **) (&JNI_OnLoad) = dlsym(handle, "JNI_OnLoad"); + if (JNI_OnLoad) { + LOGI("JNI_OnLoad symbol found, calling JNI_OnLoad."); + JNI_OnLoad(vm, NULL); + } else { + LOGE("JNI_OnLoad symbol not found in %s", new_so_path); + } + +} + +std::string GetLibDir(JavaVM *vms) { + JNIEnv *env = nullptr; + vms->AttachCurrentThread(&env, nullptr); + jclass activity_thread_clz = env->FindClass("android/app/ActivityThread"); + if (activity_thread_clz != nullptr) { + jmethodID currentApplicationId = env->GetStaticMethodID(activity_thread_clz, + "currentApplication", + "()Landroid/app/Application;"); + if (currentApplicationId) { + jobject application = env->CallStaticObjectMethod(activity_thread_clz, + currentApplicationId); + jclass application_clazz = env->GetObjectClass(application); + if (application_clazz) { + jmethodID get_application_info = env->GetMethodID(application_clazz, + "getApplicationInfo", + "()Landroid/content/pm/ApplicationInfo;"); + if (get_application_info) { + jobject application_info = env->CallObjectMethod(application, + get_application_info); + jfieldID native_library_dir_id = env->GetFieldID( + env->GetObjectClass(application_info), "nativeLibraryDir", + "Ljava/lang/String;"); + if (native_library_dir_id) { + auto native_library_dir_jstring = (jstring) env->GetObjectField( + application_info, native_library_dir_id); + auto path = env->GetStringUTFChars(native_library_dir_jstring, nullptr); + LOGI("lib dir %s", path); + std::string lib_dir(path); + env->ReleaseStringUTFChars(native_library_dir_jstring, path); + return lib_dir; + } else { + LOGE("nativeLibraryDir not found"); + } + } else { + LOGE("getApplicationInfo not found"); + } + } else { + LOGE("application class not found"); + } + } else { + LOGE("currentApplication not found"); + } + } else { + LOGE("ActivityThread not found"); + } + return {}; +} + +static std::string GetNativeBridgeLibrary() { + auto value = std::array(); + __system_property_get("ro.dalvik.vm.native.bridge", value.data()); + return {value.data()}; +} + +struct NativeBridgeCallbacks { + uint32_t version; + void *initialize; + + void *(*loadLibrary)(const char *libpath, int flag); + + void *(*getTrampoline)(void *handle, const char *name, const char *shorty, uint32_t len); + + void *isSupported; + void *getAppEnv; + void *isCompatibleWith; + void *getSignalHandler; + void *unloadLibrary; + void *getError; + void *isPathSupported; + void *initAnonymousNamespace; + void *createNamespace; + void *linkNamespaces; + + void *(*loadLibraryExt)(const char *libpath, int flag, void *ns); +}; + +bool NativeBridgeLoad(const char *game_data_dir, int api_level, void *data, size_t length) { + //TODO 等待houdini初始化 + sleep(5); + + auto libart = dlopen("libart.so", RTLD_NOW); + auto JNI_GetCreatedJavaVMs = (jint (*)(JavaVM **, jsize, jsize *)) dlsym(libart, + "JNI_GetCreatedJavaVMs"); + LOGI("JNI_GetCreatedJavaVMs %p", JNI_GetCreatedJavaVMs); + JavaVM *vms_buf[1]; + JavaVM *vms; + jsize num_vms; + jint status = JNI_GetCreatedJavaVMs(vms_buf, 1, &num_vms); + if (status == JNI_OK && num_vms > 0) { + vms = vms_buf[0]; + } else { + LOGE("GetCreatedJavaVMs error"); + return false; + } + + auto lib_dir = GetLibDir(vms); + if (lib_dir.empty()) { + LOGE("GetLibDir error"); + return false; + } + if (lib_dir.find("/lib/x86") != std::string::npos) { + LOGI("no need NativeBridge"); + munmap(data, length); + return false; + } + + auto nb = dlopen("libhoudini.so", RTLD_NOW); + if (!nb) { + auto native_bridge = GetNativeBridgeLibrary(); + LOGI("native bridge: %s", native_bridge.data()); + nb = dlopen(native_bridge.data(), RTLD_NOW); + } + if (nb) { + LOGI("nb %p", nb); + auto callbacks = (NativeBridgeCallbacks *) dlsym(nb, "NativeBridgeItf"); + if (callbacks) { + LOGI("NativeBridgeLoadLibrary %p", callbacks->loadLibrary); + LOGI("NativeBridgeLoadLibraryExt %p", callbacks->loadLibraryExt); + LOGI("NativeBridgeGetTrampoline %p", callbacks->getTrampoline); + int fd = syscall(__NR_memfd_create, "anon", MFD_CLOEXEC); + ftruncate(fd, (off_t) length); + void *mem = mmap(nullptr, length, PROT_WRITE, MAP_SHARED, fd, 0); + memcpy(mem, data, length); + munmap(mem, length); + munmap(data, length); + char path[PATH_MAX]; + snprintf(path, PATH_MAX, "/proc/self/fd/%d", fd); + LOGI("arm path %s", path); + + void *arm_handle; + if (api_level >= 26) { + arm_handle = callbacks->loadLibraryExt(path, RTLD_NOW, (void *) 3); + } else { + arm_handle = callbacks->loadLibrary(path, RTLD_NOW); + } + if (arm_handle) { + LOGI("arm handle %p", arm_handle); + auto init = (void (*)(JavaVM *, void *)) callbacks->getTrampoline(arm_handle, + "JNI_OnLoad", + nullptr, 0); + LOGI("JNI_OnLoad %p", init); + init(vms, (void *) game_data_dir); + return true; + } + close(fd); + } + } + return false; +} + +void hack_prepare(const char *_data_dir, void *data, size_t length) { + LOGI("hack thread: %d", gettid()); + int api_level = android_get_device_api_level(); + LOGI("api level: %d", api_level); + +#if defined(__i386__) || defined(__x86_64__) + if (!NativeBridgeLoad(_data_dir, api_level, data, length)) { +#endif + hack_start(_data_dir, nullptr); +#if defined(__i386__) || defined(__x86_64__) + } +#endif +} + +#if defined(__arm__) || defined(__aarch64__) + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + auto game_data_dir = (const char *) reserved; + std::thread hack_thread(hack_start, game_data_dir,vm); + hack_thread.detach(); + return JNI_VERSION_1_6; +} + +#endif \ No newline at end of file diff --git a/module/src/main/cpp/hack.h b/module/src/main/cpp/hack.h new file mode 100644 index 0000000..30912ed --- /dev/null +++ b/module/src/main/cpp/hack.h @@ -0,0 +1,12 @@ +// +// Created by Perfare on 2020/7/4. +// + +#ifndef ZYGISK_IL2CPPDUMPER_HACK_H +#define ZYGISK_IL2CPPDUMPER_HACK_H + +#include + +void hack_prepare(const char *game_data_dir, void *data, size_t length); + +#endif //ZYGISK_IL2CPPDUMPER_HACK_H diff --git a/module/src/main/cpp/log.h b/module/src/main/cpp/log.h new file mode 100644 index 0000000..bb4e5f1 --- /dev/null +++ b/module/src/main/cpp/log.h @@ -0,0 +1,16 @@ +// +// Created by Perfare on 2020/7/4. +// + +#ifndef ZYGISK_IL2CPPDUMPER_LOG_H +#define ZYGISK_IL2CPPDUMPER_LOG_H + +#include + +#define LOG_TAG "Perfare" +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) + +#endif //ZYGISK_IL2CPPDUMPER_LOG_H diff --git a/module/src/main/cpp/main.cpp b/module/src/main/cpp/main.cpp new file mode 100644 index 0000000..5fa0b4f --- /dev/null +++ b/module/src/main/cpp/main.cpp @@ -0,0 +1,81 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "hack.h" +#include "zygisk.hpp" +#include "game.h" +#include "log.h" +#include "dlfcn.h" +using zygisk::Api; +using zygisk::AppSpecializeArgs; +using zygisk::ServerSpecializeArgs; + +class MyModule : public zygisk::ModuleBase { +public: + void onLoad(Api *api, JNIEnv *env) override { + this->api = api; + this->env = env; + } + + void preAppSpecialize(AppSpecializeArgs *args) override { + auto package_name = env->GetStringUTFChars(args->nice_name, nullptr); + auto app_data_dir = env->GetStringUTFChars(args->app_data_dir, nullptr); + LOGI("preAppSpecialize %s %s", package_name, app_data_dir); + preSpecialize(package_name, app_data_dir); + env->ReleaseStringUTFChars(args->nice_name, package_name); + env->ReleaseStringUTFChars(args->app_data_dir, app_data_dir); + } + + void postAppSpecialize(const AppSpecializeArgs *) override { + if (enable_hack) { + std::thread hack_thread(hack_prepare, _data_dir, data, length); + hack_thread.detach(); + } + } + +private: + Api *api; + JNIEnv *env; + bool enable_hack; + char *_data_dir; + void *data; + size_t length; + + void preSpecialize(const char *package_name, const char *app_data_dir) { + if (strcmp(package_name, AimPackageName) == 0) { + LOGI("成功注入目标进程: %s", package_name); + enable_hack = true; + _data_dir = new char[strlen(app_data_dir) + 1]; + strcpy(_data_dir, app_data_dir); + +#if defined(__i386__) + auto path = "zygisk/armeabi-v7a.so"; +#endif +#if defined(__x86_64__) + auto path = "zygisk/arm64-v8a.so"; +#endif +#if defined(__i386__) || defined(__x86_64__) + int dirfd = api->getModuleDir(); + int fd = openat(dirfd, path, O_RDONLY); + if (fd != -1) { + struct stat sb{}; + fstat(fd, &sb); + length = sb.st_size; + data = mmap(nullptr, length, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + } else { + LOGW("Unable to open arm file"); + } +#endif + } else { + api->setOption(zygisk::Option::DLCLOSE_MODULE_LIBRARY); + } + } +}; + +REGISTER_ZYGISK_MODULE(MyModule) \ No newline at end of file diff --git a/module/src/main/cpp/xdl/include/xdl.h b/module/src/main/cpp/xdl/include/xdl.h new file mode 100644 index 0000000..c4ef845 --- /dev/null +++ b/module/src/main/cpp/xdl/include/xdl.h @@ -0,0 +1,92 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2020-10-04. + +// +// xDL version: 1.2.1 +// +// xDL is an enhanced implementation of the Android DL series functions. +// For more information, documentation, and the latest version please check: +// https://github.com/hexhacking/xDL +// + +#ifndef IO_HEXHACKING_XDL +#define IO_HEXHACKING_XDL + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + // same as Dl_info: + const char *dli_fname; // Pathname of shared object that contains address. + void *dli_fbase; // Address at which shared object is loaded. + const char *dli_sname; // Name of nearest symbol with address lower than addr. + void *dli_saddr; // Exact address of symbol named in dli_sname. + // added by xDL: + size_t dli_ssize; // Symbol size of nearest symbol with address lower than addr. + const ElfW(Phdr) *dlpi_phdr; // Pointer to array of ELF program headers for this object. + size_t dlpi_phnum; // Number of items in dlpi_phdr. +} xdl_info_t; + +// +// Default value for flags in both xdl_open() and xdl_iterate_phdr(). +// +#define XDL_DEFAULT 0x00 + +// +// Enhanced dlopen() / dlclose() / dlsym(). +// +#define XDL_TRY_FORCE_LOAD 0x01 +#define XDL_ALWAYS_FORCE_LOAD 0x02 +void *xdl_open(const char *filename, int flags); +void *xdl_close(void *handle); +void *xdl_sym(void *handle, const char *symbol, size_t *symbol_size); +void *xdl_dsym(void *handle, const char *symbol, size_t *symbol_size); + +// +// Enhanced dladdr(). +// +int xdl_addr(void *addr, xdl_info_t *info, void **cache); +void xdl_addr_clean(void **cache); + +// +// Enhanced dl_iterate_phdr(). +// +#define XDL_FULL_PATHNAME 0x01 +int xdl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data, int flags); + +// +// Custom dlinfo(). +// +#define XDL_DI_DLINFO 1 // type of info: xdl_info_t +int xdl_info(void *handle, int request, void *info); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/module/src/main/cpp/xdl/xdl.c b/module/src/main/cpp/xdl/xdl.c new file mode 100644 index 0000000..9267f26 --- /dev/null +++ b/module/src/main/cpp/xdl/xdl.c @@ -0,0 +1,869 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2020-10-04. + +#include "xdl.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xdl_iterate.h" +#include "xdl_linker.h" +#include "xdl_lzma.h" +#include "xdl_util.h" + +#ifndef __LP64__ +#define XDL_LIB_PATH "/system/lib" +#else +#define XDL_LIB_PATH "/system/lib64" +#endif + +#define XDL_DYNSYM_IS_EXPORT_SYM(shndx) (SHN_UNDEF != (shndx)) +#define XDL_SYMTAB_IS_EXPORT_SYM(shndx) \ + (SHN_UNDEF != (shndx) && !((shndx) >= SHN_LORESERVE && (shndx) <= SHN_HIRESERVE)) + +extern __attribute((weak)) unsigned long int getauxval(unsigned long int); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpadded" + +typedef struct xdl { + char *pathname; + uintptr_t load_bias; + const ElfW(Phdr) *dlpi_phdr; + ElfW(Half) dlpi_phnum; + + struct xdl *next; // to next xdl obj for cache in xdl_addr() + void *linker_handle; // hold handle returned by xdl_linker_load() + + // + // (1) for searching symbols from .dynsym + // + + bool dynsym_try_load; + ElfW(Sym) *dynsym; // .dynsym + const char *dynstr; // .dynstr + + // .hash (SYSV hash for .dynstr) + struct { + const uint32_t *buckets; + uint32_t buckets_cnt; + const uint32_t *chains; + uint32_t chains_cnt; + } sysv_hash; + + // .gnu.hash (GNU hash for .dynstr) + struct { + const uint32_t *buckets; + uint32_t buckets_cnt; + const uint32_t *chains; + uint32_t symoffset; + const ElfW(Addr) *bloom; + uint32_t bloom_cnt; + uint32_t bloom_shift; + } gnu_hash; + + // + // (2) for searching symbols from .symtab + // + + bool symtab_try_load; + uintptr_t base; + + ElfW(Sym) *symtab; // .symtab + size_t symtab_cnt; + char *strtab; // .strtab + size_t strtab_sz; +} xdl_t; + +#pragma clang diagnostic pop + +// load from memory +static int xdl_dynsym_load(xdl_t *self) { + // find the dynamic segment + ElfW(Dyn) *dynamic = NULL; + for (size_t i = 0; i < self->dlpi_phnum; i++) { + const ElfW(Phdr) *phdr = &(self->dlpi_phdr[i]); + if (PT_DYNAMIC == phdr->p_type) { + dynamic = (ElfW(Dyn) *)(self->load_bias + phdr->p_vaddr); + break; + } + } + if (NULL == dynamic) return -1; + + // iterate the dynamic segment + for (ElfW(Dyn) *entry = dynamic; entry && entry->d_tag != DT_NULL; entry++) { + switch (entry->d_tag) { + case DT_SYMTAB: //.dynsym + self->dynsym = (ElfW(Sym) *)(self->load_bias + entry->d_un.d_ptr); + break; + case DT_STRTAB: //.dynstr + self->dynstr = (const char *)(self->load_bias + entry->d_un.d_ptr); + break; + case DT_HASH: //.hash + self->sysv_hash.buckets_cnt = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[0]; + self->sysv_hash.chains_cnt = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[1]; + self->sysv_hash.buckets = &(((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[2]); + self->sysv_hash.chains = &(self->sysv_hash.buckets[self->sysv_hash.buckets_cnt]); + break; + case DT_GNU_HASH: //.gnu.hash + self->gnu_hash.buckets_cnt = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[0]; + self->gnu_hash.symoffset = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[1]; + self->gnu_hash.bloom_cnt = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[2]; + self->gnu_hash.bloom_shift = ((const uint32_t *)(self->load_bias + entry->d_un.d_ptr))[3]; + self->gnu_hash.bloom = (const ElfW(Addr) *)(self->load_bias + entry->d_un.d_ptr + 16); + self->gnu_hash.buckets = (const uint32_t *)(&(self->gnu_hash.bloom[self->gnu_hash.bloom_cnt])); + self->gnu_hash.chains = (const uint32_t *)(&(self->gnu_hash.buckets[self->gnu_hash.buckets_cnt])); + break; + default: + break; + } + } + + if (NULL == self->dynsym || NULL == self->dynstr || + (0 == self->sysv_hash.buckets_cnt && 0 == self->gnu_hash.buckets_cnt)) { + self->dynsym = NULL; + self->dynstr = NULL; + self->sysv_hash.buckets_cnt = 0; + self->gnu_hash.buckets_cnt = 0; + return -1; + } + + return 0; +} + +static void *xdl_read_file_to_heap(int file_fd, size_t file_sz, size_t data_offset, size_t data_len) { + if (0 == data_len) return NULL; + if (data_offset >= file_sz) return NULL; + if (data_offset + data_len > file_sz) return NULL; + + if (data_offset != (size_t)lseek(file_fd, (off_t)data_offset, SEEK_SET)) return NULL; + + void *data = malloc(data_len); + if (NULL == data) return NULL; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgnu-statement-expression" + if ((ssize_t)data_len != XDL_UTIL_TEMP_FAILURE_RETRY(read(file_fd, data, data_len))) +#pragma clang diagnostic pop + { + free(data); + return NULL; + } + + return data; +} + +static void *xdl_read_file_to_heap_by_section(int file_fd, size_t file_sz, ElfW(Shdr) *shdr) { + return xdl_read_file_to_heap(file_fd, file_sz, (size_t)shdr->sh_offset, shdr->sh_size); +} + +static void *xdl_read_memory_to_heap(void *mem, size_t mem_sz, size_t data_offset, size_t data_len) { + if (0 == data_len) return NULL; + if (data_offset >= mem_sz) return NULL; + if (data_offset + data_len > mem_sz) return NULL; + + void *data = malloc(data_len); + if (NULL == data) return NULL; + + memcpy(data, (void *)((uintptr_t)mem + data_offset), data_len); + return data; +} + +static void *xdl_read_memory_to_heap_by_section(void *mem, size_t mem_sz, ElfW(Shdr) *shdr) { + return xdl_read_memory_to_heap(mem, mem_sz, (size_t)shdr->sh_offset, shdr->sh_size); +} + +static void *xdl_get_memory(void *mem, size_t mem_sz, size_t data_offset, size_t data_len) { + if (0 == data_len) return NULL; + if (data_offset >= mem_sz) return NULL; + if (data_offset + data_len > mem_sz) return NULL; + + return (void *)((uintptr_t)mem + data_offset); +} + +static void *xdl_get_memory_by_section(void *mem, size_t mem_sz, ElfW(Shdr) *shdr) { + return xdl_get_memory(mem, mem_sz, (size_t)shdr->sh_offset, shdr->sh_size); +} + +// load from disk and memory +static int xdl_symtab_load_from_debugdata(xdl_t *self, int file_fd, size_t file_sz, + ElfW(Shdr) *shdr_debugdata) { + void *debugdata = NULL; + ElfW(Shdr) *shdrs = NULL; + int r = -1; + + // get zipped .gnu_debugdata + uint8_t *debugdata_zip = (uint8_t *)xdl_read_file_to_heap_by_section(file_fd, file_sz, shdr_debugdata); + if (NULL == debugdata_zip) return -1; + + // get unzipped .gnu_debugdata + size_t debugdata_sz; + if (0 != xdl_lzma_decompress(debugdata_zip, shdr_debugdata->sh_size, (uint8_t **)&debugdata, &debugdata_sz)) + goto end; + + // get ELF header + ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)debugdata; + if (0 == ehdr->e_shnum || ehdr->e_shentsize != sizeof(ElfW(Shdr))) goto end; + + // get section headers + shdrs = (ElfW(Shdr) *)xdl_read_memory_to_heap(debugdata, debugdata_sz, (size_t)ehdr->e_shoff, + ehdr->e_shentsize * ehdr->e_shnum); + if (NULL == shdrs) goto end; + + // get .shstrtab + if (SHN_UNDEF == ehdr->e_shstrndx || ehdr->e_shstrndx >= ehdr->e_shnum) goto end; + char *shstrtab = (char *)xdl_get_memory_by_section(debugdata, debugdata_sz, shdrs + ehdr->e_shstrndx); + if (NULL == shstrtab) goto end; + + // find .symtab & .strtab + for (ElfW(Shdr) *shdr = shdrs; shdr < shdrs + ehdr->e_shnum; shdr++) { + char *shdr_name = shstrtab + shdr->sh_name; + + if (SHT_SYMTAB == shdr->sh_type && 0 == strcmp(".symtab", shdr_name)) { + // get & check associated .strtab section + if (shdr->sh_link >= ehdr->e_shnum) continue; + ElfW(Shdr) *shdr_strtab = shdrs + shdr->sh_link; + if (SHT_STRTAB != shdr_strtab->sh_type) continue; + + // get .symtab & .strtab + ElfW(Sym) *symtab = (ElfW(Sym) *)xdl_read_memory_to_heap_by_section(debugdata, debugdata_sz, shdr); + if (NULL == symtab) continue; + char *strtab = (char *)xdl_read_memory_to_heap_by_section(debugdata, debugdata_sz, shdr_strtab); + if (NULL == strtab) { + free(symtab); + continue; + } + + // OK + self->symtab = symtab; + self->symtab_cnt = shdr->sh_size / shdr->sh_entsize; + self->strtab = strtab; + self->strtab_sz = shdr_strtab->sh_size; + r = 0; + break; + } + } + +end: + free(debugdata_zip); + if (NULL != debugdata) free(debugdata); + if (NULL != shdrs) free(shdrs); + return r; +} + +// load from disk and memory +static int xdl_symtab_load(xdl_t *self) { + if ('[' == self->pathname[0]) return -1; + + int r = -1; + ElfW(Shdr) *shdrs = NULL; + char *shstrtab = NULL; + + // get base address + uintptr_t vaddr_min = UINTPTR_MAX; + for (size_t i = 0; i < self->dlpi_phnum; i++) { + const ElfW(Phdr) *phdr = &(self->dlpi_phdr[i]); + if (PT_LOAD == phdr->p_type) { + if (vaddr_min > phdr->p_vaddr) vaddr_min = phdr->p_vaddr; + } + } + if (UINTPTR_MAX == vaddr_min) return -1; + self->base = self->load_bias + vaddr_min; + + // open file + int flags = O_RDONLY | O_CLOEXEC; + int file_fd; + if ('/' == self->pathname[0]) { + file_fd = open(self->pathname, flags); + } else { + char full_pathname[1024]; + // try the fast method + snprintf(full_pathname, sizeof(full_pathname), "%s/%s", XDL_LIB_PATH, self->pathname); + file_fd = open(full_pathname, flags); + if (file_fd < 0) { + // try the slow method + if (0 != xdl_iterate_get_full_pathname(self->base, full_pathname, sizeof(full_pathname))) return -1; + file_fd = open(full_pathname, flags); + } + } + if (file_fd < 0) return -1; + struct stat st; + if (0 != fstat(file_fd, &st)) goto end; + size_t file_sz = (size_t)st.st_size; + + // get ELF header + ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)self->base; + if (0 == ehdr->e_shnum || ehdr->e_shentsize != sizeof(ElfW(Shdr))) goto end; + + // get section headers + shdrs = (ElfW(Shdr) *)xdl_read_file_to_heap(file_fd, file_sz, (size_t)ehdr->e_shoff, + ehdr->e_shentsize * ehdr->e_shnum); + if (NULL == shdrs) goto end; + + // get .shstrtab + if (SHN_UNDEF == ehdr->e_shstrndx || ehdr->e_shstrndx >= ehdr->e_shnum) goto end; + shstrtab = (char *)xdl_read_file_to_heap_by_section(file_fd, file_sz, shdrs + ehdr->e_shstrndx); + if (NULL == shstrtab) goto end; + + // find .symtab & .strtab + for (ElfW(Shdr) *shdr = shdrs; shdr < shdrs + ehdr->e_shnum; shdr++) { + char *shdr_name = shstrtab + shdr->sh_name; + + if (SHT_SYMTAB == shdr->sh_type && 0 == strcmp(".symtab", shdr_name)) { + // get & check associated .strtab section + if (shdr->sh_link >= ehdr->e_shnum) continue; + ElfW(Shdr) *shdr_strtab = shdrs + shdr->sh_link; + if (SHT_STRTAB != shdr_strtab->sh_type) continue; + + // get .symtab & .strtab + ElfW(Sym) *symtab = (ElfW(Sym) *)xdl_read_file_to_heap_by_section(file_fd, file_sz, shdr); + if (NULL == symtab) continue; + char *strtab = (char *)xdl_read_file_to_heap_by_section(file_fd, file_sz, shdr_strtab); + if (NULL == strtab) { + free(symtab); + continue; + } + + // OK + self->symtab = symtab; + self->symtab_cnt = shdr->sh_size / shdr->sh_entsize; + self->strtab = strtab; + self->strtab_sz = shdr_strtab->sh_size; + r = 0; + break; + } else if (SHT_PROGBITS == shdr->sh_type && 0 == strcmp(".gnu_debugdata", shdr_name)) { + if (0 == xdl_symtab_load_from_debugdata(self, file_fd, file_sz, shdr)) { + // OK + r = 0; + break; + } + } + } + +end: + close(file_fd); + if (NULL != shdrs) free(shdrs); + if (NULL != shstrtab) free(shstrtab); + return r; +} + +static xdl_t *xdl_find_from_auxv(unsigned long type, const char *pathname) { + if (NULL == getauxval) return NULL; + + uintptr_t val = (uintptr_t)getauxval(type); + if (0 == val) return NULL; + + // get base + uintptr_t base = (AT_PHDR == type ? (val & (~0xffful)) : val); + if (0 != memcmp((void *)base, ELFMAG, SELFMAG)) return NULL; + + // ELF info + ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)base; + const ElfW(Phdr) *dlpi_phdr = (const ElfW(Phdr) *)(base + ehdr->e_phoff); + ElfW(Half) dlpi_phnum = ehdr->e_phnum; + + // get bias + uintptr_t min_vaddr = UINTPTR_MAX; + for (size_t i = 0; i < dlpi_phnum; i++) { + const ElfW(Phdr) *phdr = &(dlpi_phdr[i]); + if (PT_LOAD == phdr->p_type) { + if (min_vaddr > phdr->p_vaddr) min_vaddr = phdr->p_vaddr; + } + } + if (UINTPTR_MAX == min_vaddr || base < min_vaddr) return NULL; + uintptr_t load_bias = base - min_vaddr; + + // create xDL object + xdl_t *self; + if (NULL == (self = calloc(1, sizeof(xdl_t)))) return NULL; + if (NULL == (self->pathname = strdup(pathname))) { + free(self); + return NULL; + } + self->load_bias = load_bias; + self->dlpi_phdr = dlpi_phdr; + self->dlpi_phnum = dlpi_phnum; + self->dynsym_try_load = false; + self->symtab_try_load = false; + return self; +} + +static int xdl_find_iterate_cb(struct dl_phdr_info *info, size_t size, void *arg) { + (void)size; + + uintptr_t *pkg = (uintptr_t *)arg; + xdl_t **self = (xdl_t **)*pkg++; + const char *filename = (const char *)*pkg; + + // check load_bias + if (0 == info->dlpi_addr || NULL == info->dlpi_name) return 0; + + // check pathname + if ('[' == filename[0]) { + if (0 != strcmp(info->dlpi_name, filename)) return 0; + } else if ('/' == filename[0]) { + if ('/' == info->dlpi_name[0]) { + if (0 != strcmp(info->dlpi_name, filename)) return 0; + } else { + if (!xdl_util_ends_with(filename, info->dlpi_name)) return 0; + } + } else { + if ('/' == info->dlpi_name[0]) { + if (!xdl_util_ends_with(info->dlpi_name, filename)) return 0; + } else { + if (0 != strcmp(info->dlpi_name, filename)) return 0; + } + } + + // found the target ELF + if (NULL == ((*self) = calloc(1, sizeof(xdl_t)))) return 1; // return failed + if (NULL == ((*self)->pathname = strdup(info->dlpi_name))) { + free(*self); + *self = NULL; + return 1; // return failed + } + (*self)->load_bias = info->dlpi_addr; + (*self)->dlpi_phdr = info->dlpi_phdr; + (*self)->dlpi_phnum = info->dlpi_phnum; + (*self)->dynsym_try_load = false; + (*self)->symtab_try_load = false; + return 1; // return OK +} + +static xdl_t *xdl_find(const char *filename) { + // from auxv (linker, vDSO) + xdl_t *self = NULL; + if (xdl_util_ends_with(filename, XDL_UTIL_LINKER_BASENAME)) + self = xdl_find_from_auxv(AT_BASE, XDL_UTIL_LINKER_PATHNAME); + else if (xdl_util_ends_with(filename, XDL_UTIL_VDSO_BASENAME)) + self = xdl_find_from_auxv(AT_SYSINFO_EHDR, XDL_UTIL_VDSO_BASENAME); + + // from auxv (app_process) + const char *basename, *pathname; +#if (defined(__arm__) || defined(__i386__)) && __ANDROID_API__ < __ANDROID_API_L__ + if (xdl_util_get_api_level() < __ANDROID_API_L__) { + basename = XDL_UTIL_APP_PROCESS_BASENAME_K; + pathname = XDL_UTIL_APP_PROCESS_PATHNAME_K; + } else +#endif + { + basename = XDL_UTIL_APP_PROCESS_BASENAME; + pathname = XDL_UTIL_APP_PROCESS_PATHNAME; + } + if (xdl_util_ends_with(filename, basename)) self = xdl_find_from_auxv(AT_PHDR, pathname); + + if (NULL != self) return self; + + // from dl_iterate_phdr + uintptr_t pkg[2] = {(uintptr_t)&self, (uintptr_t)filename}; + xdl_iterate_phdr(xdl_find_iterate_cb, pkg, XDL_DEFAULT); + return self; +} + +static void *xdl_open_always_force(const char *filename) { + // always force dlopen() + void *linker_handle = xdl_linker_load(filename); + if (NULL == linker_handle) return NULL; + + // find + xdl_t *self = xdl_find(filename); + if (NULL == self) + dlclose(linker_handle); + else + self->linker_handle = linker_handle; + + return (void *)self; +} + +static void *xdl_open_try_force(const char *filename) { + // find + xdl_t *self = xdl_find(filename); + if (NULL != self) return (void *)self; + + // try force dlopen() + void *linker_handle = xdl_linker_load(filename); + if (NULL == linker_handle) return NULL; + + // find again + self = xdl_find(filename); + if (NULL == self) + dlclose(linker_handle); + else + self->linker_handle = linker_handle; + + return (void *)self; +} + +void *xdl_open(const char *filename, int flags) { + if (NULL == filename) return NULL; + + if (flags & XDL_ALWAYS_FORCE_LOAD) + return xdl_open_always_force(filename); + else if (flags & XDL_TRY_FORCE_LOAD) + return xdl_open_try_force(filename); + else + return xdl_find(filename); +} + +void *xdl_close(void *handle) { + if (NULL == handle) return NULL; + + xdl_t *self = (xdl_t *)handle; + if (NULL != self->pathname) free(self->pathname); + if (NULL != self->symtab) free(self->symtab); + if (NULL != self->strtab) free(self->strtab); + + void *linker_handle = self->linker_handle; + free(self); + return linker_handle; +} + +static uint32_t xdl_sysv_hash(const uint8_t *name) { + uint32_t h = 0, g; + + while (*name) { + h = (h << 4) + *name++; + g = h & 0xf0000000; + h ^= g; + h ^= g >> 24; + } + return h; +} + +static uint32_t xdl_gnu_hash(const uint8_t *name) { + uint32_t h = 5381; + + while (*name) { + h += (h << 5) + *name++; + } + return h; +} + +static ElfW(Sym) *xdl_dynsym_find_symbol_use_sysv_hash(xdl_t *self, const char *sym_name) { + uint32_t hash = xdl_sysv_hash((const uint8_t *)sym_name); + + for (uint32_t i = self->sysv_hash.buckets[hash % self->sysv_hash.buckets_cnt]; 0 != i; + i = self->sysv_hash.chains[i]) { + ElfW(Sym) *sym = self->dynsym + i; + if (0 != strcmp(self->dynstr + sym->st_name, sym_name)) continue; + return sym; + } + + return NULL; +} + +static ElfW(Sym) *xdl_dynsym_find_symbol_use_gnu_hash(xdl_t *self, const char *sym_name) { + uint32_t hash = xdl_gnu_hash((const uint8_t *)sym_name); + + static uint32_t elfclass_bits = sizeof(ElfW(Addr)) * 8; + size_t word = self->gnu_hash.bloom[(hash / elfclass_bits) % self->gnu_hash.bloom_cnt]; + size_t mask = 0 | (size_t)1 << (hash % elfclass_bits) | + (size_t)1 << ((hash >> self->gnu_hash.bloom_shift) % elfclass_bits); + + // if at least one bit is not set, this symbol is surely missing + if ((word & mask) != mask) return NULL; + + // ignore STN_UNDEF + uint32_t i = self->gnu_hash.buckets[hash % self->gnu_hash.buckets_cnt]; + if (i < self->gnu_hash.symoffset) return NULL; + + // loop through the chain + while (1) { + ElfW(Sym) *sym = self->dynsym + i; + uint32_t sym_hash = self->gnu_hash.chains[i - self->gnu_hash.symoffset]; + + if ((hash | (uint32_t)1) == (sym_hash | (uint32_t)1)) { + if (0 == strcmp(self->dynstr + sym->st_name, sym_name)) { + return sym; + } + } + + // chain ends with an element with the lowest bit set to 1 + if (sym_hash & (uint32_t)1) break; + + i++; + } + + return NULL; +} + +void *xdl_sym(void *handle, const char *symbol, size_t *symbol_size) { + if (NULL == handle || NULL == symbol) return NULL; + if (NULL != symbol_size) *symbol_size = 0; + + xdl_t *self = (xdl_t *)handle; + + // load .dynsym only once + if (!self->dynsym_try_load) { + self->dynsym_try_load = true; + if (0 != xdl_dynsym_load(self)) return NULL; + } + + // find symbol + if (NULL == self->dynsym) return NULL; + ElfW(Sym) *sym = NULL; + if (self->gnu_hash.buckets_cnt > 0) { + // use GNU hash (.gnu.hash -> .dynsym -> .dynstr), O(x) + O(1) + O(1) + sym = xdl_dynsym_find_symbol_use_gnu_hash(self, symbol); + } + if (NULL == sym && self->sysv_hash.buckets_cnt > 0) { + // use SYSV hash (.hash -> .dynsym -> .dynstr), O(x) + O(1) + O(1) + sym = xdl_dynsym_find_symbol_use_sysv_hash(self, symbol); + } + if (NULL == sym || !XDL_DYNSYM_IS_EXPORT_SYM(sym->st_shndx)) return NULL; + + if (NULL != symbol_size) *symbol_size = sym->st_size; + return (void *)(self->load_bias + sym->st_value); +} + +void *xdl_dsym(void *handle, const char *symbol, size_t *symbol_size) { + if (NULL == handle || NULL == symbol) return NULL; + if (NULL != symbol_size) *symbol_size = 0; + + xdl_t *self = (xdl_t *)handle; + + // load .symtab only once + if (!self->symtab_try_load) { + self->symtab_try_load = true; + if (0 != xdl_symtab_load(self)) return NULL; + } + + // find symbol + if (NULL == self->symtab) return NULL; + for (size_t i = 0; i < self->symtab_cnt; i++) { + ElfW(Sym) *sym = self->symtab + i; + + if (!XDL_SYMTAB_IS_EXPORT_SYM(sym->st_shndx)) continue; + if (0 != strncmp(self->strtab + sym->st_name, symbol, self->strtab_sz - sym->st_name)) continue; + + if (NULL != symbol_size) *symbol_size = sym->st_size; + return (void *)(self->load_bias + sym->st_value); + } + + return NULL; +} + +static bool xdl_elf_is_match(uintptr_t load_bias, const ElfW(Phdr) *dlpi_phdr, ElfW(Half) dlpi_phnum, + uintptr_t addr) { + if (addr < load_bias) return false; + + uintptr_t vaddr = addr - load_bias; + for (size_t i = 0; i < dlpi_phnum; i++) { + const ElfW(Phdr) *phdr = &(dlpi_phdr[i]); + if (PT_LOAD != phdr->p_type) continue; + + if (phdr->p_vaddr <= vaddr && vaddr < phdr->p_vaddr + phdr->p_memsz) return true; + } + + return false; +} + +static int xdl_open_by_addr_iterate_cb(struct dl_phdr_info *info, size_t size, void *arg) { + (void)size; + + uintptr_t *pkg = (uintptr_t *)arg; + xdl_t **self = (xdl_t **)*pkg++; + uintptr_t addr = *pkg; + + if (xdl_elf_is_match(info->dlpi_addr, info->dlpi_phdr, info->dlpi_phnum, addr)) { + // found the target ELF + if (NULL == ((*self) = calloc(1, sizeof(xdl_t)))) return 1; // failed + if (NULL == ((*self)->pathname = strdup(info->dlpi_name))) { + free(*self); + *self = NULL; + return 1; // failed + } + (*self)->load_bias = info->dlpi_addr; + (*self)->dlpi_phdr = info->dlpi_phdr; + (*self)->dlpi_phnum = info->dlpi_phnum; + (*self)->dynsym_try_load = false; + (*self)->symtab_try_load = false; + return 1; // OK + } + + return 0; // mismatch +} + +static void *xdl_open_by_addr(void *addr) { + if (NULL == addr) return NULL; + + xdl_t *self = NULL; + uintptr_t pkg[2] = {(uintptr_t)&self, (uintptr_t)addr}; + xdl_iterate_phdr(xdl_open_by_addr_iterate_cb, pkg, XDL_DEFAULT); + + return (void *)self; +} + +static bool xdl_sym_is_match(ElfW(Sym) *sym, uintptr_t offset, bool is_symtab) { + if (is_symtab) { + if (!XDL_SYMTAB_IS_EXPORT_SYM(sym->st_shndx)) false; + } else { + if (!XDL_DYNSYM_IS_EXPORT_SYM(sym->st_shndx)) false; + } + + return ELF_ST_TYPE(sym->st_info) != STT_TLS && offset >= sym->st_value && + offset < sym->st_value + sym->st_size; +} + +static ElfW(Sym) *xdl_sym_by_addr(void *handle, void *addr) { + xdl_t *self = (xdl_t *)handle; + + // load .dynsym only once + if (!self->dynsym_try_load) { + self->dynsym_try_load = true; + if (0 != xdl_dynsym_load(self)) return NULL; + } + + // find symbol + if (NULL == self->dynsym) return NULL; + uintptr_t offset = (uintptr_t)addr - self->load_bias; + if (self->gnu_hash.buckets_cnt > 0) { + const uint32_t *chains_all = self->gnu_hash.chains - self->gnu_hash.symoffset; + for (size_t i = 0; i < self->gnu_hash.buckets_cnt; i++) { + uint32_t n = self->gnu_hash.buckets[i]; + if (n < self->gnu_hash.symoffset) continue; + do { + ElfW(Sym) *sym = self->dynsym + n; + if (xdl_sym_is_match(sym, offset, false)) return sym; + } while ((chains_all[n++] & 1) == 0); + } + } else if (self->sysv_hash.chains_cnt > 0) { + for (size_t i = 0; i < self->sysv_hash.chains_cnt; i++) { + ElfW(Sym) *sym = self->dynsym + i; + if (xdl_sym_is_match(sym, offset, false)) return sym; + } + } + + return NULL; +} + +static ElfW(Sym) *xdl_dsym_by_addr(void *handle, void *addr) { + xdl_t *self = (xdl_t *)handle; + + // load .symtab only once + if (!self->symtab_try_load) { + self->symtab_try_load = true; + if (0 != xdl_symtab_load(self)) return NULL; + } + + // find symbol + if (NULL == self->symtab) return NULL; + uintptr_t offset = (uintptr_t)addr - self->load_bias; + for (size_t i = 0; i < self->symtab_cnt; i++) { + ElfW(Sym) *sym = self->symtab + i; + if (xdl_sym_is_match(sym, offset, true)) return sym; + } + + return NULL; +} + +int xdl_addr(void *addr, xdl_info_t *info, void **cache) { + if (NULL == addr || NULL == info || NULL == cache) return 0; + + memset(info, 0, sizeof(Dl_info)); + + // find handle from cache + xdl_t *handle = NULL; + for (handle = *((xdl_t **)cache); NULL != handle; handle = handle->next) + if (xdl_elf_is_match(handle->load_bias, handle->dlpi_phdr, handle->dlpi_phnum, (uintptr_t)addr)) break; + + // create new handle, save handle to cache + if (NULL == handle) { + handle = (xdl_t *)xdl_open_by_addr(addr); + if (NULL == handle) return 0; + handle->next = *(xdl_t **)cache; + *(xdl_t **)cache = handle; + } + + // we have at least: load_bias, pathname, dlpi_phdr, dlpi_phnum + info->dli_fbase = (void *)handle->load_bias; + info->dli_fname = handle->pathname; + info->dli_sname = NULL; + info->dli_saddr = 0; + info->dli_ssize = 0; + info->dlpi_phdr = handle->dlpi_phdr; + info->dlpi_phnum = (size_t)handle->dlpi_phnum; + + // keep looking for: symbol name, symbol offset, symbol size + ElfW(Sym) *sym; + if (NULL != (sym = xdl_sym_by_addr((void *)handle, addr))) { + info->dli_sname = handle->dynstr + sym->st_name; + info->dli_saddr = (void *)(handle->load_bias + sym->st_value); + info->dli_ssize = sym->st_size; + } else if (NULL != (sym = xdl_dsym_by_addr((void *)handle, addr))) { + info->dli_sname = handle->strtab + sym->st_name; + info->dli_saddr = (void *)(handle->load_bias + sym->st_value); + info->dli_ssize = sym->st_size; + } + + return 1; +} + +void xdl_addr_clean(void **cache) { + if (NULL == cache) return; + + xdl_t *handle = *((xdl_t **)cache); + while (NULL != handle) { + xdl_t *tmp = handle; + handle = handle->next; + xdl_close(tmp); + } + *cache = NULL; +} + +int xdl_iterate_phdr(int (*callback)(struct dl_phdr_info *, size_t, void *), void *data, int flags) { + if (NULL == callback) return 0; + + return xdl_iterate_phdr_impl(callback, data, flags); +} + +int xdl_info(void *handle, int request, void *info) { + if (NULL == handle || XDL_DI_DLINFO != request || NULL == info) return -1; + + xdl_t *self = (xdl_t *)handle; + xdl_info_t *dlinfo = (xdl_info_t *)info; + + dlinfo->dli_fbase = (void *)self->load_bias; + dlinfo->dli_fname = self->pathname; + dlinfo->dli_sname = NULL; + dlinfo->dli_saddr = 0; + dlinfo->dli_ssize = 0; + dlinfo->dlpi_phdr = self->dlpi_phdr; + dlinfo->dlpi_phnum = (size_t)self->dlpi_phnum; + return 0; +} diff --git a/module/src/main/cpp/xdl/xdl_iterate.c b/module/src/main/cpp/xdl/xdl_iterate.c new file mode 100644 index 0000000..01f4145 --- /dev/null +++ b/module/src/main/cpp/xdl/xdl_iterate.c @@ -0,0 +1,297 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2020-10-04. + +#include "xdl_iterate.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xdl.h" +#include "xdl_linker.h" +#include "xdl_util.h" + +/* + * ========================================================================================================= + * API-LEVEL ANDROID-VERSION SOLUTION + * ========================================================================================================= + * 16 4.1 /proc/self/maps + * 17 4.2 /proc/self/maps + * 18 4.3 /proc/self/maps + * 19 4.4 /proc/self/maps + * 20 4.4W /proc/self/maps + * --------------------------------------------------------------------------------------------------------- + * 21 5.0 dl_iterate_phdr() + __dl__ZL10g_dl_mutex + linker/linker64 from getauxval(3) + * 22 5.1 dl_iterate_phdr() + __dl__ZL10g_dl_mutex + linker/linker64 from getauxval(3) + * --------------------------------------------------------------------------------------------------------- + * 23 >= 6.0 dl_iterate_phdr() + linker/linker64 from getauxval(3) + * ========================================================================================================= + */ + +extern __attribute((weak)) int dl_iterate_phdr(int (*)(struct dl_phdr_info *, size_t, void *), void *); +extern __attribute((weak)) unsigned long int getauxval(unsigned long int); + +static uintptr_t xdl_iterate_get_min_vaddr(struct dl_phdr_info *info) { + uintptr_t min_vaddr = UINTPTR_MAX; + for (size_t i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *phdr = &(info->dlpi_phdr[i]); + if (PT_LOAD == phdr->p_type) { + if (min_vaddr > phdr->p_vaddr) min_vaddr = phdr->p_vaddr; + } + } + return min_vaddr; +} + +static int xdl_iterate_open_or_rewind_maps(FILE **maps) { + if (NULL == *maps) { + *maps = fopen("/proc/self/maps", "r"); + if (NULL == *maps) return -1; + } else + rewind(*maps); + + return 0; +} + +static int xdl_iterate_get_pathname_from_maps(uintptr_t base, char *buf, size_t buf_len, FILE **maps) { + // open or rewind maps-file + if (0 != xdl_iterate_open_or_rewind_maps(maps)) return -1; // failed + + char line[1024]; + while (fgets(line, sizeof(line), *maps)) { + // check base address + uintptr_t start, end; + if (2 != sscanf(line, "%" SCNxPTR "-%" SCNxPTR " r", &start, &end)) continue; + if (base < start) break; // failed + if (base >= end) continue; + + // get pathname + char *pathname = strchr(line, '/'); + if (NULL == pathname) break; // failed + xdl_util_trim_ending(pathname); + + // found it + strlcpy(buf, pathname, buf_len); + return 0; // OK + } + + return -1; // failed +} + +static int xdl_iterate_by_linker_cb(struct dl_phdr_info *info, size_t size, void *arg) { + uintptr_t *pkg = (uintptr_t *)arg; + xdl_iterate_phdr_cb_t cb = (xdl_iterate_phdr_cb_t)*pkg++; + void *cb_arg = (void *)*pkg++; + FILE **maps = (FILE **)*pkg++; + uintptr_t linker_load_bias = *pkg++; + int flags = (int)*pkg; + + // ignore invalid ELF + if (0 == info->dlpi_addr || NULL == info->dlpi_name || '\0' == info->dlpi_name[0]) return 0; + + // ignore linker if we have returned it already + if (linker_load_bias == info->dlpi_addr) return 0; + + struct dl_phdr_info info_fixed; + info_fixed.dlpi_addr = info->dlpi_addr; + info_fixed.dlpi_name = info->dlpi_name; + info_fixed.dlpi_phdr = info->dlpi_phdr; + info_fixed.dlpi_phnum = info->dlpi_phnum; + info = &info_fixed; + + // fix dlpi_phdr & dlpi_phnum (from memory) + if (NULL == info->dlpi_phdr || 0 == info->dlpi_phnum) { + ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)info->dlpi_addr; + info->dlpi_phdr = (ElfW(Phdr) *)(info->dlpi_addr + ehdr->e_phoff); + info->dlpi_phnum = ehdr->e_phnum; + } + + // fix dlpi_name (from /proc/self/maps) + if ('/' != info->dlpi_name[0] && '[' != info->dlpi_name[0] && (0 != (flags & XDL_FULL_PATHNAME))) { + // get base address + uintptr_t min_vaddr = xdl_iterate_get_min_vaddr(info); + if (UINTPTR_MAX == min_vaddr) return 0; // ignore this ELF + uintptr_t base = (uintptr_t)(info->dlpi_addr + min_vaddr); + + char buf[1024]; + if (0 != xdl_iterate_get_pathname_from_maps(base, buf, sizeof(buf), maps)) return 0; // ignore this ELF + + info->dlpi_name = (const char *)buf; + } + + // callback + return cb(info, size, cb_arg); +} + +static uintptr_t xdl_iterate_get_linker_base(void) { + if (NULL == getauxval) return 0; + + uintptr_t base = (uintptr_t)getauxval(AT_BASE); + if (0 == base) return 0; + if (0 != memcmp((void *)base, ELFMAG, SELFMAG)) return 0; + + return base; +} + +static int xdl_iterate_do_callback(xdl_iterate_phdr_cb_t cb, void *cb_arg, uintptr_t base, + const char *pathname, uintptr_t *load_bias) { + ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)base; + + struct dl_phdr_info info; + info.dlpi_name = pathname; + info.dlpi_phdr = (const ElfW(Phdr) *)(base + ehdr->e_phoff); + info.dlpi_phnum = ehdr->e_phnum; + + // get load bias + uintptr_t min_vaddr = xdl_iterate_get_min_vaddr(&info); + if (UINTPTR_MAX == min_vaddr) return 0; // ignore invalid ELF + info.dlpi_addr = (ElfW(Addr))(base - min_vaddr); + if (NULL != load_bias) *load_bias = info.dlpi_addr; + + return cb(&info, sizeof(struct dl_phdr_info), cb_arg); +} + +static int xdl_iterate_by_linker(xdl_iterate_phdr_cb_t cb, void *cb_arg, int flags) { + if (NULL == dl_iterate_phdr) return 0; + + int api_level = xdl_util_get_api_level(); + FILE *maps = NULL; + int r; + + // dl_iterate_phdr(3) does NOT contain linker/linker64 when Android version < 8.1 (API level 27). + // Here we always try to get linker base address from auxv. + uintptr_t linker_load_bias = 0; + uintptr_t linker_base = xdl_iterate_get_linker_base(); + if (0 != linker_base) { + if (0 != + (r = xdl_iterate_do_callback(cb, cb_arg, linker_base, XDL_UTIL_LINKER_PATHNAME, &linker_load_bias))) + return r; + } + + // for other ELF + uintptr_t pkg[5] = {(uintptr_t)cb, (uintptr_t)cb_arg, (uintptr_t)&maps, linker_load_bias, (uintptr_t)flags}; + if (__ANDROID_API_L__ == api_level || __ANDROID_API_L_MR1__ == api_level) xdl_linker_lock(); + r = dl_iterate_phdr(xdl_iterate_by_linker_cb, pkg); + if (__ANDROID_API_L__ == api_level || __ANDROID_API_L_MR1__ == api_level) xdl_linker_unlock(); + + if (NULL != maps) fclose(maps); + return r; +} + +#if (defined(__arm__) || defined(__i386__)) && __ANDROID_API__ < __ANDROID_API_L__ +static int xdl_iterate_by_maps(xdl_iterate_phdr_cb_t cb, void *cb_arg) { + FILE *maps = fopen("/proc/self/maps", "r"); + if (NULL == maps) return 0; + + int r = 0; + char buf1[1024], buf2[1024]; + char *line = buf1; + uintptr_t prev_base = 0; + bool try_next_line = false; + + while (fgets(line, sizeof(buf1), maps)) { + // Try to find an ELF which loaded by linker. + uintptr_t base, offset; + char exec; + if (3 != sscanf(line, "%" SCNxPTR "-%*" SCNxPTR " r%*c%cp %" SCNxPTR " ", &base, &exec, &offset)) goto clean; + + if ('-' == exec && 0 == offset) { + // r--p + prev_base = base; + line = (line == buf1 ? buf2 : buf1); + try_next_line = true; + continue; + } + else if (exec == 'x') { + // r-xp + char *pathname = NULL; + if (try_next_line && 0 != offset) { + char *prev = (line == buf1 ? buf2 : buf1); + char *prev_pathname = strchr(prev, '/'); + if (NULL == prev_pathname) goto clean; + + pathname = strchr(line, '/'); + if (NULL == pathname) goto clean; + + xdl_util_trim_ending(prev_pathname); + xdl_util_trim_ending(pathname); + if (0 != strcmp(prev_pathname, pathname)) goto clean; + + // we found the line with r-xp in the next line + base = prev_base; + offset = 0; + } + + if (0 != offset) goto clean; + + // get pathname + if (NULL == pathname) { + pathname = strchr(line, '/'); + if (NULL == pathname) goto clean; + xdl_util_trim_ending(pathname); + } + + if (0 != memcmp((void *)base, ELFMAG, SELFMAG)) goto clean; + ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)base; + struct dl_phdr_info info; + info.dlpi_name = pathname; + info.dlpi_phdr = (const ElfW(Phdr) *)(base + ehdr->e_phoff); + info.dlpi_phnum = ehdr->e_phnum; + + // callback + if (0 != (r = xdl_iterate_do_callback(cb, cb_arg, base, pathname, NULL))) break; + } + + clean: + try_next_line = false; + } + + fclose(maps); + return r; +} +#endif + +int xdl_iterate_phdr_impl(xdl_iterate_phdr_cb_t cb, void *cb_arg, int flags) { + // iterate by /proc/self/maps in Android 4.x (Android 4.x only supports arm32 and x86) +#if (defined(__arm__) || defined(__i386__)) && __ANDROID_API__ < __ANDROID_API_L__ + if (xdl_util_get_api_level() < __ANDROID_API_L__) return xdl_iterate_by_maps(cb, cb_arg); +#endif + + // iterate by dl_iterate_phdr() + return xdl_iterate_by_linker(cb, cb_arg, flags); +} + +int xdl_iterate_get_full_pathname(uintptr_t base, char *buf, size_t buf_len) { + FILE *maps = NULL; + int r = xdl_iterate_get_pathname_from_maps(base, buf, buf_len, &maps); + if (NULL != maps) fclose(maps); + return r; +} diff --git a/module/src/main/cpp/xdl/xdl_iterate.h b/module/src/main/cpp/xdl/xdl_iterate.h new file mode 100644 index 0000000..81b6188 --- /dev/null +++ b/module/src/main/cpp/xdl/xdl_iterate.h @@ -0,0 +1,43 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2020-10-04. + +#ifndef IO_HEXHACKING_XDL_ITERATE +#define IO_HEXHACKING_XDL_ITERATE + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef int (*xdl_iterate_phdr_cb_t)(struct dl_phdr_info *info, size_t size, void *arg); +int xdl_iterate_phdr_impl(xdl_iterate_phdr_cb_t cb, void *cb_arg, int flags); + +int xdl_iterate_get_full_pathname(uintptr_t base, char *buf, size_t buf_len); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/module/src/main/cpp/xdl/xdl_linker.c b/module/src/main/cpp/xdl/xdl_linker.c new file mode 100644 index 0000000..c29b950 --- /dev/null +++ b/module/src/main/cpp/xdl/xdl_linker.c @@ -0,0 +1,187 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2021-02-21. + +#include "xdl_linker.h" + +#include +#include +#include +#include + +#include "xdl.h" +#include "xdl_iterate.h" +#include "xdl_util.h" + +#define XDL_LINKER_SYM_MUTEX "__dl__ZL10g_dl_mutex" +#define XDL_LINKER_SYM_DLOPEN_EXT_N "__dl__ZL10dlopen_extPKciPK17android_dlextinfoPv" +#define XDL_LINKER_SYM_DO_DLOPEN_N "__dl__Z9do_dlopenPKciPK17android_dlextinfoPv" +#define XDL_LINKER_SYM_DLOPEN_O "__dl__Z8__dlopenPKciPKv" +#define XDL_LINKER_SYM_LOADER_DLOPEN_P "__loader_dlopen" + +typedef void *(*xdl_linker_dlopen_n_t)(const char *, int, const void *, void *); +typedef void *(*xdl_linker_dlopen_o_t)(const char *, int, const void *); + +static pthread_mutex_t *xdl_linker_mutex = NULL; +static void *xdl_linker_dlopen = NULL; + +static void *xdl_linker_caller_addr[] = { + NULL, // default + NULL, // art + NULL // vendor +}; + +#ifndef __LP64__ +#define XDL_LINKER_LIB "lib" +#else +#define XDL_LINKER_LIB "lib64" +#endif +static const char *xdl_linker_vendor_path[] = { + // order is important + "/vendor/" XDL_LINKER_LIB "/egl/", "/vendor/" XDL_LINKER_LIB "/hw/", + "/vendor/" XDL_LINKER_LIB "/", "/odm/" XDL_LINKER_LIB "/", + "/vendor/" XDL_LINKER_LIB "/vndk-sp/", "/odm/" XDL_LINKER_LIB "/vndk-sp/"}; + +static void xdl_linker_init(void) { + static bool inited = false; + if (inited) return; + inited = true; + + void *handle = xdl_open(XDL_UTIL_LINKER_BASENAME, XDL_DEFAULT); + if (NULL == handle) return; + + int api_level = xdl_util_get_api_level(); + if (__ANDROID_API_L__ == api_level || __ANDROID_API_L_MR1__ == api_level) { + // == Android 5.x + xdl_linker_mutex = (pthread_mutex_t *)xdl_dsym(handle, XDL_LINKER_SYM_MUTEX, NULL); + } else if (__ANDROID_API_N__ == api_level || __ANDROID_API_N_MR1__ == api_level) { + // == Android 7.x + xdl_linker_dlopen = xdl_dsym(handle, XDL_LINKER_SYM_DLOPEN_EXT_N, NULL); + if (NULL == xdl_linker_dlopen) { + xdl_linker_dlopen = xdl_dsym(handle, XDL_LINKER_SYM_DO_DLOPEN_N, NULL); + xdl_linker_mutex = (pthread_mutex_t *)xdl_dsym(handle, XDL_LINKER_SYM_MUTEX, NULL); + } + } else if (__ANDROID_API_O__ == api_level || __ANDROID_API_O_MR1__ == api_level) { + // == Android 8.x + xdl_linker_dlopen = xdl_dsym(handle, XDL_LINKER_SYM_DLOPEN_O, NULL); + } else if (api_level >= __ANDROID_API_P__) { + // >= Android 9.0 + xdl_linker_dlopen = xdl_sym(handle, XDL_LINKER_SYM_LOADER_DLOPEN_P, NULL); + } + + xdl_close(handle); +} + +void xdl_linker_lock(void) { + xdl_linker_init(); + + if (NULL != xdl_linker_mutex) pthread_mutex_lock(xdl_linker_mutex); +} + +void xdl_linker_unlock(void) { + if (NULL != xdl_linker_mutex) pthread_mutex_unlock(xdl_linker_mutex); +} + +static void *xdl_linker_get_caller_addr(struct dl_phdr_info *info) { + for (size_t i = 0; i < info->dlpi_phnum; i++) { + const ElfW(Phdr) *phdr = &(info->dlpi_phdr[i]); + if (PT_LOAD == phdr->p_type) { + return (void *)(info->dlpi_addr + phdr->p_vaddr); + } + } + return NULL; +} + +static int xdl_linker_get_caller_addr_cb(struct dl_phdr_info *info, size_t size, void *arg) { + (void)size; + + size_t *vendor_match = (size_t *)arg; + + if (0 == info->dlpi_addr || NULL == info->dlpi_name) return 0; // continue + + if (NULL == xdl_linker_caller_addr[0] && xdl_util_ends_with(info->dlpi_name, "/libc.so")) + xdl_linker_caller_addr[0] = xdl_linker_get_caller_addr(info); + + if (NULL == xdl_linker_caller_addr[1] && xdl_util_ends_with(info->dlpi_name, "/libart.so")) + xdl_linker_caller_addr[1] = xdl_linker_get_caller_addr(info); + + if (0 != *vendor_match) { + for (size_t i = 0; i < *vendor_match; i++) { + if (xdl_util_starts_with(info->dlpi_name, xdl_linker_vendor_path[i])) { + void *caller_addr = xdl_linker_get_caller_addr(info); + if (NULL != caller_addr) { + xdl_linker_caller_addr[2] = caller_addr; + *vendor_match = i; + } + } + } + } + + if (NULL != xdl_linker_caller_addr[0] && NULL != xdl_linker_caller_addr[1] && 0 == *vendor_match) { + return 1; // finish + } else { + return 0; // continue + } +} + +static void xdl_linker_load_caller_addr(void) { + if (NULL == xdl_linker_caller_addr[0]) { + size_t vendor_match = sizeof(xdl_linker_vendor_path) / sizeof(xdl_linker_vendor_path[0]); + xdl_iterate_phdr_impl(xdl_linker_get_caller_addr_cb, &vendor_match, XDL_DEFAULT); + } +} + +void *xdl_linker_load(const char *filename) { + int api_level = xdl_util_get_api_level(); + + if (api_level <= __ANDROID_API_M__) { + // <= Android 6.0 + return dlopen(filename, RTLD_NOW); + } else { + xdl_linker_init(); + if (NULL == xdl_linker_dlopen) return NULL; + xdl_linker_load_caller_addr(); + + void *handle = NULL; + if (__ANDROID_API_N__ == api_level || __ANDROID_API_N_MR1__ == api_level) { + // == Android 7.x + xdl_linker_lock(); + for (size_t i = 0; i < sizeof(xdl_linker_caller_addr) / sizeof(xdl_linker_caller_addr[0]); i++) { + if (NULL != xdl_linker_caller_addr[i]) { + handle = + ((xdl_linker_dlopen_n_t)xdl_linker_dlopen)(filename, RTLD_NOW, NULL, xdl_linker_caller_addr[i]); + if (NULL != handle) break; + } + } + xdl_linker_unlock(); + } else { + // >= Android 8.0 + for (size_t i = 0; i < sizeof(xdl_linker_caller_addr) / sizeof(xdl_linker_caller_addr[0]); i++) { + if (NULL != xdl_linker_caller_addr[i]) { + handle = ((xdl_linker_dlopen_o_t)xdl_linker_dlopen)(filename, RTLD_NOW, xdl_linker_caller_addr[i]); + if (NULL != handle) break; + } + } + } + return handle; + } +} diff --git a/module/src/main/cpp/xdl/xdl_linker.h b/module/src/main/cpp/xdl/xdl_linker.h new file mode 100644 index 0000000..4067de4 --- /dev/null +++ b/module/src/main/cpp/xdl/xdl_linker.h @@ -0,0 +1,40 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2021-02-21. + +#ifndef IO_HEXHACKING_XDL_LINKER +#define IO_HEXHACKING_XDL_LINKER + +#ifdef __cplusplus +extern "C" { +#endif + +void xdl_linker_lock(void); +void xdl_linker_unlock(void); + +void *xdl_linker_load(const char *filename); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/module/src/main/cpp/xdl/xdl_lzma.c b/module/src/main/cpp/xdl/xdl_lzma.c new file mode 100644 index 0000000..14b041e --- /dev/null +++ b/module/src/main/cpp/xdl/xdl_lzma.c @@ -0,0 +1,181 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2020-11-08. + +#include "xdl_lzma.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "xdl.h" +#include "xdl_util.h" + +// LZMA library pathname & symbol names +#ifndef __LP64__ +#define XDL_LZMA_PATHNAME "/system/lib/liblzma.so" +#else +#define XDL_LZMA_PATHNAME "/system/lib64/liblzma.so" +#endif +#define XDL_LZMA_SYM_CRCGEN "CrcGenerateTable" +#define XDL_LZMA_SYM_CRC64GEN "Crc64GenerateTable" +#define XDL_LZMA_SYM_CONSTRUCT "XzUnpacker_Construct" +#define XDL_LZMA_SYM_ISFINISHED "XzUnpacker_IsStreamWasFinished" +#define XDL_LZMA_SYM_FREE "XzUnpacker_Free" +#define XDL_LZMA_SYM_CODE "XzUnpacker_Code" + +// LZMA data type definition +#define SZ_OK 0 +typedef struct ISzAlloc ISzAlloc; +typedef const ISzAlloc *ISzAllocPtr; +struct ISzAlloc { + void *(*Alloc)(ISzAllocPtr p, size_t size); + void (*Free)(ISzAllocPtr p, void *address); /* address can be 0 */ +}; +typedef enum { + CODER_STATUS_NOT_SPECIFIED, /* use main error code instead */ + CODER_STATUS_FINISHED_WITH_MARK, /* stream was finished with end mark. */ + CODER_STATUS_NOT_FINISHED, /* stream was not finished */ + CODER_STATUS_NEEDS_MORE_INPUT /* you must provide more input bytes */ +} ECoderStatus; +typedef enum { + CODER_FINISH_ANY, /* finish at any point */ + CODER_FINISH_END /* block must be finished at the end */ +} ECoderFinishMode; + +// LZMA function type definition +typedef void (*xdl_lzma_crcgen_t)(void); +typedef void (*xdl_lzma_crc64gen_t)(void); +typedef void (*xdl_lzma_construct_t)(void *, ISzAllocPtr); +typedef int (*xdl_lzma_isfinished_t)(const void *); +typedef void (*xdl_lzma_free_t)(void *); +typedef int (*xdl_lzma_code_t)(void *, uint8_t *, size_t *, const uint8_t *, size_t *, ECoderFinishMode, + ECoderStatus *); +typedef int (*xdl_lzma_code_q_t)(void *, uint8_t *, size_t *, const uint8_t *, size_t *, int, + ECoderFinishMode, ECoderStatus *); + +// LZMA function pointor +static xdl_lzma_construct_t xdl_lzma_construct = NULL; +static xdl_lzma_isfinished_t xdl_lzma_isfinished = NULL; +static xdl_lzma_free_t xdl_lzma_free = NULL; +static void *xdl_lzma_code = NULL; + +// LZMA init +static void xdl_lzma_init() { + void *lzma = xdl_open(XDL_LZMA_PATHNAME, XDL_TRY_FORCE_LOAD); + if (NULL == lzma) return; + + xdl_lzma_crcgen_t crcgen = NULL; + xdl_lzma_crc64gen_t crc64gen = NULL; + if (NULL == (crcgen = (xdl_lzma_crcgen_t)xdl_sym(lzma, XDL_LZMA_SYM_CRCGEN, NULL))) goto end; + if (NULL == (crc64gen = (xdl_lzma_crc64gen_t)xdl_sym(lzma, XDL_LZMA_SYM_CRC64GEN, NULL))) goto end; + if (NULL == (xdl_lzma_construct = (xdl_lzma_construct_t)xdl_sym(lzma, XDL_LZMA_SYM_CONSTRUCT, NULL))) + goto end; + if (NULL == (xdl_lzma_isfinished = (xdl_lzma_isfinished_t)xdl_sym(lzma, XDL_LZMA_SYM_ISFINISHED, NULL))) + goto end; + if (NULL == (xdl_lzma_free = (xdl_lzma_free_t)xdl_sym(lzma, XDL_LZMA_SYM_FREE, NULL))) goto end; + if (NULL == (xdl_lzma_code = xdl_sym(lzma, XDL_LZMA_SYM_CODE, NULL))) goto end; + crcgen(); + crc64gen(); + +end: + xdl_close(lzma); +} + +// LZMA internal alloc / free +static void *xdl_lzma_internal_alloc(ISzAllocPtr p, size_t size) { + (void)p; + return malloc(size); +} +static void xdl_lzma_internal_free(ISzAllocPtr p, void *address) { + (void)p; + free(address); +} + +int xdl_lzma_decompress(uint8_t *src, size_t src_size, uint8_t **dst, size_t *dst_size) { + size_t src_offset = 0; + size_t dst_offset = 0; + size_t src_remaining; + size_t dst_remaining; + ISzAlloc alloc = {.Alloc = xdl_lzma_internal_alloc, .Free = xdl_lzma_internal_free}; + long long state[4096 / sizeof(long long)]; // must be enough, 8-bit aligned + ECoderStatus status; + int api_level = xdl_util_get_api_level(); + + // init and check + static bool inited = false; + if (!inited) { + xdl_lzma_init(); + inited = true; + } + if (NULL == xdl_lzma_code) return -1; + + xdl_lzma_construct(&state, &alloc); + + *dst_size = 2 * src_size; + *dst = NULL; + do { + *dst_size *= 2; + if (NULL == (*dst = realloc(*dst, *dst_size))) { + xdl_lzma_free(&state); + return -1; + } + + src_remaining = src_size - src_offset; + dst_remaining = *dst_size - dst_offset; + + int result; + if (api_level >= __ANDROID_API_Q__) { + xdl_lzma_code_q_t lzma_code_q = (xdl_lzma_code_q_t)xdl_lzma_code; + result = lzma_code_q(&state, *dst + dst_offset, &dst_remaining, src + src_offset, &src_remaining, 1, + CODER_FINISH_ANY, &status); + } else { + xdl_lzma_code_t lzma_code = (xdl_lzma_code_t)xdl_lzma_code; + result = lzma_code(&state, *dst + dst_offset, &dst_remaining, src + src_offset, &src_remaining, + CODER_FINISH_ANY, &status); + } + if (SZ_OK != result) { + free(*dst); + xdl_lzma_free(&state); + return -1; + } + + src_offset += src_remaining; + dst_offset += dst_remaining; + } while (status == CODER_STATUS_NOT_FINISHED); + + xdl_lzma_free(&state); + + if (!xdl_lzma_isfinished(&state)) { + free(*dst); + return -1; + } + + *dst_size = dst_offset; + *dst = realloc(*dst, *dst_size); + return 0; +} diff --git a/module/src/main/cpp/xdl/xdl_lzma.h b/module/src/main/cpp/xdl/xdl_lzma.h new file mode 100644 index 0000000..bd12ac0 --- /dev/null +++ b/module/src/main/cpp/xdl/xdl_lzma.h @@ -0,0 +1,40 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2020-11-08. + +#ifndef IO_HEXHACKING_XDL_LZMA +#define IO_HEXHACKING_XDL_LZMA + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int xdl_lzma_decompress(uint8_t *src, size_t src_size, uint8_t **dst, size_t *dst_size); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/module/src/main/cpp/xdl/xdl_util.c b/module/src/main/cpp/xdl/xdl_util.c new file mode 100644 index 0000000..1405a2e --- /dev/null +++ b/module/src/main/cpp/xdl/xdl_util.c @@ -0,0 +1,95 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2020-10-04. + +#include "xdl_util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool xdl_util_starts_with(const char *str, const char *start) { + while (*str && *str == *start) { + str++; + start++; + } + + return '\0' == *start; +} + +bool xdl_util_ends_with(const char *str, const char *ending) { + size_t str_len = strlen(str); + size_t ending_len = strlen(ending); + + if (ending_len > str_len) return false; + + return 0 == strcmp(str + (str_len - ending_len), ending); +} + +size_t xdl_util_trim_ending(char *start) { + char *end = start + strlen(start); + while (start < end && isspace((int)(*(end - 1)))) { + end--; + *end = '\0'; + } + return (size_t)(end - start); +} + +static int xdl_util_get_api_level_from_build_prop(void) { + char buf[128]; + int api_level = -1; + + FILE *fp = fopen("/system/build.prop", "r"); + if (NULL == fp) goto end; + + while (fgets(buf, sizeof(buf), fp)) { + if (xdl_util_starts_with(buf, "ro.build.version.sdk=")) { + api_level = atoi(buf + 21); + break; + } + } + fclose(fp); + +end: + return (api_level > 0) ? api_level : -1; +} + +int xdl_util_get_api_level(void) { + static int xdl_util_api_level = -1; + + if (xdl_util_api_level < 0) { + int api_level = android_get_device_api_level(); + if (api_level < 0) + api_level = xdl_util_get_api_level_from_build_prop(); // compatible with unusual models + if (api_level < __ANDROID_API_J__) api_level = __ANDROID_API_J__; + + __atomic_store_n(&xdl_util_api_level, api_level, __ATOMIC_SEQ_CST); + } + + return xdl_util_api_level; +} diff --git a/module/src/main/cpp/xdl/xdl_util.h b/module/src/main/cpp/xdl/xdl_util.h new file mode 100644 index 0000000..1bd8502 --- /dev/null +++ b/module/src/main/cpp/xdl/xdl_util.h @@ -0,0 +1,71 @@ +// Copyright (c) 2020-2021 HexHacking Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// Created by caikelun on 2020-10-04. + +#ifndef IO_HEXHACKING_XDL_UTIL +#define IO_HEXHACKING_XDL_UTIL + +#include +#include +#include + +#ifndef __LP64__ +#define XDL_UTIL_LINKER_BASENAME "linker" +#define XDL_UTIL_LINKER_PATHNAME "/system/bin/linker" +#define XDL_UTIL_APP_PROCESS_BASENAME "app_process32" +#define XDL_UTIL_APP_PROCESS_PATHNAME "/system/bin/app_process32" +#define XDL_UTIL_APP_PROCESS_BASENAME_K "app_process" +#define XDL_UTIL_APP_PROCESS_PATHNAME_K "/system/bin/app_process" +#else +#define XDL_UTIL_LINKER_BASENAME "linker64" +#define XDL_UTIL_LINKER_PATHNAME "/system/bin/linker64" +#define XDL_UTIL_APP_PROCESS_BASENAME "app_process64" +#define XDL_UTIL_APP_PROCESS_PATHNAME "/system/bin/app_process64" +#endif +#define XDL_UTIL_VDSO_BASENAME "[vdso]" + +#define XDL_UTIL_TEMP_FAILURE_RETRY(exp) \ + ({ \ + __typeof__(exp) _rc; \ + do { \ + errno = 0; \ + _rc = (exp); \ + } while (_rc == -1 && errno == EINTR); \ + _rc; \ + }) + +#ifdef __cplusplus +extern "C" { +#endif + +bool xdl_util_starts_with(const char *str, const char *start); +bool xdl_util_ends_with(const char *str, const char *ending); + +size_t xdl_util_trim_ending(char *start); + +int xdl_util_get_api_level(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/module/src/main/cpp/zygisk.hpp b/module/src/main/cpp/zygisk.hpp new file mode 100644 index 0000000..a7383e5 --- /dev/null +++ b/module/src/main/cpp/zygisk.hpp @@ -0,0 +1,326 @@ +// This is the public API for Zygisk modules. +// DO NOT MODIFY ANY CODE IN THIS HEADER. + +#pragma once + +#include + +#define ZYGISK_API_VERSION 2 + +/* + +Define a class and inherit zygisk::ModuleBase to implement the functionality of your module. +Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk. + +Please note that modules will only be loaded after zygote has forked the child process. +THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM SERVER PROCESS, NOT THE ZYGOTE DAEMON! + +Example code: + +static jint (*orig_logger_entry_max)(JNIEnv *env); +static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); } + +static void example_handler(int socket) { ... } + +class ExampleModule : public zygisk::ModuleBase { +public: + void onLoad(zygisk::Api *api, JNIEnv *env) override { + this->api = api; + this->env = env; + } + void preAppSpecialize(zygisk::AppSpecializeArgs *args) override { + JNINativeMethod methods[] = { + { "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max }, + }; + api->hookJniNativeMethods(env, "android/util/Log", methods, 1); + *(void **) &orig_logger_entry_max = methods[0].fnPtr; + } +private: + zygisk::Api *api; + JNIEnv *env; +}; + +REGISTER_ZYGISK_MODULE(ExampleModule) + +REGISTER_ZYGISK_COMPANION(example_handler) + +*/ + +namespace zygisk { + +struct Api; +struct AppSpecializeArgs; +struct ServerSpecializeArgs; + +class ModuleBase { +public: + + // This function is called when the module is loaded into the target process. + // A Zygisk API handle will be sent as an argument; call utility functions or interface + // with Zygisk through this handle. + virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {} + + // This function is called before the app process is specialized. + // At this point, the process just got forked from zygote, but no app specific specialization + // is applied. This means that the process does not have any sandbox restrictions and + // still runs with the same privilege of zygote. + // + // All the arguments that will be sent and used for app specialization is passed as a single + // AppSpecializeArgs object. You can read and overwrite these arguments to change how the app + // process will be specialized. + // + // If you need to run some operations as superuser, you can call Api::connectCompanion() to + // get a socket to do IPC calls with a root companion process. + // See Api::connectCompanion() for more info. + virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {} + + // This function is called after the app process is specialized. + // At this point, the process has all sandbox restrictions enabled for this application. + // This means that this function runs as the same privilege of the app's own code. + virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {} + + // This function is called before the system server process is specialized. + // See preAppSpecialize(args) for more info. + virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {} + + // This function is called after the system server process is specialized. + // At this point, the process runs with the privilege of system_server. + virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {} +}; + +struct AppSpecializeArgs { + // Required arguments. These arguments are guaranteed to exist on all Android versions. + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jint &mount_external; + jstring &se_info; + jstring &nice_name; + jstring &instruction_set; + jstring &app_data_dir; + + // Optional arguments. Please check whether the pointer is null before de-referencing + jboolean *const is_child_zygote; + jboolean *const is_top_app; + jobjectArray *const pkg_data_info_list; + jobjectArray *const whitelisted_data_info_list; + jboolean *const mount_data_dirs; + jboolean *const mount_storage_dirs; + + AppSpecializeArgs() = delete; +}; + +struct ServerSpecializeArgs { + jint &uid; + jint &gid; + jintArray &gids; + jint &runtime_flags; + jlong &permitted_capabilities; + jlong &effective_capabilities; + + ServerSpecializeArgs() = delete; +}; + +namespace internal { +struct api_table; +template void entry_impl(api_table *, JNIEnv *); +} + +// These values are used in Api::setOption(Option) +enum Option : int { + // Force Magisk's denylist unmount routines to run on this process. + // + // Setting this option only makes sense in preAppSpecialize. + // The actual unmounting happens during app process specialization. + // + // Set this option to force all Magisk and modules' files to be unmounted from the + // mount namespace of the process, regardless of the denylist enforcement status. + FORCE_DENYLIST_UNMOUNT = 0, + + // When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize. + // Be aware that after dlclose-ing your module, all of your code will be unmapped from memory. + // YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS. + DLCLOSE_MODULE_LIBRARY = 1, +}; + +// Bit masks of the return value of Api::getFlags() +enum StateFlag : uint32_t { + // The user has granted root access to the current process + PROCESS_GRANTED_ROOT = (1u << 0), + + // The current process was added on the denylist + PROCESS_ON_DENYLIST = (1u << 1), +}; + +// All API functions will stop working after post[XXX]Specialize as Zygisk will be unloaded +// from the specialized process afterwards. +struct Api { + + // Connect to a root companion process and get a Unix domain socket for IPC. + // + // This API only works in the pre[XXX]Specialize functions due to SELinux restrictions. + // + // The pre[XXX]Specialize functions run with the same privilege of zygote. + // If you would like to do some operations with superuser permissions, register a handler + // function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func). + // Another good use case for a companion process is that if you want to share some resources + // across multiple processes, hold the resources in the companion process and pass it over. + // + // The root companion process is ABI aware; that is, when calling this function from a 32-bit + // process, you will be connected to a 32-bit companion process, and vice versa for 64-bit. + // + // Returns a file descriptor to a socket that is connected to the socket passed to your + // module's companion request handler. Returns -1 if the connection attempt failed. + int connectCompanion(); + + // Get the file descriptor of the root folder of the current module. + // + // This API only works in the pre[XXX]Specialize functions. + // Accessing the directory returned is only possible in the pre[XXX]Specialize functions + // or in the root companion process (assuming that you sent the fd over the socket). + // Both restrictions are due to SELinux and UID. + // + // Returns -1 if errors occurred. + int getModuleDir(); + + // Set various options for your module. + // Please note that this function accepts one single option at a time. + // Check zygisk::Option for the full list of options available. + void setOption(Option opt); + + // Get information about the current process. + // Returns bitwise-or'd zygisk::StateFlag values. + uint32_t getFlags(); + + // Hook JNI native methods for a class + // + // Lookup all registered JNI native methods and replace it with your own functions. + // The original function pointer will be saved in each JNINativeMethod's fnPtr. + // If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr + // will be set to nullptr. + void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods); + + // For ELFs loaded in memory matching `regex`, replace function `symbol` with `newFunc`. + // If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`. + void pltHookRegister(const char *regex, const char *symbol, void *newFunc, void **oldFunc); + + // For ELFs loaded in memory matching `regex`, exclude hooks registered for `symbol`. + // If `symbol` is nullptr, then all symbols will be excluded. + void pltHookExclude(const char *regex, const char *symbol); + + // Commit all the hooks that was previously registered. + // Returns false if an error occurred. + bool pltHookCommit(); + +private: + internal::api_table *impl; + template friend void internal::entry_impl(internal::api_table *, JNIEnv *); +}; + +// Register a class as a Zygisk module + +#define REGISTER_ZYGISK_MODULE(clazz) \ +void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \ + zygisk::internal::entry_impl(table, env); \ +} + +// Register a root companion request handler function for your module +// +// The function runs in a superuser daemon process and handles a root companion request from +// your module running in a target process. The function has to accept an integer value, +// which is a socket that is connected to the target process. +// See Api::connectCompanion() for more info. +// +// NOTE: the function can run concurrently on multiple threads. +// Be aware of race conditions if you have a globally shared resource. + +#define REGISTER_ZYGISK_COMPANION(func) \ +void zygisk_companion_entry(int client) { func(client); } + +/************************************************************************************ + * All the code after this point is internal code used to interface with Zygisk + * and guarantee ABI stability. You do not have to understand what it is doing. + ************************************************************************************/ + +namespace internal { + +struct module_abi { + long api_version; + ModuleBase *_this; + + void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *); + void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *); + void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *); + void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *); + + module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), _this(module) { + preAppSpecialize = [](auto self, auto args) { self->preAppSpecialize(args); }; + postAppSpecialize = [](auto self, auto args) { self->postAppSpecialize(args); }; + preServerSpecialize = [](auto self, auto args) { self->preServerSpecialize(args); }; + postServerSpecialize = [](auto self, auto args) { self->postServerSpecialize(args); }; + } +}; + +struct api_table { + // These first 2 entries are permanent, shall never change + void *_this; + bool (*registerModule)(api_table *, module_abi *); + + // Utility functions + void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int); + void (*pltHookRegister)(const char *, const char *, void *, void **); + void (*pltHookExclude)(const char *, const char *); + bool (*pltHookCommit)(); + + // Zygisk functions + int (*connectCompanion)(void * /* _this */); + void (*setOption)(void * /* _this */, Option); + int (*getModuleDir)(void * /* _this */); + uint32_t (*getFlags)(void * /* _this */); +}; + +template +void entry_impl(api_table *table, JNIEnv *env) { + ModuleBase *module = new T(); + if (!table->registerModule(table, new module_abi(module))) + return; + auto api = new Api(); + api->impl = table; + module->onLoad(api, env); +} + +} // namespace internal + +inline int Api::connectCompanion() { + return impl->connectCompanion ? impl->connectCompanion(impl->_this) : -1; +} +inline int Api::getModuleDir() { + return impl->getModuleDir ? impl->getModuleDir(impl->_this) : -1; +} +inline void Api::setOption(Option opt) { + if (impl->setOption) impl->setOption(impl->_this, opt); +} +inline uint32_t Api::getFlags() { + return impl->getFlags ? impl->getFlags(impl->_this) : 0; +} +inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) { + if (impl->hookJniNativeMethods) impl->hookJniNativeMethods(env, className, methods, numMethods); +} +inline void Api::pltHookRegister(const char *regex, const char *symbol, void *newFunc, void **oldFunc) { + if (impl->pltHookRegister) impl->pltHookRegister(regex, symbol, newFunc, oldFunc); +} +inline void Api::pltHookExclude(const char *regex, const char *symbol) { + if (impl->pltHookExclude) impl->pltHookExclude(regex, symbol); +} +inline bool Api::pltHookCommit() { + return impl->pltHookCommit != nullptr && impl->pltHookCommit(); +} + +} // namespace zygisk + +[[gnu::visibility("default")]] [[gnu::used]] +extern "C" void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *); + +[[gnu::visibility("default")]] [[gnu::used]] +extern "C" void zygisk_companion_entry(int); diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..a843339 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +include ':module' + +import org.apache.tools.ant.DirectoryScanner + +DirectoryScanner.removeDefaultExclude('**/.gitattributes') diff --git a/template/magisk_module/META-INF/com/google/android/update-binary b/template/magisk_module/META-INF/com/google/android/update-binary new file mode 100644 index 0000000..28b48e5 --- /dev/null +++ b/template/magisk_module/META-INF/com/google/android/update-binary @@ -0,0 +1,33 @@ +#!/sbin/sh + +################# +# Initialization +################# + +umask 022 + +# echo before loading util_functions +ui_print() { echo "$1"; } + +require_new_magisk() { + ui_print "*******************************" + ui_print " Please install Magisk v20.4+! " + ui_print "*******************************" + exit 1 +} + +######################### +# Load util_functions.sh +######################### + +OUTFD=$2 +ZIPFILE=$3 + +mount /data 2>/dev/null + +[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk +. /data/adb/magisk/util_functions.sh +[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk + +install_module +exit 0 diff --git a/template/magisk_module/META-INF/com/google/android/updater-script b/template/magisk_module/META-INF/com/google/android/updater-script new file mode 100644 index 0000000..11d5c96 --- /dev/null +++ b/template/magisk_module/META-INF/com/google/android/updater-script @@ -0,0 +1 @@ +#MAGISK diff --git a/template/magisk_module/module.prop b/template/magisk_module/module.prop new file mode 100644 index 0000000..236bd20 --- /dev/null +++ b/template/magisk_module/module.prop @@ -0,0 +1,6 @@ +id=${id} +name=${name} +version=${version} +versionCode=${versionCode} +author=${author} +description=${description}