Skip to content

Instantly share code, notes, and snippets.

@Inori
Created August 11, 2018 13:20
Show Gist options
  • Save Inori/6cf94bcd1b1c50ff4cd44ac18133566d to your computer and use it in GitHub Desktop.
Save Inori/6cf94bcd1b1c50ff4cd44ac18133566d to your computer and use it in GitHub Desktop.
首先说明LBA保护是什么。讲到这里就插入一点背景知识。
PSP的UMD数据基于ISO9660格式存储。(关于ISO9660的具体信息请查阅相关资料)
众所周知,ISO9660的数据存储基于逻辑块(Logical Block)。数据必须以逻辑块为单位对齐。
其大小可在VolumeDescriptor结构的logical_block_size中获得。
在PSP上,这个值是0x800,也就是2048字节。
sony为PSP的isofs驱动(在用户层一般由disc0访问)提供了一种特殊的访问模式。
这种模式就是LBA,也就是Logical Block Addressing,即逻辑块寻址模式。
可以使用户通过提供逻辑块编号和文件长度,直接访问相应位置的数据,而不需要沿ISO数据结构下溯。
(事实上,下溯的结果也只是这两个数据而已。)
但事实上,提供这种方法的主要目的并不是节约寻址时间。
由于UMD的读取速度强差人意,对于需要频繁读取数据的游戏,将数据平均的分散在1.8G的存储空间内将有助于减少平均读取时间。
——这才是提供LBA的目的。
出于这种目的,很多游戏开发者也会选择使用LBA。
但同时,LBA的使用也对游戏数据的修改造成了影响——自然,这种影响是在开发的时候不需要考虑到的。
在修改数据后重新对游戏打包,一般都会改变数据的逻辑块排列和大小。这种情况下,访问将会失效。
这种情况,对破解者来说就像是开发者使用的一层保护措施。这也就是“LBA保护”一词的来源。
在下面的文章中,笔者也将继续使用LBA保护这个称呼,尽管并不贴切。
作为基础知识,自然也需要了解Sony为LBA提供的方法。
一般使用下列串进行访问:
"%s:/sce_lbn0%x_size0x%x"
换句话说也就是 sprintf(pszLbaString,"%s:/sce_lbn0%x_size0x%x",pszDriverName,dwLBNumber,dwFileSize);
(注意,一般来说是这个格式,但也有其它的情况。但sce_lbn这个特征串是不会变的)
举例而言,如果一个文件的起始逻辑块为0x120,长度为0x4800,那么将disc0:/sce_lbn0120_size0x4800作为文件名即可。
IO句柄打开后,一切操作都和普通文件一样了。
例程:
int fd=sceIoOpen("disc0:/sce_lbn0120_size0x4800",PSP_O_RDONLY,0777);
int dwSize=sceIoLseek32(fd,0,SEEK_END);
sceIoClose(fd);
下面进入正题,也就是LBA保护的破解办法。举例的时候将以绝体绝命都市3(ULJS-00191)这个游戏为例。
将ISO Unpack再Repack以后无法运行——这是LBA保护的最明显特征。一般来说,有2种解决方法。笔者将分别进行讲解。
一:LBA表修复
出于编程简便和模块化的考量,大多数开发者在编写游戏时仍将使用文件名作为调用依据。
(极少部分游戏除外,例如空之轨迹系列的一部分文件名就是以LBA形式硬编码在BOOT里的,
这样的数据除了构建一个文件名的替换表以外没有特别合适的处理办法)
而这种情况下,一般会在ISO里放一个文件,用于记录每个文件的LBA值、大小和文件名的对应关系(部分游戏会把这个表和PackList合起来)。
在游戏运行时,首先加载这个表,之后其它的文件访问将通过查找这个表来进行。这个表称为LBA表。
通常LBA表都是单独加载的,因此它在BOOT内的文件名都是以普通方式记录,或至少是一个单独的LBA文件名。
只要生成一个新表,就可以让游戏正常运行。那么怎么找到这个文件呢?
对于采用了LBA表的游戏,BOOT内记录的普通文件名会很少,因此只要逐个筛查这些普通的文件名("disc0:/PSP_GAME/"开头的)即可,检查方法下述。
如果是有一些经验的话,也可以先去搜索特征串(sce_lbn),
基本上用LBA保护的游戏都一定要把这个串作为sprintf的格式串,而表的名称应该就在.rdata区内其所在位置的附近。
(因为同属类似的功能,代码里使用处应该也相近,而编译器在编译时会按照顺序来存储这些串)
在找到一个文件之后,检查其是否记录有大量文件名,并对一些LBA值和文件大小值进行搜索。这些值用UMDGEN先行导出。
如果存在,那么基本可以认定它就是LBA表。在本例中,LBA表是PSP_GAME/USRDIR/INSTALL/HEADER.BIN。
之后是分析这个表的格式。这一步属于破解范畴,不再赘述。注意“文件名-LBA-文件大小”的对应关系即可推出。
下面给出HEADER.BIN的格式:
typedef struct _LBA_TABLE_HEADER
{
int dwElementNum; //文件数
int dwOffsetStringTableStart; //文件名表位置
int dwReserved0; //-1
int dwReserved1; //-1
}Lba_Table_Header;
typedef struct _LBA_ELEMENT
{
int dwNameOffsetInStringTable; //文件名在文件名表中的位置
int dwAccumulateLengthValue; //累计的一个长度和
int dwFileLength; //文件长度
int LbaStart; //起始逻辑块
int dwCheckSum; //数据校验和
}Lba_Element;
有了格式之后,编写程序,将新的数据对应的LBA表重新生成,并替换即可使游戏正常访问数据。
尽可能将所有数据的含义都解明,不明的数据可以进行动态调试。例如此例中,校验和建议也要更新。
这种方法的好处是稳定易行,缺点则是每次更新数据都要重新构建一个LBA表,对于需要不断调试的游戏来说过于繁琐。
二:修改程序
对于一些游戏,其LBA表格式过于复杂,或需要不断调试数据,这种情况下构建LBA表是一项较为繁琐的工作。
这种情况下,便建议采用这里将要叙述的第二种方法,也就是修改程序。由于修改后的“一劳永逸”,在可行的情况下也是推荐的方法。
这种方法对破解者的反汇编能力有一点要求,不过不是很高。对于PSP程序的反汇编,Ps2Dev等处有一些不错的教程,略去不论。
在下面的叙述中笔者将使用PrxTool作为反汇编工具。
思路介绍如下:
1.首先查找对特征串的引用,一般来说集中在一处,而且一般是用于sprintf(sprintf的目标串此处称为Dst)。
2.在sprintf之前一般是根据普通文件名(Name)获得文件长度和起始逻辑块的操作,找到这个操作。
3.将函数修改为"strcpy(Dst,Name)"这样的操作
4.检查其它对数据的检查操作
基本上就是按照这个思路进行。对反汇编来说,没有什么通用的方法,更多依靠破解者自身的耐心和分析能力。
本例具体讲解如下:
特征串位置 : 0x08BE02E0。
=> 调用 : loc_0883D570处发现。 (第一步)
相关代码段:
loc_0883D50C: ; Refs: 0x0883D4D4
0x0883D50C: 0x52600012 ''..`R'' - beqzl $s3, loc_0883D558
0x0883D510: 0x02A02021 ''! ..'' - move $a0, $s5
0x0883D514: 0x8E62000C ''..b.'' - lw $v0, 12($s3)
0x0883D518: 0x50400006 ''..@P'' - beqzl $v0, loc_0883D534
0x0883D51C: 0x02A02021 ''! ..'' - move $a0, $s5
0x0883D520: 0x8E620008 ''..b.'' - lw $v0, 8($s3)
0x0883D524: 0xAEA20018 ''....'' - sw $v0, 24($s5)
0x0883D528: 0x8E62000C ''..b.'' - lw $v0, 12($s3)
0x0883D52C: 0x10000010 ''....'' - b loc_0883D570
0x0883D530: 0xAEA2001C ''....'' - sw $v0, 28($s5)
loc_0883D534: ; Refs: 0x0883D518
0x0883D534: 0x0E20F5E1 ''.. .'' - jal sub_0883D784
0x0883D538: 0x00000000 ''....'' - nop
0x0883D53C: 0x00402021 ''! @.'' - move $a0, $v0
0x0883D540: 0x0E20F202 ''.. .'' - jal sub_0883C808
0x0883D544: 0x26A5001C ''...&'' - addiu $a1, $s5, 28
0x0883D548: 0xAEA20018 ''....'' - sw $v0, 24($s5)
0x0883D54C: 0x8EA2001C ''....'' - lw $v0, 28($s5)
0x0883D550: 0x10000007 ''....'' - b loc_0883D570
0x0883D554: 0xAE62000C ''..b.'' - sw $v0, 12($s3)
loc_0883D558: ; Refs: 0x0883D50C
0x0883D558: 0x0E20F5E1 ''.. .'' - jal sub_0883D784
0x0883D55C: 0x00000000 ''....'' - nop
0x0883D560: 0x00402021 ''! @.'' - move $a0, $v0
0x0883D564: 0x0E20F202 ''.. .'' - jal sub_0883C808
0x0883D568: 0x26A5001C ''...&'' - addiu $a1, $s5, 28
0x0883D56C: 0xAEA20018 ''....'' - sw $v0, 24($s5)
loc_0883D570: ; Refs: 0x0883D52C 0x0883D550
0x0883D570: 0x8EA6001C ''....'' - lw $a2, 28($s5)
0x0883D574: 0x3C0508BE ''...<'' - lui $a1, 0x8BE
0x0883D578: 0x8EA70018 ''....'' - lw $a3, 24($s5)
0x0883D57C: 0x26A4002C '',..&'' - addiu $a0, $s5, 44
0x0883D580: 0x0E20AF40 ''@. .'' - jal sub_0882BD00
0x0883D584: 0x24A502E0 ''...$'' - addiu $a1, $a1, 736
0x0883D588: 0x8FBF001C ''....'' - lw $ra, 28($sp)
特征串被作为第二个参数($a1)传入,可以推知将要进入的函数sub_0882BD00是sprintf。
=> 还原为:sprintf(($s5+44),pszLbaFormat,($s5+28),($s5+24));
得知$s5+44为文件名存储位置,$s5+28为LBA值,$s5+24为文件长度。同时推知$s5是一个结构指针。
前溯,发现$s5+24和$s5+28有两种获得方式:
由$s3所指向的结构中读取(loc_0883D50C),
和由sub_0883C808的返回值和传入的$a1获得(loc_0883D534,loc_0883D558)。 (第二步)
由sub_0883C808入手,发现返回值为LBA值,传入的第2参数是长度的地址。
=> 其作用为获得LBA值和长度。
又其$a0为sub_0883D784($s5)返回值,查得为$s5+44。
=> $s5+44保存的是文件名。
=> 还原为dwSize=sub_0883C808(pszFileName,&dwLba);
=> $s5+44原来存储的是普通文件名,sprintf后为LBA文件名。因此直接去除sub_0882BD00的调用。 (第三步)
(若不同,则需将其改为一个strcpy操作,strcpy函数可在上下文中试着查找)
运行游戏后发现游戏反复读取数据,无法继续进行。疑为某些数据无法通过校验。
在0x883D57C下断,于此处强行修改$sp+24(长度)后即可通过,得知游戏要检验长度值。
进入sub_0883C808,检查后发现其内部操作是打开文件名所指的文件来获得长度,故而不存在问题。
返回考虑loc_0883D50C,此处检查$s3是否为空指针,如果不是则由$s3+8得到长度,$s3+12得到LBA。
=> 猜测此处为查表操作。(事实上$s3指向的正是前文的Lba_Element结构)
因此将查表操作统一替换即可:将0x883D50C的beqzl $s3, loc_0883D558替换为b loc_0883D558。 (第四步)
运行游戏后一切正常。修改成功。
第二种办法的优点是一次修改即可生效,缺点为操作较第一种而言比较复杂,而且去除其它校验的过程也相对繁琐。
在过程中请多多利用动态调试。
通过这样的工作,我们已经成功的去除了游戏的LBA保护。此时再重新打包数据将不会有任何障碍。
Elysion
May.2009
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment