吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8002|回复: 30
上一主题 下一主题
收起左侧

[2021] 2021 腾讯游戏安全技术复赛 android 客户端安全 wp

  [复制链接]
跳转到指定楼层
楼主
shyoshyo 发表于 2021-4-18 12:53 回帖奖励
点开发现仍然是一个unity 工程,版本号2018.3.1f,后端il2cpp,global-metadata.dat 被加密,libunity.so 和 libil2cpp.so 内有壳

最首先是libunity.so、libil2cpp.so、libsec2021.so 的 ELF 文件头被恶意破坏了(但是libmain.so 的没有),IDA 无法分析。结合 readelf 给的信息(或者对比libunity.so、libmain.so,或者对比unity 2018.3.1f  官方 libunity.so 与这个 libunity.so的文件头),可直接修复。




然后是分析反调试策略的绕过。Libsec2021 中没有保护好 ida 的调试端口 23946,直接搜代码就能搜到:

$ objdump -D libsec2021.so | grep \#23946      
   1bf30: 8a 1d 05 e3                          movw        r1, #23946

这样就直接定位到了 ida 反调试的地方,根据静态分析交叉引用,结合动态调试,ida 不难发现出现问题的地方 1B5F0



经过一定时间分析,不难发现其中有一个疑似与反调试和检测高度相关的变量 debug_related 。只要经过了他所在的那一句,程序就会崩溃退出。

此外,在摸索过程中,我发现一个有趣的现象。利用他可以快速的暴露当前是哪一地方检测没通过。具体而言,当libsec2021.so 被 patch 之后,下面两句的 BLX 就会出现野指针,然后跑飞出现异常。如果观察崩溃时系统的 dump 记录,很快就能知道是哪里出问题了。这个看起来是这个壳设计上的失误。例如,下面是程序崩溃时系统的 dump:





当程序跑飞时的瞬间,返回值 BL 就是跑飞的地方。

当前libsec2021的基是c1aed000,lr = c1b11604,一减就是出问题地方的RVA

>>> hex(0xc1b11604 - 0xc1aed000)
'0x24604'



此外,除了 patch 程序,修改 debug_related 的值,也会造成上述的崩溃且被系统记录,通过查看日志很快就可以绕过。




在试图绕过的时候,我发现不能直接 patch 程序,因为在最开始的时候程序用他的代码算了一些东西(通过打硬件断点不难发现),例如下面是其中一处:



当程序运行到此处内容被 patch 导致计算结果变化时。后面的解壳代码会出错

基于上述考虑,我使用了一种动态改内存的方法绕过反调。我写了一个命令行程序,解壳完毕游戏启动之后就启动。一上来先把 debug_related (004B230) 修改了。然后根据日志把其他具备反调试策略的代码的修改也一一加入,最后知道 ida/gdb attach 之后不会报错位置。写内存是通过读写 /proc/pid/mem 文件实现的,主要代码如下

[Asm] 纯文本查看 复制代码

void ipatch(int mem_fd, unsigned long long addr, unsigned char old, unsigned char new, int dir)
{
	unsigned char buf1[] = {0, 0};
	unsigned char buf2[] = {0, 0};
	unsigned char buf3[] = {dir ? old : new, 0};

	lseek64(mem_fd, addr, SEEK_SET);
	read(mem_fd, buf1, 1);

	lseek64(mem_fd, addr, SEEK_SET);
	write(mem_fd, buf3, 1);

	lseek64(mem_fd, addr, SEEK_SET);
	read(mem_fd, buf2, 1);

	printf("%s  %08llx: %02x -> %02x\n", dir ? "old" : "new", addr, buf1[0], buf2[0]);
}

int main(int argc, char *argv[])
{
	//…
	sprintf(mem_file_name, "/proc/%s/mem", argv[1]);
	mem_fd = open(mem_file_name, O_RDWR);
	long long addr = findaddr(argv[1], "sec2021", "00000000");

	if (addr != -1)
	{

		printf("start patch ...\n");

		ipatch(mem_fd, addr + 0x01B541 - 1, 0x2A, 0x00, 0);
		ipatch(mem_fd, addr + 0x01B543 - 1, 0x00, 0xA0, 0);
		// 省略若干,都可以根据崩溃日志分析得到
		ipatch(mem_fd, addr + 0x00024602, 0x2F, 0xA0, 0);
		ipatch(mem_fd, addr + 0x00024606, 0xE0, 0x00, 0);


		ipatch(mem_fd, addr + 0x0004B230, 0x9f, 0x12, 0);
		ipatch(mem_fd, addr + 0x0004B231, 0x13, 0x34, 0);
		ipatch(mem_fd, addr + 0x0004B232, 0x79, 0x56, 0);
		ipatch(mem_fd, addr + 0x0004B233, 0x66, 0x78, 0);

		printf("patch done %d ...\n", 0);
	}
	return 0;
}



具体代码详见 bypass.c,编译命令为

armv7a-linux-androideabi18-clang /tmp/bypass.c -llog -o /tmp/bypass

以root 权限运行bypass 程序(程序接受一个命令行参数,为游戏进程 pid)后效果如下:



可见确实起了反反调试的作用,strace 不再会导致程序退出了。IDA 和 gdb 也是一样的。至此反调试部分介绍完毕。








接下来分析游戏逻辑。由于使用了 il2cpp,且libil2cpp.so 有壳,global-metadata.dat 也被加密,因此接下来常见的思路是恢复 global-metadata.dat,脱壳 libil2cpp.so,然后上 il2cppDump 获取 cs 头文件,从而知道后面该看什么地方。但是针对这个问题,我发现一个更简单的思路。

Libil2cpp.so 脱壳还是照常,结合 /proc/pid/maps 和 /proc/pid/mem 很容易搞定。见 dump.c。dump 完了之后还要再修一下文件头。



接下来是逻辑的分析。我在尝试的时候发现了一种基于string 的方法。具体而言,在 il2cpp plt 表的 strlen 函数上下断点:


(gdb) x/5i (0xbea2e000 + 0x8FD78)
   0xbeabdd78 <strlen@plt>:        add        r12, pc, #6291456        ; 0x600000
   0xbeabdd7c <strlen@plt+4>:        add        r12, r12, #811008        ; 0xc6000
   0xbeabdd80 <strlen@plt+8>:        ldr        pc, [r12, #1504]!        ; 0x5e0
   0xbeabdd84 <_Znwj@plt>:        add        r12, pc, #6291456        ; 0x600000
   0xbeabdd88 <_Znwj@plt+4>:        add        r12, r12, #811008        ; 0xc6000
(gdb) hb *(0xbea2e000 + 0x8FD78)
Hardware assisted breakpoint 1 at 0xbeabdd78
(gdb) command
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>x/s $r0
>c
>end
(gdb) c
Continuing.
[New Thread 15415.15932]
[New Thread 15415.15934]
[New Thread 15415.15935]
[Switching to Thread 15415.15438]

Thread 14 "UnityMain" hit Breakpoint 1, 0xbeabdd78 in strlen@plt () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libil2cpp.so
0xbec2ed84:        "UnityEngine.Object::Destroy(UnityEngine.Object,System.Single)"

Thread 14 "UnityMain" hit Breakpoint 1, 0xbeabdd78 in strlen@plt () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libil2cpp.so
0xbec2ed84:        "UnityEngine.Object::Destroy(UnityEngine.Object,System.Single)"

// 省略

Thread 14 "UnityMain" hit Breakpoint 1, 0xbeabdd78 in strlen@plt () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libil2cpp.so
0xbe3329a4:        "EndGame"

Thread 14 "UnityMain" hit Breakpoint 1, 0xbeabdd78 in strlen@plt () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libil2cpp.so
0xbe3329ac:        "GameOver"

Thread 14 "UnityMain" hit Breakpoint 1, 0xbeabdd78 in strlen@plt () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libil2cpp.so
0xbe3329b5:        "Replay"

// 省略

可以看到很多字符串都出现了。其中 GameOver 意味着判定上了,游戏失败,是赛题中需要处理的地方。因此在 "GameOver" 下硬件断点

^C
Thread 1 "onal.flappybird" received signal SIGINT, Interrupt.
[Switching to Thread 15415.15415]
0xea621394 in __epoll_pwait () from target:/apex/com.android.runtime/lib/bionic/libc.so
(gdb) dis
(gdb) awatch *(0xbe3329ac)
Hardware access (read/write) watchpoint 2: *(0xbe3329ac)
(gdb) c
Continuing.
[New Thread 15415.16058]
[New Thread 15415.16059]
[Switching to Thread 15415.15438]


// 操作游戏,当撞上的时候,立马就出现下面的一句
Thread 14 "UnityMain" hit Hardware access (read/write) watchpoint 2: *(0xbe3329ac)

Value = 1701667143
0xea5e6568 in strcmp_a15 () from target:/apex/com.android.runtime/lib/bionic/libc.so
(gdb)

硬件断点表明:读 "GameOver" 的时候很有就是判定失败时的逻辑,因为每次撞上(不管是柱子还是地板)都会执行这一句。

不妨根据调用栈
(gdb) bt
#0  0xea5e6568 in strcmp_a15 () from target:/apex/com.android.runtime/lib/bionic/libc.so
#1  0xc0dbe48c in ?? () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libunity.so
#2  0xc0db0884 in ?? () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libunity.so
#3  0xc0da80d8 in ?? () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libunity.so
#4  0xc0da8084 in ?? () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libunity.so
#5  0xc0d354bc in ?? () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libunity.so
#6  0xbef6d0ec in ?? () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libil2cpp.so
#7  0xbef6f2a0 in ?? () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libil2cpp.so
#8  0xbef6f1f0 in ?? () from target:/data/app/com.personal.flappybird-5yocNuCl7GDoMAhFUkMXKg==/lib/arm/libil2cpp.so
Backtrace stopped: previous frame identical to this frame (corrupt stack?)

在上述标红的 libil2cpp 相关的地方前后、栈上栈下都下断点,看看故意撞上/没撞上柱子时,断点有没有段下。由于脱壳后代码没有跳转混淆,因此大段大段的代码:要么是无论撞没撞上都会断下来;要么是前半段无论撞没撞上都会断下来但后半段是只有撞上才断下来;要么是全都只有撞上才断下来。使用二分搜索的策略,可以很快的定位到上一句无论撞不撞都断,但下一句不断(这一句就是个跳转)。这个跳转就是赛题任务的关键了,劫持它就能实现任务。

经过一段时间的分析,不难定位到,有两个关键的跳转(对应撞地板和撞柱子),分别位于

LOAD:00540DA8                 BNE             loc_540E24
LOAD:005413E0                 BNE             loc_541458







在之前 bypass 反调试的基础上,增加几句 il2cpp的 patch 就能实现赛题的要求。

[C] 纯文本查看 复制代码

int main(int argc, char *argv[])
{
	//…
	for (;;)
	{
		addr = findaddr(argv[1], "il2cpp", "00090000");

		if (addr > 0)
		{
			printf("il2cpp OK\n");
		}
		else
		{
			printf("wait il2cpp\n");
			sleep(1);
			continue;
		}

		ipatch(mem_fd, 0x540DA8 + addr - 0x90000, 0x00, 0x00, 0);
		ipatch(mem_fd, 0x540DA9 + addr - 0x90000, 0x00, 0x00, 0);
		ipatch(mem_fd, 0x540DAA + addr - 0x90000, 0x00, 0xA0, 0);
		ipatch(mem_fd, 0x540DAB + addr - 0x90000, 0x00, 0xE1, 0);
		ipatch(mem_fd, 0x5413E3 + addr - 0x90000, 0x1A, 0xEA, 0);

		break;
	}

	return 0;
}




详见 bypass-patch.c。只要在 root 的手机上运行该程序,就能够实现题目要求的功能。至此游戏逻辑分析部分介绍完毕。







最后是封包的部分。一个直观的想法是修复脱壳后的程序使得能够直接patch文件就能运行,而不是上面那样 hot-patch。这里由于比赛时间有限,我采取了一个变通方案——通过静态注入的方式注入一个共享库,然后在共享库里放 hot-patch 的代码(以及绕过包重签名的)

注入利用 libmain,方式如下:将原来的 libmian.so 重命名为 libmain2.so,注入用的新代码写在 libmain.so 里,并 libmian.so 拉 libmain2.so,而且libmain.so 要将 libmain2.so 导出的 JNI_OnLoad 向调用者传过去。libmian.so 的入口是 my_init()

[C] 纯文本查看 复制代码

void __attribute__ ((constructor)) my_init(void);

typedef int (*PJNI_ONLOAD)(void *, void *);
PJNI_ONLOAD JNI_OnLoad_forward = NULL;

int JNI_OnLoad(void *a, void *b)
{
	return JNI_OnLoad_forward(a, b);
}
void my_init()
{
	LOGI("hello!!");

	void *p = dlopen("libmain2.so", RTLD_NOW);
	JNI_OnLoad1 = dlsym(p, "JNI_OnLoad");
	LOGI("libmain2.so = %p, JNI_OnLoad = %p", p, JNI_OnLoad1);

	patchlibsec2021();
	pthread_create(&tid, NULL, main_thread, NULL);
}





剩下的就是跟前面一样的bypass 与patch 。

[C] 纯文本查看 复制代码

unsigned long long findaddr(const char* s, const char* o)
{
	unsigned long long addr = -1;
	char buf[0x1000];
	char buf2[0x1000];
	int pid = getpid();
	sprintf(buf2, "/proc/%d/maps", pid);

	FILE *fp = fopen(buf2, "r");

	while (fgets(buf, sizeof buf, fp) != NULL) {
		if (strstr(buf, s) && strstr(buf, o))
		{
			LOGI("found in maps: %s\n", buf);
			sscanf(buf, "%llx", &addr);
		}
	}

	LOGI("addr = %llx\n", addr);

	fclose(fp);
	return addr;
}

void patchlibsec2021()
{
	usleep(4000 + (rand() % 100));

	long long libsec2021addr = -1;
	for (;;)
	{
		libsec2021addr = findaddr("libsec2021.so", "00000000");
		if (libsec2021addr != -1) break;
		usleep(5 + (rand() % 5));

		LOGI("wait for libsec2021 ...\n");
	}

	mprotect((void *)libsec2021addr, 0x40000, PROT_READ | PROT_WRITE | PROT_EXEC);

	LOGI("start patch libsec2021 ...\n");

	*(unsigned char *)(libsec2021addr + 0x01B541 - 1) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x01B543 - 1) = 0xA0;
	*(unsigned char *)(libsec2021addr + 0x01B544 - 1) = 0xE1;
	*(unsigned char *)(libsec2021addr + 0x01B551 - 1) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x01B553 - 1) = 0xA0;
	*(unsigned char *)(libsec2021addr + 0x01B554 - 1) = 0xE1;
	*(unsigned char *)(libsec2021addr + 0x01B5F0 - 1) = 0xEA;
	*(unsigned char *)(libsec2021addr + 0x032A61 - 1) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x032EA9 - 1) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x032EAB - 1) = 0x50;
	*(unsigned char *)(libsec2021addr + 0x037DEF - 1) = 0x50;
	*(unsigned char *)(libsec2021addr + 0x037DF0 - 1) = 0xE1;
	*(unsigned char *)(libsec2021addr + 0x037F94 - 1) = 0xE1;
	*(unsigned char *)(libsec2021addr + 0x038457) = 0xE1;
	*(unsigned char *)(libsec2021addr + 0x001DB8F) = 0xEA;
	*(unsigned char *)(libsec2021addr + 0x001DC23) = 0xEA;
	*(unsigned char *)(libsec2021addr + 0x0001E37C) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x0001E37D) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x0001E37E) = 0xA0;
	*(unsigned char *)(libsec2021addr + 0x0001E382) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x000261A8) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x000261A9) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x000261AA) = 0xA0;
	*(unsigned char *)(libsec2021addr + 0x000261AE) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x00024600) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x00024601) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x00024602) = 0xA0;
	*(unsigned char *)(libsec2021addr + 0x00024606) = 0x00;
	*(unsigned char *)(libsec2021addr + 0x0004B230) = 0x12;
	*(unsigned char *)(libsec2021addr + 0x0004B231) = 0x34;
	*(unsigned char *)(libsec2021addr + 0x0004B232) = 0x56;
	*(unsigned char *)(libsec2021addr + 0x0004B233) = 0x78;

	LOGI("patch libsec2021 done...\n");
}

void patchil2cpp()
{
	long long libil2cppaddr = -1;

	for (;;)
	{
		libil2cppaddr = findaddr("libil2cpp.so", "00090000");
		if (libil2cppaddr != -1) break;
		sleep(1);
		LOGI("wait for il2cpp");
	}
	sleep(2);

	mprotect((void *)libil2cppaddr, 0x600000, PROT_READ | PROT_WRITE | PROT_EXEC);

	LOGI("start patch il2cpp ...\n");
	*(unsigned char *)(libil2cppaddr + 0x540DA8 - 0x90000) = 0x00;
	*(unsigned char *)(libil2cppaddr + 0x540DA9 - 0x90000) = 0x00;
	*(unsigned char *)(libil2cppaddr + 0x540DAA - 0x90000) = 0xA0;
	*(unsigned char *)(libil2cppaddr + 0x540DAB - 0x90000) = 0xE1;
	*(unsigned char *)(libil2cppaddr + 0x5413E3 - 0x90000) = 0xEA;
	LOGI("patch il2cpp done...\n");
}


详细代码见 inject.c,编译命令为
armv7a-linux-androideabi18-clang /tmp/inject.c --shared -fPIC -llog -o /tmp/inject.so

打包命令为
java -jar apktool_2.5.0.jar d -srf FlappyBird.apk
mv FlappyBird/lib/armeabi-v7a/libmain.so FlappyBird/lib/armeabi-v7a/libmain2.so
cp /tmp/inject.so FlappyBird/lib/armeabi-v7a/libmain.so
java -jar apktool_2.5.0.jar b FlappyBird
jarsigner -keystore ??? -storepass ??? FlappyBird/dist/FlappyBird.apk -signedjar hack.apk test

至此所有解题步骤的介绍完毕。





本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册[Register]

x

免费评分

参与人数 17威望 +1 吾爱币 +38 热心值 +16 收起 理由
tongzi + 1 + 1 我很赞同!
xiananren + 1 + 1 谢谢@Thanks!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
daisukikotori + 1 我很赞同!
水浊则有渔 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
芥末味彩虹 + 1 + 1 反正我大部分看不懂的就很牛逼
jiou234 + 1 + 1 用心讨论,共获提升!
马潇洒 + 1 用心讨论,共获提升!
Jack2002 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
4everlove + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Eirc + 1 + 1 谢谢@Thanks!
梦天DreamSky + 1 + 1 谢谢@Thanks!
hqt + 2 + 1 用心讨论,共获提升!
hlrlqy + 2 + 1 我很赞同!
杨辣子 + 1 + 1 用心讨论,共获提升!
无敌小车 + 1 + 1 热心回复!
shuaibi_chen + 1 + 1 &amp;lt;font style=&amp;quot;vertical-align: inherit;&amp;quot;&amp;gt;&amp;lt;font style=

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
Jack-yu 发表于 2021-4-20 22:31
每每看到您这样的大佬发纯技术帖子,我也总想着哪天能够发发
推荐
oah1021 发表于 2021-4-19 10:45
感谢楼主的分享

免费评分

参与人数 1吾爱币 -1 收起 理由
Xay0ah -1 &amp;lt;font style=&amp;quot;vertical-align: inherit;&amp;quot;&amp;gt;&amp;lt;font style=

查看全部评分

3#
guangzisam 发表于 2021-4-19 11:11
4#
Jaybo 发表于 2021-4-19 15:56
感想大佬的分享!!谢谢,666
5#
huangbot 发表于 2021-4-19 16:02
太专业了
6#
qidalinokia 发表于 2021-4-19 16:38
这太专业了
7#
Baxy 发表于 2021-4-19 17:11
看不懂,支持一个666!
8#
hqt 发表于 2021-4-19 18:41
好专业的亚子
看不懂(我只学过vb,c#...
9#
南极. 发表于 2021-4-19 20:33
膜拜大佬
10#
4everlove 发表于 2021-4-19 22:34
很久没有看到这么专业的文章了  点个赞
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-27 04:58

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表