前言:
坛友们,年轻就是资本,和我一起逆天改命吧,我的学习过程全部记录及学习资源:https://www.52pojie.cn/thread-1582287-1-1.html
立帖为证!--------记录学习的点点滴滴
0x1 静态分析
1.用Android killer反编译apk文件,然后卡住了???
2.关闭Androidkiller重开,点刚刚反编译过的工程,点是即可,但是看不到apk对应的java源码,这个只有不影响反编译即可。
3.我还是想先看看源码,使用jadx-gui工具,打开apk,就可以查看源码了。
4.首先还是运行apk,点击注册,输入用户名和密码,会提示注册失败。
5.查看j反编译出的ava源码,可以看到,判断我输入的是否为空,不为空进入下一步调用JNI函数。
public class MainActivity extends AppCompatActivity {
String name = BuildConfig.FLAVOR;
String code = BuildConfig.FLAVOR;
public native String stringFromJNI(String str, String str2);
static {
System.loadLibrary("native-lib");
}
/* JADX INFO: Access modifiers changed from: protected */
@Override // android.support.v7.app.AppCompatActivity, android.support.v4.app.FragmentActivity, android.support.v4.app.SupportActivity, android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((Button) findViewById(R.id.bt)).setOnClickListener(new View.OnClickListener() { // from class: com.cm.shuair.crackme1.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View v) {
MainActivity.this.name = ((EditText) MainActivity.this.findViewById(R.id.name)).getText().toString();
MainActivity.this.code = ((EditText) MainActivity.this.findViewById(R.id.code)).getText().toString();
if (MainActivity.this.name.equals(BuildConfig.FLAVOR) || MainActivity.this.code.equals(BuildConfig.FLAVOR)) {
Toast.makeText(MainActivity.this, "不能为空哈", 0).show();
} else {
Toast.makeText(MainActivity.this, MainActivity.this.stringFromJNI(MainActivity.this.name, MainActivity.this.code), 0).show();
}
}
});
}
}
6.因为我的雷电模拟器是x86架构,所以选择x86目录下的so文件,通过IDA加载,根据java调用的函数名,找到对应的函数名。
7.F5反编译一下,调用时传递了name,code,0三个参数,然后返回值是v8,最后将v8展示在屏幕上。
int __cdecl Java_com_cm_shuair_crackme1_MainActivity_stringFromJNI(_JNIEnv *a1, int a2, int a3, int a4)
{
int v5; // [esp+2Ch] [ebp-20h]
char *v6; // [esp+30h] [ebp-1Ch]
char *v7; // [esp+34h] [ebp-18h]
int v8; // [esp+38h] [ebp-14h]
v7 = (char *)Jstring2CStr(a1, a3);
v6 = (char *)Jstring2CStr(a1, a4);
v5 = n(v7);
if ( v5 == c(v6) )
v8 = _JNIEnv::NewStringUTF(a1, byte_102B);
else
v8 = _JNIEnv::NewStringUTF(a1, byte_1062);
return v8;
}
8.v8的两次赋值我们点进去看看是什么,一串十六进制数据,猜测是中文字符串,放在在线HEX解码工具,也就是v5==c(v6)就会提示注册成功。
.rodata:00001062 byte_1062 db 0E6h, 0B3h, 0A8h, 0E5h, 86h, 8Ch, 0E5h, 0A4h, 0B1h
.rodata:00001062 ; DATA XREF: Java_com_cm_shuair_crackme1_MainActivity_stringFromJNI+CA↑o
.rodata:00001062 db 0E8h, 0B4h, 0A5h, 0
.rodata:0000102B byte_102B db 0E6h, 0B3h, 0A8h, 0E5h, 86h, 8Ch, 0E6h, 88h, 90h, 0E5h
.rodata:0000102B ; DATA XREF: Java_com_cm_shuair_crackme1_MainActivity_stringFromJNI+A6↑o
.rodata:0000102B db 8Ah, 9Fh, 0EFh, 0BCh, 81h, 0EFh, 0BCh, 81h, 0EFh, 0BCh
.rodata:0000102B db 81h, 0EFh, 0BCh, 81h, 0EFh, 0BCh, 81h, 0EFh, 0BCh, 81h
.rodata:0000102B db 0EFh, 0BCh, 81h, 0EFh, 0BCh, 81h, 0EFh, 0BCh, 81h, 0EFh
.rodata:0000102B db 0BCh, 81h, 0EFh, 0BCh, 81h, 0EFh, 0BCh, 81h, 0EFh, 0BCh
.rodata:0000102B db 81h, 0EFh, 0BCh, 81h, 0
注册成功:E6B3A8E5868CE68890E58A9F
注册失败:E6B3A8E5868CE5A4B1E8B4A5
9.比较流程很清楚了n(v7)==c(v6),而v6和v7是调用时传进来的参数,看so里面的调用约定是__cdecl从右往左入栈,那么a3是?晕了晕了,一会动态看看。
0x2 动态调试
1.翻一翻笔记,ida调试步骤:
1.将android-server拷贝到手机,赋予可执行权限并运行
2.adb shell am start -D -n包名/.入口界面
3.使用ida附加目标进程,下断点
4. jdb -connect com.sun.jdi.SocketAttach:hostname= localhost,port=8700(ddms中看端口)
2.在IDA目录中找到对应版本的server,直接在目录上输入cmd打开命令行,就不用切换目录,直接push上去。
3.首先输入adb shell,然后输入mount -o rw,remount /重新挂载目录可读可写,exit退出shell环境,输入adb push android_x86_server /sbin,将文件上传到目录,adb shell再次进入shell环境,chmod +x /sbin/android_x86_server,给它可执行权限,然后输入android_x86_server跑起来,注意别把这个窗口关了。
4.在开一个窗口输入adb forward tcp:23946 tcp:23946,转发一下端口到本地,这里因为没有反调试,所以没有开ddms看端口再以debug模式启动app。
5.在模拟器中将apk运行起来,然后IDA attach,注意选remote linux调试器,然后ip填127.0.0.1即可,找到crackme包名,双击附加程序,运行起来,搜索so文件,找到模块中对应的函数,双击过来,看到地址复制一下在反汇编窗口按g键粘贴地址,就到了我前面静态分析时看到的函数了。
6.下一个断点,name输入123456,code输入654321,点注册成功断了下来:
7.F8运行两次,然后看看v7和v6的值,对着v7双击过来,全是未定义的数据,右键double word,就显示出来了v6是code,v7是name。
[stack]:CFFFD890 dd offset a654321 ; "654321" v6
[stack]:CFFFD894 dd offset a123456 ; "123456" v7
8.继续F8向下走,接着执行n函数,将name进行处理,for循环遍历字符串,小于65直接跳出循环,小于等于90,存起来,大于90减去32,对照ascii码表,这段代码的意思就是小写字母转大写字母,大写字母就是直接存起来,将name的每一位大写字母ascii值加起来最后异或0x9988。
int __cdecl Z1nPc(int a1)
{
int v2; // [esp+4h] [ebp-14h]
char v3; // [esp+Fh] [ebp-9h]
int v4; // [esp+10h] [ebp-8h]
int i; // [esp+14h] [ebp-4h]
v4 = 0;
for ( i = 0; *(_BYTE *)(a1 + i); ++i )
{
v3 = *(_BYTE *)(a1 + i);
if ( v3 < 65 )
break;
if ( v3 <= 90 )
v2 = v3;
else
v2 = v3 - 32;
v4 += v2;
}
return v4 ^ 0x9988;
}
9.继续F8向下走,同样遍历code,将code的的每一位减去48,48对应数字0的ascii码值,每次对v2乘以10再累加,最终返回v2与0x1256的结果。
int __cdecl Z1cPc(int a1)
{
int v2; // [esp+10h] [ebp-8h]
int i; // [esp+14h] [ebp-4h]
v2 = 0;
for ( i = 0; *(_BYTE *)(a1 + i); ++i )
v2 = *(char *)(a1 + i) + 10 * v2 - 48;
return v2 ^ 0x1256;
}
0x3 注册机编写
1.到这里整个程序的算法是分析完成了,接下来就可以编写算法注册机了,算法注册机就是利用name计算code,那么对name进行处理的函数我就可以直接拿来用,对code处理的函数颠倒过来。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char name[255] = "";
printf("请输入用户名:\n");
scanf("%s", name);
//对name处理
char v3;
int v2,n_name,code;
for (int i = 0; name[i]; ++i)
{
v3 = name[i];
if (v3 < 65)
break;
if (v3 <= 90)
v2 = v3;
else
v2 = v3 - 32;
n_name += v2;
}
n_name ^= 0x9988;
//计算code
code = n_name ^0x1256;
}
2.name不用改,可以直接套用,code该怎么去写呢?循环的退出条件应该是什么?抽丝剥茧后v2 = a1[i] + 10 v2 - 48;核心代码是这里,注意这里可不能像解方程一样,不然a1[i] = v2-10v2+48,不用想也知道不对,这和我之前分析的算法不一样,这里赋值是一个累加的过程。
for ( i = 0; a1[i]; ++i )
v2 = a1[i] + 10 * v2 - 48;
每次将自身乘以10+a[i]-48
反过来应该是每次将自身除以10+48再减去a1[i]的值,那么a1[i]就可以看做是余数
3.按照刚刚分析的思路,写出code的计算过程,输入验证一下,不成功。
//计算code
n_code = n_name ^ 0x1256;
for (j = 0; n_code >= 10; ++j) //循环退出条件就是ncode小于10
{
code[j] = n_code % 10 + 48; //计算ncode对10的余数再加上48
n_code /= 10; //自身除以10
}
code[j] = n_code + 48; //最后一次不计算直接+48
code[j+1] = 0;//末尾补0
4.实在是想不到哪里出错了,vscode里面lanuch.json文件这里false改成true,"externalConsole": true,然后打上断点,单步调试,可以看到ncode异或的值35511,最后赋值出来的code字符串是11553,试试输入35511提示注册成功。
5.好家伙,原因找到了,我的思路并没有错,但是最后35511,从高位到低位赋值1 1 5 5 3,所以我应该先计算出循环的次数,然后倒着赋值,得到最终版注册机。
#include <stdio.h>
#include <stdlib.h>
int main()
{
char name[255] = "";
char code[255] = "";
int i; //循环变量
int j; //循环变量
printf("Please enter name:\n");
scanf("%s", name);
//对name处理
char v3 = 0;
int v2 = 0, n_name = 0, n_code = 0;
for (i = 0; name[i]; ++i)
{
v3 = name[i];
if (v3 < 65)
break;
if (v3 <= 90)
v2 = v3;
else
v2 = v3 - 32;
n_name += v2;
}
n_name ^= 0x9988;
//计算code
n_code = n_name ^ 0x1256;
j = 0;
v2 = n_code;
while (v2)//计算循环的次数j
{
v2 /= 10;
j++;
}
for (; j > 0; --j) //循环退出条件就是ncode小于0
{
code[j-1] = n_code % 10 + 48; //计算ncode对10的余数再加上48
n_code /= 10; //自身除以10
}
printf("Code is:%s\n", code);
system("pause");
}
6.现在试试注册机,生成三组code,都是成功:
name:baidu
code:35515
name:WwWAdminWwW
code:34989
name:MySqlOracle
code:34962
0x4 总结
1.因为这题纯so层运算,显示的结果也是so层出来的,所以没有用到JEB动态调试。
2.字母大小写转换,数字除以10,我也喜欢这么写,比较熟悉,所以分析的比较顺利,在code反推的那个地方卡了一下,把它想复杂了。
3.code那里其实只会是数字,减去48就是字符到ascii值的转换,反推这里时我把他当成数列求和,越想越晕,好在最后从死胡同里绕出来了。
4.vs code运行的时候没找到输入窗口,最后百度了一下,改一下launch文件,成功弹出dos窗口了。
0x5 参考资料
1.CM来源地址:新出炉的入门级crackMe
https://www.52pojie.cn/thread-956163-1-1.html
(出处: 吾爱破解论坛)