-
原本在arm9.bin中试图定位API,失败,认为是SDK版本太老,但隐约觉得这个想法不对劲。
J同学给我的SDK是2 刚才睡醒上网GOOGLE发现3都已经满天飞了,最新的4倒是没有,这么看来关于SDK的信息不是什么需要避讳的东西了,随处可下不是一天两天的事情了。找yeyezai同学要了下3版,QQ传的速度总比国外BT快的多,入手后用IDA观察.a静态库,发现没有什么大变化,还是寻找的手法不够先进。
突破在于crt0.o模块。
老任的文档说游戏程序启动要做如下几件事:
1.初始化堆栈
2.初始化BSS区域
3.初始化浮点库
4.调用
NitroStartUp5.构造静态对象
6.执行用户的入口函数
NitroMainCT2可以定位NDS程序的ARM9入口点,与crt0.o比对,真相大白。
相似度99%,由此可定位用户入口
NitroMain在目标游戏的0x02000c9c处。RAM:02000C9C NitroMain ; DATA XREF: RAM:off_200094Co
RAM:02000C9C STMFD SP!, {R3-R11,LR}
RAM:02000CA0 SUB SP, SP, #0x18
RAM:02000CA4 BL loc_200F25C
RAM:02000CA8 MOV R7, R0
RAM:02000CAC BL loc_2010684
RAM:02000CB0 STR R0, [SP]
RAM:02000CB4 BL 0x20C5364
RAM:02000CB8 LDR R1, =0x4001000
RAM:02000CBC LDR R0, [R1]
RAM:02000CC0 BIC R0, R0, #0x10000
RAM:02000CC4 STR R0, [R1]
RAM:02000CC8 BL sub_20129FC
RAM:02000CCC BL 0x20A2984
......J同学送我的No$GBA Debugger派上了用场,导出汇编到文本,100多M,用UE打开即可。
参照crt0和系统入口函数的相似度后得到一些结论,可以用于快速批量确定API位置,举例来说:
OS_arena.o模块,有API OS_AllocFromArenaLo,可以从低地址分配内存,汇编代码为:
.text:00000098 EXPORT OS_AllocFromArenaLo
.text:00000098 OS_AllocFromArenaLo
.text:00000098 STMFD SP!, {R4-R7,LR}
.text:0000009C SUB SP, SP, #4
.text:000000A0 MOV R7, R0
.text:000000A4 MOV R6, R1
.text:000000A8 MOV R5, R2
.text:000000AC BL dword_35C+8
.text:000000B0 CMP R0, #0
.text:000000B4 ADDEQ SP, SP, #4
.text:000000B8 MOVEQ R0, #0
.text:000000BC LDMEQFD SP!, {R4-R7,LR}
.text:000000C0 BXEQ LR
.text:000000C4 ADD R0, R0, R5
.text:000000C8 SUB R1, R5, #1
.text:000000CC MVN R2, R1
.text:000000D0 SUB R0, R0, #1
.text:000000D4 AND R4, R2, R0
.text:000000D8 ADD R0, R4, R6
.text:000000DC ADD R0, R0, R5
.text:000000E0 SUB R1, R0, #1
.text:000000E4 MOV R0, R7
.text:000000E8 AND R5, R2, R1
.text:000000EC BL loc_370+8
.text:000000F0 CMP R5, R0
.text:000000F4 ADDHI SP, SP, #4
.text:000000F8 MOVHI R0, #0
.text:000000FC LDMHIFD SP!, {R4-R7,LR}
.text:00000100 BXHI LR
.text:00000104 MOV R0, R7
.text:00000108 MOV R1, R5
.text:0000010C BL loc_118+8
.text:00000110 MOV R0, R4
.text:00000114 ADD SP, SP, #4
.text:00000118
.text:00000118 loc_118
.text:00000118 LDMFD SP!, {R4-R7,LR}
.text:0000011C BX LR
.text:0000011C ; End of function OS_AllocFromArenaLo注意.text:0000010C BL loc_118+8,其实是OS_SetArenaLo
而这个API为:
.text:00000120 EXPORT OS_SetArenaLo
.text:00000120 OS_SetArenaLo ; CODE XREF: OS_AllocFromArenaLo+74p
.text:00000120 ; OS_InitArenaEx+2Cp
.text:00000120 ; OS_InitArena+4Cp
.text:00000120 ; OS_InitArena+58p
.text:00000120 ; OS_InitArena+8Cp
.text:00000120 ; OS_InitArena+B4p
.text:00000120 ; OS_InitArena+DCp
.text:00000120 ; OS_InitArena+104p
.text:00000120 MOV R0, R0,LSL#2
.text:00000124 ADD R0, R0, #0x2700000
.text:00000128 ADD R0, R0, #0xFF000
.text:0000012C
.text:0000012C loc_12C
.text:0000012C STR R1, [R0,#0xDA0]
.text:00000130 BX LR在从游戏中导出的汇编代码里搜索0x2700000,观察上下文,在0x20c9fec处发现:
020CA000 E1A00100 mov r0,r0,lsl #0x2 ;1 383423
020CA004 E2800627 add r0,r0,#0x2700000 ;1 383424
020CA008 E2800AFF add r0,r0,#0xFF000 ;1 383425
020CA00C E5900DA0 ldr r0,[r0,#0xDA0] ;3 383428
020CA010 E12FFF1E bx r14 ;3 383431找到OS_SetArenaLo API的地址后,在从游戏中导出的汇编代码里搜索该地址,即获得调用它的API的地址,即OS_AllocFromArenaLo OS_InitArenaEx OS_InitArena,获得OS_InitArenaEx的地址后,在从游戏中导出的汇编代码里搜索该地址,即获得调用它的API的地址,即OS_Init Api的地址,这里为020C9E34:
020C9E34 E92D4008 stmfd r13!,{r3,r14} ;1 383188
020C9E38 EB00000F bl #0x20C9E7C ;3 383191
020C9E3C EB000A68 bl #0x20CC7E4 ;3 383194
020C9E40 EBFFFA80 bl #0x20C8848 ;3 383197
020C9E44 EB00004D bl #0x20C9F80 ;3 383200
020C9E48 EBFFF9D4 bl #0x20C85A0 ;3 383203
020C9E4C EBFFFA70 bl #0x20C8814 ;3 383206
020C9E50 EB0001AC bl #0x20CA508 ;3 383209
020C9E54 EB000A5C bl #0x20CC7CC ;3 383212
020C9E58 EB0003BD bl #0x20CAD54 ;3 383215
020C9E5C EB0005AF bl #0x20CB520 ;3 383218
020C9E60 EBFFFC30 bl #0x20C8F28 ;3 383221
020C9E64 EB000527 bl #0x20CB308 ;3 383224
020C9E68 EB002496 bl #0x20D30C8 ;3 383227
020C9E6C EB0022C7 bl #0x20D2990 ;3 383230
020C9E70 EB0017B1 bl #0x20CFD3C ;3 383233
020C9E74 EBFFFFE6 bl #0x20C9E14 ;3 383236
020C9E78 E8BD8008 ldmfd r13!,{r3,r15} ;4 383240IDA反汇编os_init.o模块:
OS_Init
STMFD SP!, {LR}
SUB SP, SP, #4
BL locret_4C+4
BL locret_4C+8
BL OS_InitArena+8
BL PXI_Init+8
BL OS_InitLock+8
BL OS_InitArenaEx+8
BL OS_InitIrqTable+8
BL OS_SetIrqStackChecker+8
BL OS_InitException+8
BL MI_Init+8
BL OS_InitVAlarm+8
BL OSi_InitVramExclusive+8
BL OS_InitThread+8
BL OS_InitReset+8
BL CTRDG_Init+8
ADD SP, SP, #4
LDMFD SP!, {LR}
locret_4C
BX LR由此可确定系列API的地址,再去这一系列API地址处观察,看它调用了哪些API或者被哪些API调用,即可网状扩大API地址数据,此为由已知发展未知,如果是想指哪打哪的获得某API地址,可在直接调用该API 或者被该API调用的地方,或者同模块内的API(一般来说相对位置在静态库中和目标程序中不变)上下功夫。
-
周二晚上和LF ztabris去西单打djmax和gf dm,终于两人吉他一人鼓的联机了一次,感觉不错。
djmax街机版和应援团很像,不过没找到乐感,除了DIVINE SERVICE这首歌,做几天背景音乐。
等到奶牛和院长下班,就跑去4P 新超马和幽白,很欢乐,但回家就感冒了,是个杯具。
然后得知其实某位同学放出的DQ9破解在烧录卡并不能跑起来,某工具的符号定位功能也是未完成版。由于烧录卡替换的IO函数不可重入导致的概率卡死问题在足够的内存空间获得前貌似无法解决,尝试了一下堆栈移动法,在模拟器上移动一定数量的话是可行的 但是在烧录卡上跑不起来,对NDS的软件系统 ARM9 ARM7的硬件知识都基本一无所知,不能确定问题所在,这个方法虽然理论上可行但要我这个门外汗通过他来实现内存获取还不现实。
如果能定位API的话,那么就可屏蔽底层实现的细节,调用或者拦截逻辑上的内存接口来实现这个功能,个人的直觉是需要的空间应该是可以被留出的,但是Joyce同学给我的2.0版SDK似乎和游戏程序对不上号,正在找新版链接库,结果如何还是未可知。
-
好像整一年前写过类似的日志,不过当时是针对WIN32上的其他人的窗口,一年的时间眨眼就过来了,也许在win32上进行的探索过程会在wii上重演吧,不过以公司刚有点好转迹象的情况来看拿不出多少心力,最近一段时间把工作上的事情解决掉之后专心享受一下游戏,堆下来的游戏够玩一阵的了,至于一些在酝酿的想法,明年春节后再说。
WII硬件寄存器内存0xcc002000至0xcc002080为视频接口,其中0xcc00201c处的4个字节为帧缓存的位置,当首字节标志为0x10时,后面的3字节数据需要左移5位作为偏移量,否则后面3字节数据直接作为偏移量,将偏移量加上基址0x80000000即得到当前的帧缓存地址,这个地址的数据实时映射到画面。
观察0xcc00201c处内存。可以看到稳定时总在两个数据间变化,这说明WII的视频缓冲是双缓存,从视频API的设计来看也可以说明这点。
帧缓存类似DOS年代的VGA13H模式(95年没有电脑不可能是DOS时代的程序员,但3年前“误入歧途”时也用TC在WIN上写过这种程序),线性映射,像素格式为32位RGBA。
帧缓存实时映射,要自定义游戏界面,找到一个合理的拦截点把指令拐走就行了,这个应该不难选择,等待视频回扫API可能就是一个好的拐走点,拐到钩子函数中愿意做什么就是自定义者自己的事情了,比如可以把帧缓存截图,可以实时给游戏画面加overlay,甚至加马赛克,改变个色调亮度之类的都是有可能的。
其实在大多数API都可以定位的前提下,在指令拐走后实现自己的用户界面也是可能的,对游戏程序自己来说,只是其中一行代码不知道到哪去了而已,返回后该去哪不会有什么影响,
渐渐没有以前那种很兴奋的感觉,似乎还有点没劲,2010年春节前,WII上的探索就这样告一段落吧。
存点关于ios开发的资料:
http://wiibrew.org/wiki/IOS
http://wiibrew.org/wiki/Custom_IOS_Module_Toolkit
http://wiibrew.org/wiki/Starlet
无意下到去年年底的一个版本的dip_module cios源代码,原来驱动是运行ARM9上的。
-----
背景音乐用舞曲感觉太闹,还是用回抒情些的好了。
-
说来很有意思,想到这个方案是无聊时闲的分析别人遇到的问题时发生的。
前些日子某汉化组放出了NDS的DQ9汉化版,由于原字库存于高速缓存中且空间不足,所以破解者用IO重定向的方式试图解决汉化字库的读取动作,由于一些细节的访问冲突,在某些烧录卡上运行时会出现死机情况。
从细节上看,这位破解组长水平不错并且很有毅力,况且是女子选手,很难得了,生读ARM9 CPU指令结合主机BIOS调用进行逆向工程能够说明问题。
不过个人观点其实做这些事没必要非得去读晦涩不易读的CPU指令,对于逆向工程来说,与其将原程序编写者当成敌人撕烂了不如当做伙伴以己推人,试着去理解他的想法比试图攻破他的弱点更有用,具体点说,如果原作者工作在汇编层次,则我们在汇编层次进行逆向工程效果最佳,如果原作者工作在C层次,则我们在C语言层次进行逆向工程效果最佳,如果原作者工作在C++层次,则我们不妨也把自己放到这个位置去理解代码的设计意图。
把我使用SDK定位目标游戏中API的基本思路和手法给她介绍了一下,权当抛砖引玉吧。
(本人玩逆向纯YY流,汇编指令阅读水平不高。)实际上,CT2在之前的某个版本就已经开始支持符号化任天堂NDS官方开发包中的某些API。
------------------------以上废话 某位同学已经隐姓埋名的放出了DQ9的破解------------------------
注意到一个事实,WII主程序(其实不止WII NDS也一样)的指令映射到内存后除非手动去修改它,那么在生存期内都不会被修改,而新指令被尾部映射到虚拟内存后却会被无视(或清除或挪作他用),原因是程序编译连接时需要确定运行时的一些基本设施状态,比如堆栈底的地址,这些基础设施往往紧跟着主程序结束内存映射的地方(或者进行某种字节对齐后的地方)占用内存,以WII上某个游戏为例:
li %r0, 0
li %r3, 0
li %r4, 0
li %r5, 0
li %r6, 0
li %r7, 0
li %r8, 0
li %r9, 0
li %r10, 0
li %r11, 0
li %r12, 0
li %r14, 0
li %r15, 0
li %r16, 0
li %r17, 0
li %r18, 0
li %r19, 0
li %r20, 0
li %r21, 0
li %r22, 0
li %r23, 0
li %r24, 0
li %r25, 0
li %r26, 0
li %r27, 0
li %r28, 0
li %r29, 0
li %r30, 0
li %r31, 0
lis %sp, -0x7FBD # 0x8043FF20
ori %sp, %sp, -0xE0 # 0x8043FF20
lis %rtoc, -0x7FBD # 0x80433360
ori %rtoc, %rtoc, 0x3360 # 0x80433360
lis %r13, unk_8042F980@h
ori %r13, %r13, unk_8042F980@l
blr这段指令其实是所有WII程序入口点里最先要执行的,CPU级别初始化基本运行环境,其中r13资料中说为系统线程ID,这里不关心,rtoc其实是r2寄存器,资料说是内容表,系统调用时候包含调用号,这里不关心,最后剩下的sp其实是r1寄存器,为栈顶指针,也是这三个非0寄存器中位置最高得一个,这里可以理解为设置栈底。这些值都在编译连接时确定。
几乎在所有WII游戏中,都是用大小为8K的堆栈,在全程序指令中,栈底地址和栈顶地址(栈底地址+8K)都会出现4次,共8次,将这8次调用中的地址同时加上同样的一个数字,即可将堆栈空间向堆方向移动,空出来的区域就可以放自己的指令和数据了。(似乎大多数系统都是这样,栈在低地址向下生长 堆在高地址向上生长,有可能堆底地址就是贴着空栈的栈顶指针的,总之不会交叉,印象中More Effective C++书中使用过此法确定C++对象创建于堆上还是栈上)
试验时将两个游戏的堆栈空间都移动了65K,游戏正常运行,新增加空间在生存期内永不被改写,所以说不必担心新加入指令会覆盖掉原这片区域的数据而无法执行程序,毕竟任何一个程序开发者都不是猿人,他不可能背下来一个每时每刻变化的内存使用魔法数字表然后生猛的写在自己的程序里,也是通过编程语言 或操作系统分配或定位到一个地址,要不冲突的工作只需大家都按照既定的标准和规则走就行了。
NDS上的主程序arm9.bin没有过多考察,主要是还不能拿IDA看,但感觉也是类似的情况,程序指令和静态数据编译链接完了都在其中,基础设施的初始化如果得到修改应该可以获得大量新指令的执行可能,毕竟在入口时直接调C库或API内存申请函数都不靠谱,可运行环境还没跑起来,DQ9已经有同学破了,相信卡死问题的解决不会太远,arm9程序指令插入这个课题有兴趣的朋友可以研究一下。
-
9号晚出现破解方法,迅速传播到国外论坛,夜间由WiiPower开发的NEOGAMMA抢先在软件内部加入内存补丁,之后wiipower知会了两位自制软件的开发者,于是USBLOADER GX和openWIIFlow其次支持直接玩原版ISO。
之后几天,硬改机玩家和全部USBLOADER玩家都可以正常游戏了,但软破玩家直读光盘仍无法游戏。
最后符合事前预测,Waninkoko大神现身在前天或昨天放出了cIOS rev15,软破玩家使用这个驱动就可以正常游戏了。
下午和ztabris以及院长三人联了会新超马,这游戏多人模式的确是够欢乐的……
PS:wiipower同学开发的neogamma(俗称绿巨人)软件很不错,推荐一下,我现在把其他loader 都删了,这个loader可以从光盘 硬盘 SD卡启动游戏,并具备调试器模块。
====
背景音乐为Hey Mickey,近期WII舞蹈游戏 大家的拉拉队2 里觉得这首歌很好听,之前的同类型游戏快乐组舞属于日式风格,判定也更难些,而这个游戏是欧美舞曲风格的,并且支持双手模式以及三种难度选择。
对于常坐在电脑前的人来说,玩玩这种游戏出出汗运动运动很合适,似乎很久没爬山了和长时间玩DDR了。
-
中午起床后在TGBUS论坛发现WII 欧版新马里奥兄弟刚刚偷跑,但是USBLOADER无法加载,软改光盘引导似乎也不行,似乎只有硬改机更新IOS53或模拟器才能成功加载游戏,但即便运行成功也会不定时黑屏。
看来任天堂对WII破解如今大开门户的现状也不是无动于衷,终于也开始搞点手段了。
游戏ISO中有一个文本文件,内容为:
d:\home\Project\WIIMJ2D\EU\PRD\RVL\bin\d_profileNP.plf
d:\home\Project\WIIMJ2D\EU\PRD\RVL\bin\d_basesNP.plf
d:\home\Project\WIIMJ2D\EU\PRD\RVL\bin\d_enemiesNP.plf
d:\home\Project\WIIMJ2D\EU\PRD\RVL\bin\d_en_bossNP.plf
很明显这是开发时的遗留文件,根据RVL开发包的文档,PLF是WII上使用可重定位模块系统(类似WIN32的DLL机制)过程中的一个中间产物,
也就是一个部分连接的ELF程序文件,最终存在于WII光盘上的应该是rel文件。
ISO的确有类似的4个文件:
d_basesNP.rel.LZ
d_en_bossNP.rel.LZ
d_enemiesNP.rel.LZ
d_profileNP.rel.LZ
出于未知原因,这些动态库都以0X11字节开头的LZ77算法压缩过
于是可以在源代码层面YY这部分指令执行过程如下:
DVDFileInfo relInfo;
DVDOpen("/d_basesNP.rel.LZ",&relInfo);
u32 length = OSRoundUp32B(DVDGetLength(&fileInfo));
void* lzData =OSAllocFromArenaHi(length, 32);
DVDReadPrio(&relInfo,lzData,(s32)length,0,2);
DVDClose(&relInfo);
u32 uncmpressLength = OSRoundUp32B(CXGetUncompressedSize(lzData));
OSModuleHeader* relModule = (OSModuleHeader*)OSAllocFromArenaHi(uncmpressLength, 32);
CXUncompressLZ(lzData,relModule);
void* bss = OSAllocFromArenaHi(relModule->bssSize, 32);
OSLink(&relModule->info, bss);
((void (*)(void)) relModule->prolog)();
卸载时调用:
((void (*)(void))relModule->epilog)();
OSUnlink(&relModule->info);
其中prolog和epilog都是为了支持C++特性而准备的,简单的说就是帮助全局对象构造或析构.
以上纯属YY,如有雷同,其实很正常。另外发现dol的IDA插件对程序入口点的识别貌似对每个游戏都提前了0x10个字节,不是什么严重问题就不知会插件作者了,如果有同好路过可以参考。
作为WII上的头号黑客,Wannikoko许久没有大动作之后不知会否再次出山,IOS249如果针对这个游戏出新版就标志着新一轮的战斗,已经开始。 -
终于实现了系统字库重定向,在usbloader和gecko os均测试通过。
本方案基于以下事实:
1.内存0x80001800起168个字节可写(其实可用空间很大,但是0x800018a8起被gecko os占了)并且不会被清除或改写。
2.dol文件头中保存着程序入口点地址,添加一个.text节(大小32字节对齐)可以映射到较低内存空间。
3.商业游戏使用RVL开发包,那么就一定会使用RVL开发包中提供的光盘读取函数,只要找到这些函数的地址,就可以自行调用,来读取光盘上的文件
于是修改DOL文件,选择在内存空间0x804E9A80处开始(这个地址的选择的方法就是参考其他.text .data .bss节中最高地址的那个结束地址),开始映射176+112字节数据。
前176字节数据为保存到0x80001800处的stub数据,多出来的8个字节是为了内存对齐。
unsigned char codeStub[] ={
0x2F,0x66,0x6F,0x6E,0x74,0x2F,0x62,0x6D,0x70,0x66,0x6F,0x6E,0x74,0x2E,0x62,0x72,
0x66,0x6E,0x61,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x94,0x21,0xFF,0xE0,0x7C,0x08,0x02,0xA6,0x90,0x01,0x00,0x24,0x90,0x61,0x00,0x1C,
0x3C,0x60,0x80,0x00,0x60,0x63,0x18,0x00,0x38,0x83,0x00,0x14,0x48,0x19,0xA1,0xED,
0x3C,0x60,0x80,0x00,0x60,0x63,0x18,0x14,0x3C,0x80,0x80,0xEF,0x60,0x84,0xAD,0x00,
0x3C,0xA0,0x00,0x11,0x60,0xA5,0x08,0x00,0x38,0xC0,0x00,0x00,0x38,0xE0,0x00,0x02,
0x48,0x19,0xA7,0x41,0x80,0x01,0x00,0x24,0x80,0x61,0x00,0x1C,0x7C,0x08,0x03,0xA6,
0x38,0x21,0x00,0x20,0x4E,0x80,0x00,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
};后112字节数据为新的程序入口点所需要的代码和数据,最后8字节同样为了字节对齐而出现:
unsigned char codeEntry[] ={
0x94,0x21,0xFF,0xE0,0x93,0xE1,0x00,0x1C,0x7C,0x3F,0x0B,0x78,0x38,0x00,0x00,0x00,
0x90,0x1F,0x00,0x08,0x48,0x00,0x00,0x3C,0x80,0x1F,0x00,0x08,0x3D,0x20,0x80,0x00,
0x7C,0x00,0x4A,0x14,0x30,0x00,0x18,0x00,0x81,0x3F,0x00,0x08,0x3D,0x29,0x80,0x4F,
0x39,0x29,0x9A,0x80,0x89,0x29,0x00,0x00,0x55,0x29,0x06,0x3E,0x7C,0x0B,0x03,0x78,
0x99,0x2B,0x00,0x00,0x80,0x1F,0x00,0x08,0x30,0x00,0x00,0x01,0x90,0x1F,0x00,0x08,
0x80,0x1F,0x00,0x08,0x2F,0x80,0x00,0xA7,0x40,0x9D,0xFF,0xC0,0x39,0x7F,0x00,0x20,
0x83,0xEB,0xFF,0xFC,0x7D,0x61,0x5B,0x78,0x4B,0xB1,0xA4,0xA4,0x00,0x00,0x00,0x00
};然后修改dol入口点为0x804E9A80+176 = 0x804E9B30。
也就是codeEntry这片数据,这是用gekko-gcc编译出来的目标文件,C语言代码为:
void abc()
{
int i=0;
for(;i<168;++i)
{
*(unsigned char*)(0x80001800+ i) =*(unsigned char*)(0x804E9A80+ i);
}
}唯一不同的原本最后一句汇编指令是blr,这里手工修改为b 0x8000403c,指令进入原程序入口点。
可以看到此函数只是简单的将stub数据拷贝到期望内存,一旦指令进入原入口点,很快映射的176+112字节数据
就被清除了。
下面的任务交给stub完成,stub数据由三部分组成:
1.字库文件绝对路径,这里是/font/bmpfont.brfna。
2.DVDFileInfo结构,占用60字节,其实这个空间完全可以从模拟的堆栈上分配,而不占用宝贵的168字节stub空间,这里恰好够用就这样吧。
3.读取/font/bmpfont.brfna文件数据到系统字库内存地址的汇编代码本身:
stwu r1,-32(r1)
mflr r0,
stw r0,36(r1)
stw r3,28(r1)
lis r3,-0x8000
ori r3,r3,0x1800
lis r4,-0x8000
ori r4,r4,0x1814
bl 0x8019BA58 //BOOL DVDOpen(const char* fileName, DVDFileInfo* fileInfo);lis r3,-0x8000
ori r3,r3,0x1814
lis r4,-0x80ef
ori r4,r4,0xad00
lis r5,-0x11
ori r5,r5,0x0800
li r6,0
li r7,2
bl 0x8019BFD0 //s32 DVDReadPrio ( DVDFileInfo* fileInfo, void* addr, s32 length,s32 offset, s32prio );
//lis r3,-0x8000
//ori r3,r3,0x1814
//bl 0x8019BD04
lwz r0,36(r1)
lwz r3,28(r1)
mtlr r0
addi r1,r1,32
blr注释掉的那三句用来执行DVDClose API关闭文件,这里没地方了省略也没啥问题。
powerpc 用r1模拟栈顶指针,按照惯例r0寄存器保存链接寄存器的值,这里为了函数流程的正确不仅作了这些操作,还保存了一下r3,r3作为函数返回值肯定是会被使用的,如果修改恐怕会引起不良反应。
用IDA查找反汇编找到OSLoadFont API的最后一句汇编代码,blr,将其修改为b 0x80001850,即在游戏初始化加载系统字库后立刻对同一片内存再次刷入新字库数据。
==============
终于找到了RVL开发包的正确用法,采取和今年上半年为公司逆向微软的DirectUI界面库同样的思路,可以调用或者拦截很多基础API,而如果能巧妙利用stub区域,比如stub代码只负责读取文件并将里面的数据当做代码来执行的话,可破除很多限制,这很有趣。
本系列日志结束,也不算浪费那刻费了的10张光盘。
-
在用调试器运行时上传字库数据测试成功后,如何正常运行游戏也能修改字库的问题目前看起来仍旧之差一步,可能有些思路还要再仔细整理一下。
经过使用ida反汇编代码比对,已经确定目标游戏通过
u32 OSLoadFont (OSFontHeader* fontData, void* temp);char* OSGetFontTexture(char* string, void** image, s32 *x, s32 *y, s32* width);
来使用系统字库。
在源代码层面游戏先调用OSAlloc分配内存,作为OSFontHeader结构指针,调用前者,
第2那个参数只是作为函数内部使用,调用完毕后可释放。
OSFontHeader结构如下:
typedef struct OSFontHeader
{
u16 fontType; // font type
u16 firstChar; // the first character code defined in the font
u16 lastChar; // the last character code defined in the font
u16 invalChar; // the font code to be substituted for invalid characters
u16 ascent; // the ascent (units above the base line) of characters
u16 descent; // the descent (units below the base line) of characters
u16 width; // the width of the widest character
u16 leading; // the leading (space) between rows
u16 cellWidth; // the cell (a single character) width in a sheet
u16 cellHeight; // the cell (a single character) height in a sheet
u32 sheetSize; // the size of a sheet in bytes
u16 sheetFormat; // the texture format of a sheet (GX_TF_*)
u16 sheetColumn; // the number of characters in a row
u16 sheetRow; // the number of lines in a sheet
u16 sheetWidth; // the sheet width (texture width)
//32B
u16 sheetHeight; // the sheet height (texture height)
u16 widthTable; // offset to character width table (const)
u32 sheetImage; // offset to sheet image
u32 sheetFullSize; // the size of all sheets
u8 c0; // font color table
u8 c1;
u8 c2;
u8 c3;
// 48B
} OSFontHeader;- 在调试器中在字模位置前翻几页,可以发现:

- 可对号入座,如
firstChar为0x8140lastChar为0x9872,参阅shift-jis码表和字库得到证实。(8140= 9872=腕)
之后游戏调用OSGetFontTexture获取一段字符串所应对应的纹理图片,搞到之后处理渲染。
在目标游戏程序中只有一处调用了此函数:
.text1:8031799C addi %r3, %sp, 8 # Add Immediate
.text1:803179A0 addi %r4, %sp, 0x18 # Add Immediate
.text1:803179A4 addi %r5, %sp, 0x14 # Add Immediate
.text1:803179A8 addi %r6, %sp, 0x10 # Add Immediate
.text1:803179AC addi %r7, %sp, 0xC # Add Immediate
.text1:803179B0 bl OSGetFontTexture # Branch
ppc汇编使用r3开始左起传递参数,同时r3寄存器作为返回值,sp其实是r1类似x86系列的栈顶指针,
(但貌似并没有堆栈基址指针),总之这段代码看起来就是函数内部创建了一个结构体,
此结构8字节偏移为当前字符串,0x18字节偏移为纹理二重指针,0x14 0x10 0xc字节偏移内容可对号入座解释。
目前问题在于,如果只是期望在入口处抢先执行一些指令是可以的,但是字库替换动作有时机要求和数据过大的
问题,
如果在入口处直接替换对应字库内存,则初始化时会出错,看来那片内存开始使用过。那么考虑先放到别处,
在合适时机读取,
问题就是没有合适的地方,几乎所有内存可用区域在初始化后都会清0,
而0x90000000开始的64M外存也被游戏所使用,
缓存些图片纹理什么的。
甚至考虑过用GCT金手指锁死对应内存实现字库替换这种搞笑的临时方案,但纳闷的是似乎金手指功能并没有生效,
原因不明,贴个链接:http://wiird.l0nk.org/codetypes.html
目前寄希望于对这些过程的再考察,或利用其他RVL开发包提供的系统调用加载其他位置的数据? -
WII现在主程序是DOL格式。
该格式如下:
start end size description 0x0000 0x001B Text[0..6] sections File Positions 0x001C 0x0047 Data[0..10] sections File Positions 0x0048 0x0063 Text[0..6] sections Mem Address 0x0064 0x008F Data[0..10] sections Mem Address 0x0090 0x00AB Text[0..6] sections Sizes 0x00AC 0x00D7 Data[0..10] sections Sizes 0x00D8 0x04 BSS Mem address 0x00DC 0x04 BSS Size 0x00E0 0x04 Entry Point 0x00e4 0x1c unused 0x0100 Start of sections data (body) 头部用C结构描述为:
typedef struct dol
{
int nOffsetText[7];
int nOffsetData[11];
int nAddrText[7];
int nAddrData[11];
int nSizeText[7];
int nSizeData[11];
int nBssAddr;
int nBssSize;
int nEntryAddr;
}dol;注意ppc gekko处理器是大头的,在win上写程序要留意字节序问题。
两个字节序转换宏:
#define M2I(a) (((DWORD)a>>24) + ((((DWORD)a<<8)>>16) & 0xff00) + ((((DWORD)a<<16)>>8) & 0xff0000) + ((DWORD)a<<24))
#define D2I(a) ((((WORD)a>>8)&0x00ff) + ((((WORD)a<<8)) & 0xff00))要在WII的可执行程序中植入自己的代码,可先用gekko-gcc编译写好的C语言代码,然后objcopy -o binary得到机器码,在最后一个已经使用的.textX节头追加自己的新节,文件偏移就在原文件尾部,内存起始映射地址在原程序映射的内存结束处即可。
要拦截函数,可在该处修改该指令为bl 到新指令内存地址。
对于字库重写这个过程,由于调试器的一些未知问题,并没有找到写字库的函数,而是寻找到一个字库写入内存后一定会流经的指令,将其定位到新指令,而对主程序的数据植入不仅新指令本身,还有新字库数据,新指令做简单的内存拷贝即可。
--------------------------17号更新---------------------
实验发现,如果一个节在较前开始的虚拟内存开始映射,那么在指令执行到入口点时,内存的确已经被映射,但是这片内存有被重写的可能,对于我所选择的游戏更甚,其画面帧缓存跨越了指定的映射地址,而如果映射内存地址选择过高,比如从堆头地址向下计算,那么映射不会成功,程序无法执行。
对于这种情况,拟定一个方案待尝试:
新字库数据映射到较低内存地址,修改程序入口点不在0x8000403c处而到尾随新字库数据后的新指令内存地址处,内存拷贝,然后一个b指令跳到0x80000403c处执行,在这之后,按原程序逻辑会立刻读取ROM字库数据到内存,下面要做的就是找到这个动作指令的位置,将它干掉,这不是无的放矢的。
注意到,RVL库是可以下载到的,并且现在得商业WII游戏基本都是用这个库开发的,又可以预测这个库中理应提供了对系统字库的支持,于是可以逆向RVL库中的加载字库部分代码,在目标游戏程序的汇编代码中暴力搜索,通过结构化比对确定位置。
举例来说,RVL库的release版osfont.a静态库中
ReadFont函数的汇编代码基本和发行版游戏的一致,其他函数可能版本变化导致略有不同。
用在线调试器将 0x80191F48直接blr,函数返回,
游戏中所有需要显示文字的地方均变为留空,说明生效 -
目前这个平台上还没人使用系统字库的游戏被汉化,这里是有技术瓶颈的,主要问题在于既然字库不存在于光盘中,也没有在线调试器,似乎有点无从下手,但其实WII上有个usb gecko,虽然停产了但是应该可以搞到2手的,可惜官网挂了很多资料拿不到了。
有了debugger之后,我选择将自0x80000000起的24m物理内存下载到电脑,然后用CT2观察,终于在某处发现了普通4BPP格式的字库,使用色盘模式,16灰度,黑底白字,tile块为8*8大小,当每行显示64个tile块时,24*24点阵的字库就看到全貌了。
挂起游戏,对该区域上传无效内存观察,发现游戏中显示文字的时候的确显示出了无意义图快,证明系统字库就是加载到了这个地址给游戏使用。
之后要解决的问题就是生成新字库,和把新字库写到这个内存区域。
这里当做两个问题考虑,问题一可在PC端进行,生成数据。
问题二在WII端进行,需要在WII的游戏执行程序里添加代码,方案可行,有待实现。
晚上试了下兼容格式的字库生成,还算顺利,就是涉及数据在文件里的位置计算头大了一阵,感觉最近一段时间不锻炼脑力要生锈了。
基本思路:
CreateFont生成24*24的字体,然后在内存里创建个兼容DC,在DC上设置背景色为0,前景色为0xf,然后在这个DC上输出文字,之后遍历这个DC上的像素,纵向逐个遍历,横向两个一组遍历,每组数据组合为一个字节存放到文件里。

如图,每个字模由9快TILE组成,每个TILE都是8*8的,
那么对于第一个字模:
设i为列的索引,j为行的索引,
则每两个点(i,j),(i+1,j)在字库文件里的位置应按如下方法计算:
j%8*4+i%8/2+i/8*32+j/8*2048
-
ISO拆/打包使用WIIScrubber,1.31支持批量操作。
游戏的主程序一律为main.dol,命令行工具DolTool可以转换ELF<->DOL,得到对应的ELF,可使用IDA静态反汇编为POWERPC750的汇编指令。
另外HyperIris写了个IDA5.2的插件Nintendo GameCube Gekko CPU Extension 2007.12.23,在代码识别上可参考使用。
要调试WII游戏除了在模拟器上加参数进行外,更多的需要上真机调试,也就是使用USB GECKO连接PC,目前设备已停产,正在寻找。
任天堂若干年前公布了新开发环境,名为RVL_SDK,通过对若干近期发行的WII游戏进行逆向工程,可以确定这些游戏就是用这个开发包制作的。
此开发包可在http://rapidshare.com/files/17920409/NINTENDO_Revolution_SDK_2.1_Wii-SYNDiCATE.rar这里获得。
开发包有一些实用工具可以处理加密的文件格式,如TexConv,用来将TGA贴图转换为TPL格式,反向转换可以使用GCUBE模拟器附带的TPLX.exe,darchD.exe,用来处理arc目录结构包,3D贴图包格式brres可到TGBUS的ACG汉化组区查找TPU发布的一个转换工具,字库格式brfnt或brfna类似NDS的字库格式,TGB工作室的YEYEZAI曾经发布过相关文档,视频方面开发包里提供了对THP格式的生成编辑工具,要在WINDOWS上浏览THP视频可参考http://www.amnoid.de/gc/的thpplay 1.5,带源码。音频RSTM格式没有过多考察,猜测开发包里的工具可以处理。
对于头字节为 0x11的LZ77压缩数据包,可使用工具crystaltile2在09年3月后的版本进行压缩/解压缩。
可参考:
http://wiibrew.org/wiki/Assembler_Tutorial
http://hitmen.c02.at/files/yagcd/yagcd/
http://wiibrew.org/wiki/Opening.bnr
http://wiibrew.org/wiki/Benzin
http://wiibrew.org/wiki/Data_Containers
http://wiibrew.org/wiki/Wii_Animations
-
本网银助手由于开发周期极短,UI构建并没有采用的传统的C++编码方式,而是采取外层窗口套IE控件再套activeX控件的方式,记得以前在BLOG里提到过,作为两个极端,用IE控件开发界面和无窗口控件都是当前热点.
对于一个安全性要求高的软件,加载的网页源代码暴露是不可忍受的,另外会给人一种不专业的感觉,也不便于升级,将其加密后打包到一个PE文件里是迫在眉睫的需要.
(但事实上这种意义上的加密就像是马其诺防线不堪一击,如果是一定要做坏事的人,运行时发个消息就能取得经过列集的webbrowser相关指针)
正向做这件事需要读取每个资源的数据,解密,然后再想办法传递给IE控件,对目前的情况来说除非放到2期不然基本不用考虑了,周内争取能够返京.
于是晚上使用ie4起支持的res协议结合API拦截实现了一个版本:
打包器端调用UpdateResource系列API修改PE文件的资源,在添加资源前先对其数据加密,库是同事提供的,不过根据我的需要加写了两个内存to内存的版本,另外在头部追加了8个字节记录加密前和加密后的数据,为解析器提供便利.
解包器端,用res:\\文件路径/网页资源名 来导航IE控件.
IE控件按照其内部逻辑,会调用资源加载系列API:FindResource
LoadResource
LockResource
SizeofResource拦截这4个API,抢先读取对应的加密后数据然后解密后将网页或XML JS图片等数据返回给IE控件,于是IE控件渲染页面,测试通过.
-
项目做秃了,于我有关的原因是状态没有调整好,基本处于不作为模式,先自我批评一下.
现在有个棘手问题,网银助手有一些不完全可知的需要管理员权限才能执行的操作,已知的是安装需要UAC的程序(目前使用从工行的助手里拿过来的 CreateProcess法,该方法对于主进程非管理员权限而子进程要弹出UAC的情况不能正确执行,ShellExecuteEx法可以正常开启进 程,但是如果有多个这样的程序要作为子进程启动,那么出现多次UAC窗口让用户确认又是个无法忍受的现象),注册com组件之类.
那么让助手本身启动时申请管理员权限似乎是个好办法,但问题也很是啼笑皆非,IE一些选项设置包括信任站点,允许的弹出窗口等,是每个用户相关的, 那么如果当前系统是一个普通用户登陆的,以管理员权限启动程序,助手会将IE设置生效到该管理员用户旗下,而不是生效给当前普通用户,这是一个令人感到沮 丧的结果.
PM建议助手作为两个2个可执行程序启动,一个开启UAC 一个以当前用户身份执行,可能与权限有关的操作交给前者去做,这个方案理论可行,但是以目前项目实际情况来看并不靠谱,首先由于助手程序高度可配置,哪些 操作可能与权限有关并不好确定,其次双进程模式存在某进程意外终止的可能,即便忽略该问题,双进程模式涉及的进程间消息传递以及对应代码转移需要的工作量 可能都不是当前能承受的,毕竟按原计划9号大家就该回北京了.
一怒之下,晚上采取了一个很鲁的方式做掉这个问题:
助手程序仍旧为管理员权限运行,启动后立刻枚举所有进程,找到windows外壳EXPLORER.EXE,取出它的进程令牌,通过令牌取该进程的 用户名,结合该信息调用LoadUserProfile获得该用户对应的注册表USER主键句柄,拦截RegOpenKeyEx,当注册表主键为 HKEY_CURRENT_USER时重定向到取得的那个USER主键句柄即可.
也就是说此法将本进程所有要读写当前用户注册表信息的地方都重定向到系统登陆用户的注册表里.
发现两个要注意的地方:
1.枚举进程时要注意某一进程是否为当前用户所有,方法是调用ProcessIdToSessionId把进程ID转为SessionID,再将此ID与WTSGetActiveConsoleSessionId获得的当前ID比对,相同则为当前用户的进程.
2.获取进程的令牌时,期望权限传TOKEN_QUERY即可,贪多嚼不烂.
-----8月14日更新:
explorer.exe进程可能有多个,可在枚举进程时取会话ID,WTSGetActiveConsoleSessionId不支持WIN2000,可用WTSQuerySessionInformation WTSSessionId替代,另外按照MSDN上的表述,LoadUserProfile需要的token应具有 TOKEN_QUERY,TOKEN_IMPERSONAT,TOKEN_DUPLICATE访问权限,但既然如此就用不到拦截API了,直接使用API ImpersonateLoggedOnUser实时模拟该token身份,操作完用户相关的数据,调用RevertToSelf返回原权限即可.
然而这两种方案在PM的WIN7RTM和同事的VISTA上均不明所以的失败,于是转而使用第三种方案:
注册表里HKEY_CURRENT_USER主键其实是HKEY_USERS主键里某项的映射,该项为用户的SID字符串描述.
通过GetTokenInformation获得token归属的用户sid,然后通过函数
BOOL ConvertSid(PSID pSid,LPTSTR TextualSid,LPDWORD lpdwBufferLen)
{
PSID_IDENTIFIER_AUTHORITY psia;
DWORD dwSubAuthorities;
DWORD dwSidRev = SID_REVISION;
DWORD dwCounter;
DWORD dwSidSize;
if(!IsValidSid(pSid))
{
return FALSE;
}
psia = GetSidIdentifierAuthority(pSid);
dwSubAuthorities = *GetSidSubAuthorityCount(pSid);
dwSidSize=(15 + 12 + (12 * dwSubAuthorities) + 1) * sizeof(TCHAR);
if (*lpdwBufferLen < dwSidSize)
{
*lpdwBufferLen = dwSidSize;
SetLastError(ERROR_INSUFFICIENT_BUFFER);
return FALSE;
}
dwSidSize = wsprintf(TextualSid, TEXT("S-%lu-"), dwSidRev);
if ((psia->Value[0] != 0) || (psia->Value[1] != 0))
{
dwSidSize += wsprintf(TextualSid + lstrlen(TextualSid),TEXT("0x%02hx%02hx%02hx%02hx%02hx%02hx"),
(USHORT)psia->Value[0],
(USHORT)psia->Value[1],
(USHORT)psia->Value[2],
(USHORT)psia->Value[3],
(USHORT)psia->Value[4],
(USHORT)psia->Value[5]);
}
else
{
dwSidSize += wsprintf(TextualSid + lstrlen(TextualSid),TEXT("%lu"),
(ULONG)(psia->Value[5]) +
(ULONG)(psia->Value[4] << 8) +
(ULONG)(psia->Value[3] << 16) +
(ULONG)(psia->Value[2] << 24));
}
for (dwCounter=0; dwCounter < dwSubAuthorities; dwCounter++)
{
dwSidSize += wsprintf(TextualSid + dwSidSize, TEXT("-%lu"),*GetSidSubAuthority(pSid, dwCounter));
}
return TRUE;
}
获得注册表里对应的SID描述,然后仍旧拦截注册表API RegOpenKeyEx在主键为HKEY_CURRENT_USER时矫正为HKEY_USERS,在subkey前添加SID描述\\ 即可.
-
#include <Wincrypt.h>
#pragma comment(lib,"Crypt32.lib")
bool ImportUserCert(const char* szCertFilePath)
{
FILE* file = fopen(szCertFilePath,"rb");
if(!file) return false;
fseek(file,0,SEEK_END);
long certificateLen = ftell(file);
unsigned char *certificate = new unsigned char[certificateLen];
fseek(file,0,0);
fread(certificate,1,certificateLen,file);
fclose(file);
HCRYPTPROV hProv = 0;
HCRYPTKEY hUserKey = NULL;
PCCERT_CONTEXT pCertContext = NULL;
HCERTSTORE hCertStore;
hCertStore = CertOpenStore(CERT_STORE_PROV_SYSTEM_W,0,0,CERT_STORE_OPEN_EXISTING_FLAG|CERT_SYSTEM_STORE_CURRENT_USER, L"Root");
if(hCertStore == NULL)
{
CryptReleaseContext(hProv,0);
delete [] certificate;
return false;
}
bool bRet = false;
if(pCertContext = CertCreateCertificateContext(X509_ASN_ENCODING,certificate,certificateLen))
{
if(CertAddCertificateContextToStore(hCertStore,pCertContext,CERT_STORE_ADD_REPLACE_EXISTING,0))
{
bRet = true;
}
CertFreeCertificateContext(pCertContext);
}
CertCloseStore(hCertStore,CERT_CLOSE_STORE_FORCE_FLAG);
delete [] certificate;
return bRet;
}
但当安装到Root区域(受信任的根证书颁发机构)时,会弹出安全确认对话框.
如果项目中需要暴力安装,可以直接写注册表.
Software\Microsoft\SystemCertificates\Root\Certificates各指纹子项即为证书所在. -


手法比较挫,这个工具写来用于观察任意程序界面上的无窗口文本框是否为RichEdit,支持通过dll名为richedit20.dll或者msftedit.dll创建出的控件,如图所示,此类控件都被黑色矩形包围,当鼠标在每个黑色矩形内按下左键视键盘状态进行不同操作:
如果CTRL键在按下状态,则弹出如上的对话框显示当前内容.
如果SHIFT键在按下状态,则修改当前内容为Let's go.QQ2009只是用来测试的例子,并非针对其进行.
纳米盘:http://d.namipan.com/d/3a2a34a2134cbe5706289c5d438d14c4c6bc65c7007c0000
DLL注入器这有一个:http://bbs.pediy.com/showthread.php?t=20189